|
30 | 30 | from opaque_keys.edx.keys import CourseKey |
31 | 31 | from opaque_keys.edx.locator import UsageKey |
32 | 32 | from pytz import UTC |
| 33 | +from rest_framework.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_200_OK, HTTP_404_NOT_FOUND |
33 | 34 | from testfixtures import LogCapture |
| 35 | +from rest_framework.test import APITestCase |
34 | 36 |
|
35 | 37 | from common.djangoapps.course_modes.models import CourseMode |
36 | 38 | from common.djangoapps.course_modes.tests.factories import CourseModeFactory |
|
54 | 56 | CourseBetaTesterRole, |
55 | 57 | CourseDataResearcherRole, |
56 | 58 | CourseFinanceAdminRole, |
57 | | - CourseInstructorRole |
| 59 | + CourseInstructorRole, CourseStaffRole |
58 | 60 | ) |
59 | 61 | from common.djangoapps.student.tests.factories import ( |
60 | 62 | BetaTesterFactory, |
|
88 | 90 | from lms.djangoapps.instructor_task.data import InstructorTaskTypes |
89 | 91 | from lms.djangoapps.instructor_task.models import InstructorTask, InstructorTaskSchedule |
90 | 92 | from lms.djangoapps.program_enrollments.tests.factories import ProgramEnrollmentFactory |
| 93 | +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview |
| 94 | +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory |
91 | 95 | from openedx.core.djangoapps.course_date_signals.handlers import extract_dates |
92 | 96 | from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted |
93 | 97 | from openedx.core.djangoapps.django_comment_common.models import FORUM_ROLE_COMMUNITY_TA, Role |
|
160 | 164 | 'get_issued_certificates', |
161 | 165 | 'instructor_api_v1:list_instructor_tasks', |
162 | 166 | 'instructor_api_v1:list_report_downloads', |
| 167 | + 'instructor_api_v1:course_modes_list', |
163 | 168 | } |
164 | 169 | INSTRUCTOR_POST_ENDPOINTS = { |
165 | 170 | 'add_users_to_cohorts', |
@@ -2694,9 +2699,9 @@ def test_get_problem_responses_successful(self, endpoint, post_data): |
2694 | 2699 | response = self.client.post(url, post_data, content_type="application/json") |
2695 | 2700 | res_json = json.loads(response.content.decode('utf-8')) |
2696 | 2701 | assert 'status' in res_json |
2697 | | - status = res_json['status'] |
2698 | | - assert 'is being created' in status |
2699 | | - assert 'already in progress' not in status |
| 2702 | + state = res_json['status'] |
| 2703 | + assert 'is being created' in state |
| 2704 | + assert 'already in progress' not in state |
2700 | 2705 | assert 'task_id' in res_json |
2701 | 2706 |
|
2702 | 2707 | @valid_problem_location |
@@ -5060,3 +5065,153 @@ def test_end_points_with_oauth_with_permissions(self): |
5060 | 5065 | Verify the endpoint using JWT authentication with permissions. |
5061 | 5066 | """ |
5062 | 5067 | self.run_endpoint_tests(expected_status=200, add_role=True, use_jwt=True) |
| 5068 | + |
| 5069 | + |
| 5070 | +@ddt.ddt |
| 5071 | +class CourseModeListViewTest(SharedModuleStoreTestCase, APITestCase): |
| 5072 | + """ |
| 5073 | + Tests for the CourseModeListView API endpoint. |
| 5074 | + """ |
| 5075 | + |
| 5076 | + def setUp(self): |
| 5077 | + """Set up test data.""" |
| 5078 | + super().setUp() |
| 5079 | + self.course = CourseFactory.create() |
| 5080 | + self.course_overview = CourseOverviewFactory.create( |
| 5081 | + id=self.course.id, |
| 5082 | + org='org' |
| 5083 | + ) |
| 5084 | + self.url = reverse('instructor_api_v1:course_modes_list', kwargs={'course_id': str(self.course.id)}) |
| 5085 | + |
| 5086 | + # Create users |
| 5087 | + self.instructor_user = UserFactory.create() |
| 5088 | + self.staff_user = UserFactory.create() |
| 5089 | + self.student_user = UserFactory.create() |
| 5090 | + self.anonymous_user = None |
| 5091 | + |
| 5092 | + # Assign roles |
| 5093 | + CourseInstructorRole(self.course.id).add_users(self.instructor_user) |
| 5094 | + CourseStaffRole(self.course.id).add_users(self.staff_user) |
| 5095 | + |
| 5096 | + self.mode_audit = None |
| 5097 | + self.exp_dt = None |
| 5098 | + self.mode_verified = None |
| 5099 | + self.status = None |
| 5100 | + |
| 5101 | + def _create_test_modes(self): |
| 5102 | + """Helper to create standard modes for the test course.""" |
| 5103 | + self.mode_audit = CourseModeFactory.create( |
| 5104 | + course=self.course_overview, |
| 5105 | + mode_slug='audit', |
| 5106 | + mode_display_name='Audit', |
| 5107 | + min_price=0, |
| 5108 | + sku='AUDIT-SKU' |
| 5109 | + ) |
| 5110 | + |
| 5111 | + self.exp_dt = (datetime.datetime.now(UTC) + datetime.timedelta(days=30)).replace(microsecond=0) |
| 5112 | + self.mode_verified = CourseModeFactory.create( |
| 5113 | + course=self.course_overview, |
| 5114 | + mode_slug='verified', |
| 5115 | + mode_display_name='Verified', |
| 5116 | + min_price=99, |
| 5117 | + expiration_datetime=self.exp_dt, |
| 5118 | + expiration_datetime_is_explicit=True, |
| 5119 | + sku='VERIFIED-SKU', |
| 5120 | + android_sku='ANDROID-SKU', |
| 5121 | + ios_sku='IOS-SKU', |
| 5122 | + bulk_sku='BULK-SKU' |
| 5123 | + ) |
| 5124 | + |
| 5125 | + # --- Authentication and Permission Tests --- |
| 5126 | + |
| 5127 | + def test_anonymous_user_forbidden(self): |
| 5128 | + """Verify anonymous users receive 401 Unauthorized.""" |
| 5129 | + response = self.client.get(self.url) |
| 5130 | + self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) |
| 5131 | + |
| 5132 | + def test_student_user_forbidden(self): |
| 5133 | + """Verify users without instructor/staff access receive 403 Forbidden.""" |
| 5134 | + self.client.force_authenticate(user=self.student_user) |
| 5135 | + response = self.client.get(self.url) |
| 5136 | + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) |
| 5137 | + |
| 5138 | + def test_course_not_found(self): |
| 5139 | + """Verify a 404 is returned for a non-existent course.""" |
| 5140 | + self.client.force_authenticate(user=self.instructor_user) |
| 5141 | + bad_url = reverse( |
| 5142 | + 'instructor_api_v1:course_modes_list', |
| 5143 | + kwargs={'course_id': 'course-v1:Missing+Org+Run'} |
| 5144 | + ) |
| 5145 | + CourseOverview.objects.filter(id=self.course.id).delete() |
| 5146 | + response = self.client.get(bad_url) |
| 5147 | + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) |
| 5148 | + |
| 5149 | + @ddt.data('instructor_user', 'staff_user') |
| 5150 | + def test_authorized_user_success(self, user_type): |
| 5151 | + """Verify instructors and staff receive 200 OK.""" |
| 5152 | + user = getattr(self, user_type) |
| 5153 | + self.client.force_authenticate(user=user) |
| 5154 | + response = self.client.get(self.url) |
| 5155 | + self.assertEqual(response.status_code, HTTP_200_OK) |
| 5156 | + |
| 5157 | + def test_returns_all_course_modes(self): |
| 5158 | + """ |
| 5159 | + Verify the API returns all modes associated with the course. |
| 5160 | + """ |
| 5161 | + self._create_test_modes() |
| 5162 | + self.client.force_authenticate(user=self.instructor_user) |
| 5163 | + response = self.client.get(self.url) |
| 5164 | + self.assertEqual(response.status_code, HTTP_200_OK) |
| 5165 | + |
| 5166 | + data = response.json() |
| 5167 | + self.assertIn('modes', data) |
| 5168 | + self.assertEqual(len(data['modes']), 2) |
| 5169 | + |
| 5170 | + # Sort by slug to ensure consistent order for comparison |
| 5171 | + modes = sorted(data['modes'], key=lambda x: x['mode_slug']) |
| 5172 | + |
| 5173 | + # Test audit mode |
| 5174 | + self.assertEqual(modes[0]['mode_slug'], 'audit') |
| 5175 | + self.assertEqual(modes[0]['mode_display_name'], 'Audit') |
| 5176 | + self.assertEqual(modes[0]['min_price'], 0) |
| 5177 | + self.assertEqual(modes[0]['currency'], 'usd') |
| 5178 | + self.assertIsNone(modes[0]['expiration_datetime']) |
| 5179 | + self.assertEqual(modes[0]['sku'], 'AUDIT-SKU') |
| 5180 | + self.assertIsNone(modes[0]['bulk_sku']) |
| 5181 | + |
| 5182 | + # Test verified mode |
| 5183 | + self.assertEqual(modes[1]['mode_slug'], 'verified') |
| 5184 | + self.assertEqual(modes[1]['mode_display_name'], 'Verified') |
| 5185 | + self.assertEqual(modes[1]['min_price'], 99) |
| 5186 | + self.assertEqual( |
| 5187 | + modes[1]['expiration_datetime'], |
| 5188 | + self.exp_dt.isoformat().replace('+00:00', 'Z') |
| 5189 | + ) |
| 5190 | + self.assertEqual(modes[1]['sku'], 'VERIFIED-SKU') |
| 5191 | + self.assertEqual(modes[1]['android_sku'], 'ANDROID-SKU') |
| 5192 | + self.assertEqual(modes[1]['ios_sku'], 'IOS-SKU') |
| 5193 | + self.assertEqual(modes[1]['bulk_sku'], 'BULK-SKU') |
| 5194 | + |
| 5195 | + def test_returns_expired_mode(self): |
| 5196 | + """ |
| 5197 | + The API should include expired modes for instructors. |
| 5198 | + """ |
| 5199 | + exp_dt = (datetime.datetime.now(UTC) - datetime.timedelta(days=10)).replace(microsecond=0) |
| 5200 | + CourseModeFactory.create( |
| 5201 | + course=self.course_overview, |
| 5202 | + mode_slug='verified', |
| 5203 | + expiration_datetime=exp_dt, |
| 5204 | + expiration_datetime_is_explicit=True, |
| 5205 | + ) |
| 5206 | + |
| 5207 | + self.client.force_authenticate(user=self.instructor_user) |
| 5208 | + response = self.client.get(self.url) |
| 5209 | + self.assertEqual(response.status_code, HTTP_200_OK) |
| 5210 | + |
| 5211 | + data = response.json() |
| 5212 | + self.assertEqual(len(data['modes']), 1) |
| 5213 | + self.assertEqual(data['modes'][0]['mode_slug'], 'verified') |
| 5214 | + self.assertEqual( |
| 5215 | + data['modes'][0]['expiration_datetime'], |
| 5216 | + exp_dt.isoformat().replace('+00:00', 'Z') |
| 5217 | + ) |
0 commit comments