|
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 | +from rest_framework.status import ( |
| 34 | + HTTP_401_UNAUTHORIZED, |
| 35 | + HTTP_403_FORBIDDEN, |
| 36 | + HTTP_200_OK, |
| 37 | + HTTP_404_NOT_FOUND, |
| 38 | + HTTP_400_BAD_REQUEST, |
| 39 | + HTTP_204_NO_CONTENT, |
| 40 | +) |
34 | 41 | from testfixtures import LogCapture |
35 | 42 | from rest_framework.test import APITestCase |
36 | 43 |
|
|
165 | 172 | 'instructor_api_v1:list_instructor_tasks', |
166 | 173 | 'instructor_api_v1:list_report_downloads', |
167 | 174 | 'instructor_api_v1:course_modes_list', |
| 175 | + 'instructor_api_v1:course_mode_price', |
168 | 176 | } |
169 | 177 | INSTRUCTOR_POST_ENDPOINTS = { |
170 | 178 | 'add_users_to_cohorts', |
@@ -5215,3 +5223,193 @@ def test_returns_expired_mode(self): |
5215 | 5223 | data['modes'][0]['expiration_datetime'], |
5216 | 5224 | exp_dt.isoformat().replace('+00:00', 'Z') |
5217 | 5225 | ) |
| 5226 | + |
| 5227 | + |
| 5228 | +@ddt.ddt |
| 5229 | +class TestCourseModePriceView(SharedModuleStoreTestCase, APITestCase): |
| 5230 | + """ |
| 5231 | + Test suite for the CourseModePriceView PATCH endpoint. |
| 5232 | +
|
| 5233 | + This suite tests the view with the permission class |
| 5234 | + (IsAuthenticated, permissions.InstructorPermission). |
| 5235 | + """ |
| 5236 | + |
| 5237 | + def setUp(self): |
| 5238 | + """Set up the test environment.""" |
| 5239 | + super().setUp() |
| 5240 | + |
| 5241 | + self.course = CourseFactory.create() |
| 5242 | + self.course_overview = CourseOverviewFactory.create( |
| 5243 | + id=self.course.id, |
| 5244 | + org='org' |
| 5245 | + ) |
| 5246 | + |
| 5247 | + self.staff_user = UserFactory(is_staff=True) |
| 5248 | + self.student_user = UserFactory() |
| 5249 | + self.instructor_user = UserFactory() |
| 5250 | + self.staff_user = UserFactory() |
| 5251 | + |
| 5252 | + CourseInstructorRole(self.course.id).add_users(self.instructor_user) |
| 5253 | + CourseStaffRole(self.course.id).add_users(self.staff_user) |
| 5254 | + |
| 5255 | + self.verified_mode = CourseModeFactory( |
| 5256 | + course_id=self.course_overview.id, |
| 5257 | + mode_slug='verified', |
| 5258 | + min_price=4900, # $49.00 |
| 5259 | + currency='USD' |
| 5260 | + ) |
| 5261 | + |
| 5262 | + self.url = reverse('instructor_api_v1:course_mode_price', kwargs={ |
| 5263 | + 'course_id': self.course.id, |
| 5264 | + 'mode_slug': self.verified_mode.mode_slug |
| 5265 | + }) |
| 5266 | + |
| 5267 | + self.valid_payload = {'price': 3900} # $39.00 |
| 5268 | + |
| 5269 | + @ddt.data('instructor_user', 'staff_user') |
| 5270 | + def test_update_price_success_as_instructor(self, user_type): |
| 5271 | + """ |
| 5272 | + [204] Test successful price update by an authenticated instructor. |
| 5273 | + """ |
| 5274 | + # Authenticate as the instructor |
| 5275 | + self.client.force_authenticate(user=getattr(self, user_type)) |
| 5276 | + |
| 5277 | + response = self.client.patch( |
| 5278 | + self.url, |
| 5279 | + data=self.valid_payload, |
| 5280 | + format='json' |
| 5281 | + ) |
| 5282 | + |
| 5283 | + # 1. Check for 204 No Content response |
| 5284 | + self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) |
| 5285 | + # 2. Verify the price was *actually* changed in the database |
| 5286 | + self.verified_mode.refresh_from_db() |
| 5287 | + self.assertEqual(self.verified_mode.min_price, self.valid_payload['price']) |
| 5288 | + |
| 5289 | + def test_update_price_forbidden_as_student(self): |
| 5290 | + """ |
| 5291 | + [403] Test that a non-instructor (student) is forbidden. |
| 5292 | + """ |
| 5293 | + # Authenticate as the student |
| 5294 | + self.client.force_authenticate(user=self.student_user) |
| 5295 | + |
| 5296 | + response = self.client.patch( |
| 5297 | + self.url, |
| 5298 | + data=self.valid_payload, |
| 5299 | + format='json' |
| 5300 | + ) |
| 5301 | + |
| 5302 | + # 1. Check for 403 Forbidden response |
| 5303 | + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) |
| 5304 | + |
| 5305 | + # 2. Verify the price was *not* changed |
| 5306 | + self.verified_mode.refresh_from_db() |
| 5307 | + self.assertEqual(self.verified_mode.min_price, 4900) # Original price |
| 5308 | + |
| 5309 | + def test_update_price_unauthenticated(self): |
| 5310 | + """ |
| 5311 | + [401] Test that an unauthenticated user is unauthorized. |
| 5312 | + """ |
| 5313 | + |
| 5314 | + response = self.client.patch( |
| 5315 | + self.url, |
| 5316 | + data=self.valid_payload, |
| 5317 | + format='json' |
| 5318 | + ) |
| 5319 | + |
| 5320 | + # 1. Check for 401 Unauthorized response |
| 5321 | + self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) |
| 5322 | + |
| 5323 | + # 2. Verify the price was *not* changed |
| 5324 | + self.verified_mode.refresh_from_db() |
| 5325 | + self.assertEqual(self.verified_mode.min_price, 4900) |
| 5326 | + |
| 5327 | + def test_update_price_course_not_found(self): |
| 5328 | + """ |
| 5329 | + [404] Test request for a non-existent course_key. |
| 5330 | + """ |
| 5331 | + self.client.force_authenticate(user=self.instructor_user) |
| 5332 | + |
| 5333 | + invalid_url = reverse('instructor_api_v1:course_mode_price', kwargs={ |
| 5334 | + 'course_id': 'course-v1:FakeOrg+Nope+123', |
| 5335 | + 'mode_slug': 'non-existent-mode' |
| 5336 | + }) |
| 5337 | + |
| 5338 | + response = self.client.patch( |
| 5339 | + invalid_url, |
| 5340 | + data=self.valid_payload, |
| 5341 | + format='json' |
| 5342 | + ) |
| 5343 | + |
| 5344 | + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) |
| 5345 | + |
| 5346 | + def test_update_price_mode_not_found(self): |
| 5347 | + """ |
| 5348 | + [404] Test request for a non-existent mode_slug for the given course. |
| 5349 | + """ |
| 5350 | + self.client.force_authenticate(user=self.instructor_user) |
| 5351 | + |
| 5352 | + invalid_url = reverse('instructor_api_v1:course_mode_price', kwargs={ |
| 5353 | + 'course_id': self.course.id, |
| 5354 | + 'mode_slug': 'non-existent-mode' |
| 5355 | + }) |
| 5356 | + response = self.client.patch( |
| 5357 | + invalid_url, |
| 5358 | + data=self.valid_payload, |
| 5359 | + format='json' |
| 5360 | + ) |
| 5361 | + |
| 5362 | + self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) |
| 5363 | + |
| 5364 | + def test_bad_request_missing_price_field(self): |
| 5365 | + """ |
| 5366 | + [400] Test request with a missing 'price' field in the body. |
| 5367 | + """ |
| 5368 | + self.client.force_authenticate(user=self.instructor_user) |
| 5369 | + |
| 5370 | + invalid_payload = {'not_price': 123} |
| 5371 | + |
| 5372 | + response = self.client.patch( |
| 5373 | + self.url, |
| 5374 | + data=invalid_payload, |
| 5375 | + format='json' |
| 5376 | + ) |
| 5377 | + |
| 5378 | + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) |
| 5379 | + # Check that the error response correctly identifies the missing field |
| 5380 | + self.assertIn('price', response.data) |
| 5381 | + self.assertEqual(str(response.data['price'][0]), 'This field is required.') |
| 5382 | + |
| 5383 | + def test_bad_request_invalid_price_negative(self): |
| 5384 | + """ |
| 5385 | + [400] Test request with an invalid negative 'price'. |
| 5386 | + """ |
| 5387 | + self.client.force_authenticate(user=self.instructor_user) |
| 5388 | + |
| 5389 | + invalid_payload = {'price': -100} |
| 5390 | + |
| 5391 | + response = self.client.patch( |
| 5392 | + self.url, |
| 5393 | + data=invalid_payload, |
| 5394 | + format='json' |
| 5395 | + ) |
| 5396 | + |
| 5397 | + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) |
| 5398 | + self.assertIn('price', response.data) |
| 5399 | + |
| 5400 | + def test_bad_request_invalid_price_string(self): |
| 5401 | + """ |
| 5402 | + [400] Test request with an invalid string 'price'. |
| 5403 | + """ |
| 5404 | + self.client.force_authenticate(user=self.instructor_user) |
| 5405 | + |
| 5406 | + invalid_payload = {'price': 'one-hundred'} |
| 5407 | + |
| 5408 | + response = self.client.patch( |
| 5409 | + self.url, |
| 5410 | + data=invalid_payload, |
| 5411 | + format='json' |
| 5412 | + ) |
| 5413 | + |
| 5414 | + self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) |
| 5415 | + self.assertIn('price', response.data) |
0 commit comments