def test_format_and_parse(self): user = UserFactory() body = 'This is a test body.' course_run_key = 'course-v1:testX+TestX+Test' with mock.patch(self.salesforce_path): util = SalesforceUtil(self.salesforce_config.partner) formatted_message = util.format_user_comment_body( user, body, course_run_key) expected_formatted_message = '[User]\n{}\n\n[Course Run]\n{}\n\n[Body]\n{}'.format( '{} {} ({})'.format(user.first_name, user.last_name, user.username), course_run_key, body) assert formatted_message == expected_formatted_message parsed_message = util._parse_user_comment_body( # pylint: disable=protected-access {'Body': formatted_message}) parsed_user = parsed_message.get('user') assert parsed_user.get('username') == user.username # Below 3 will always be None for a matched comment assert parsed_user.get('email') is None assert parsed_user.get('first_name') is None assert parsed_user.get('last_name') is None assert parsed_message.get('course_run_key') == course_run_key assert parsed_message.get('comment') == body user.first_name = '' user.last_name = '' user.save() formatted_message = util.format_user_comment_body( user, body, course_run_key) expected_formatted_message = '[User]\n{}\n\n[Course Run]\n{}\n\n[Body]\n{}'.format( '{}'.format(user.username), course_run_key, body) assert formatted_message == expected_formatted_message
class RateLimitingTest(SiteMixin, APITestCase): """ Testing rate limiting of API calls. """ def setUp(self): super(RateLimitingTest, self).setUp() self.url = reverse('api_docs') self.user = UserFactory() self.client.login(username=self.user.username, password=USER_PASSWORD) def tearDown(self): """ Clear the cache, since DRF uses it for recording requests against a URL. Django does not clear the cache between test runs. """ super(RateLimitingTest, self).tearDown() cache.clear() def _make_requests(self): """ Make multiple requests until the throttle's limit is exceeded. Returns Response: Response of the last request. """ num_requests = OverridableUserRateThrottle().num_requests for __ in range(num_requests + 1): response = self.client.get(self.url) return response def test_rate_limiting(self): """ Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """ response = self._make_requests() assert response.status_code == 429 def test_user_throttle_rate(self): """ Verify the UserThrottleRate can be used to override the default rate limit. """ UserThrottleRate.objects.create(user=self.user, rate='1000/day') self.assert_rate_limit_successfully_exceeded() def assert_rate_limit_successfully_exceeded(self): """ Asserts that the throttle's rate limit can be exceeded without encountering an error. """ response = self._make_requests() assert response.status_code == 200 def test_superuser_throttling(self): """ Verify superusers are not throttled. """ self.user.is_superuser = True self.user.save() self.assert_rate_limit_successfully_exceeded() def test_staff_throttling(self): """ Verify staff users are not throttled. """ self.user.is_staff = True self.user.save() self.assert_rate_limit_successfully_exceeded()
class RateLimitingTest(APITestCase): """ Testing rate limiting of API calls. """ def setUp(self): super(RateLimitingTest, self).setUp() self.url = reverse('django.swagger.resources.view') self.user = UserFactory() self.client.login(username=self.user.username, password=USER_PASSWORD) def tearDown(self): """ Clear the cache, since DRF uses it for recording requests against a URL. Django does not clear the cache between test runs. """ super(RateLimitingTest, self).tearDown() cache.clear() def _make_requests(self): """ Make multiple requests until the throttle's limit is exceeded. Returns Response: Response of the last request. """ num_requests = OverridableUserRateThrottle().num_requests for __ in range(num_requests + 1): response = self.client.get(self.url) return response def test_rate_limiting(self): """ Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """ response = self._make_requests() self.assertEqual(response.status_code, 429) def test_user_throttle_rate(self): """ Verify the UserThrottleRate can be used to override the default rate limit. """ UserThrottleRate.objects.create(user=self.user, rate='1000/day') self.assert_rate_limit_successfully_exceeded() def assert_rate_limit_successfully_exceeded(self): """ Asserts that the throttle's rate limit can be exceeded without encountering an error. """ response = self._make_requests() self.assertEqual(response.status_code, 200) def test_superuser_throttling(self): """ Verify superusers are not throttled. """ self.user.is_superuser = True self.user.save() self.assert_rate_limit_successfully_exceeded() def test_staff_throttling(self): """ Verify staff users are not throttled. """ self.user.is_staff = True self.user.save() self.assert_rate_limit_successfully_exceeded()
class UserAutocompleteTests(SiteMixin, TestCase): """ Tests for user autocomplete lookups.""" def setUp(self): super(UserAutocompleteTests, self).setUp() self.user = UserFactory(username='******', is_staff=True) self.client.login(username=self.user.username, password=USER_PASSWORD) self.users_list = UserFactory.create_batch(5) def test_user_autocomplete(self): """ Verify user autocomplete returns the data. """ response = self.client.get( reverse('admin_core:user-autocomplete') + '?q={user}'.format(user='******')) self._assert_response(response, 5) # update first user's username self.users_list[0].username = '******' self.users_list[0].save() response = self.client.get( reverse('admin_core:user-autocomplete') + '?q={user}'.format(user='******')) self._assert_response(response, 1) def test_course_autocomplete_un_authorize_user(self): """ Verify user autocomplete returns empty list for un-authorized users. """ self.client.logout() self.user.is_staff = False self.user.save() self.client.login(username=self.user.username, password=USER_PASSWORD) response = self.client.get(reverse('admin_core:user-autocomplete')) self._assert_response(response, 0) def _assert_response(self, response, expected_length): """ Assert autocomplete response. """ self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode('utf-8')) self.assertEqual(len(data['results']), expected_length)
class CourseRunViewSetTests(SerializationMixin, ElasticsearchTestMixin, OAuth2Mixin, APITestCase): def setUp(self): super(CourseRunViewSetTests, self).setUp() self.user = UserFactory(is_staff=True) self.client.force_authenticate(self.user) self.course_run = CourseRunFactory(course__partner=self.partner) self.course_run.course.authoring_organizations.add( OrganizationFactory(key='course-id')) self.course_run_2 = CourseRunFactory(course__key='Test+Course', course__partner=self.partner) self.refresh_index() self.request = APIRequestFactory().get('/') self.request.user = self.user def mock_patch_to_studio(self, key, access_token=True, status=200): if access_token: self.mock_access_token() studio_url = '{root}/api/v1/course_runs/{key}/'.format( root=self.partner.studio_url.strip('/'), key=key) responses.add(responses.PATCH, studio_url, status=status) responses.add(responses.POST, '{url}images/'.format(url=studio_url), status=status) def mock_post_to_studio(self, key, access_token=True): if access_token: self.mock_access_token() studio_url = '{root}/api/v1/course_runs/'.format( root=self.partner.studio_url.strip('/')) responses.add(responses.POST, studio_url, status=200) responses.add(responses.POST, '{url}{key}/images/'.format(url=studio_url, key=key), status=200) def test_get(self): """ Verify the endpoint returns the details for a single course. """ url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) with self.assertNumQueries(11): response = self.client.get(url) assert response.status_code == 200 self.assertEqual(response.data, self.serialize_course_run(self.course_run)) def test_get_exclude_deleted_programs(self): """ Verify the endpoint returns no associated deleted programs """ ProgramFactory(courses=[self.course_run.course], status=ProgramStatus.Deleted) url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) with self.assertNumQueries(12): response = self.client.get(url) assert response.status_code == 200 assert response.data.get('programs') == [] def test_get_include_deleted_programs(self): """ Verify the endpoint returns associated deleted programs with the 'include_deleted_programs' flag set to True """ ProgramFactory(courses=[self.course_run.course], status=ProgramStatus.Deleted) url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) url += '?include_deleted_programs=1' with self.assertNumQueries(14): response = self.client.get(url) assert response.status_code == 200 assert response.data == \ self.serialize_course_run(self.course_run, extra_context={'include_deleted_programs': True}) def test_get_exclude_unpublished_programs(self): """ Verify the endpoint returns no associated unpublished programs """ ProgramFactory(courses=[self.course_run.course], status=ProgramStatus.Unpublished) url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) with self.assertNumQueries(12): response = self.client.get(url) assert response.status_code == 200 assert response.data.get('programs') == [] def test_get_include_unpublished_programs(self): """ Verify the endpoint returns associated unpublished programs with the 'include_unpublished_programs' flag set to True """ ProgramFactory(courses=[self.course_run.course], status=ProgramStatus.Unpublished) url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) url += '?include_unpublished_programs=1' with self.assertNumQueries(14): response = self.client.get(url) assert response.status_code == 200 assert response.data == \ self.serialize_course_run(self.course_run, extra_context={'include_unpublished_programs': True}) @responses.activate def test_create_minimum(self): """ Verify the endpoint supports creating a course_run with the least info. """ course = self.course_run.course new_key = 'course-v1:{}+1T2000'.format(course.key.replace('/', '+')) self.mock_post_to_studio(new_key) url = reverse('api:v1:course_run-list') # Send nothing - expect complaints response = self.client.post(url, {}, format='json') self.assertEqual(response.status_code, 400) self.assertDictEqual( response.data, { 'course': ['This field is required.'], 'start': ['This field is required.'], 'end': ['This field is required.'], }) # Send minimum requested response = self.client.post(url, { 'course': course.key, 'start': '2000-01-01T00:00:00Z', 'end': '2001-01-01T00:00:00Z', }, format='json') self.assertEqual(response.status_code, 201) new_course_run = CourseRun.objects.get(key=new_key) self.assertDictEqual(response.data, self.serialize_course_run(new_course_run)) self.assertEqual(new_course_run.pacing_type, 'instructor_paced') # default we provide self.assertEqual( str(new_course_run.end), '2001-01-01 00:00:00+00:00') # spot check that input made it @responses.activate def test_create_with_key(self): """ Verify the endpoint supports creating a course_run when specifying a key (if allowed). """ course = self.course_run.course date_key = 'course-v1:{}+1T2000'.format(course.key.replace('/', '+')) desired_key = 'course-v1:{}+HowdyDoing'.format( course.key.replace('/', '+')) url = reverse('api:v1:course_run-list') data = { 'course': course.key, 'start': '2000-01-01T00:00:00Z', 'end': '2001-01-01T00:00:00Z', 'key': desired_key, } # If org doesn't specifically allow it, incoming key is ignored self.mock_post_to_studio(date_key) response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, 201) new_course_run = CourseRun.objects.get(key=date_key) self.assertDictEqual(response.data, self.serialize_course_run(new_course_run)) # Turn on this feature for this org, notice that we can now specify the course key we want org_ext = OrganizationExtensionFactory( organization=course.authoring_organizations.first()) org_ext.auto_create_in_studio = False # badly named, but this controls whether we let org name their keys org_ext.save() self.mock_post_to_studio(desired_key, access_token=False) response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, 201) new_course_run = CourseRun.objects.get(key=desired_key) self.assertDictEqual(response.data, self.serialize_course_run(new_course_run)) def test_create_if_in_org(self): """ Verify the endpoint supports creating a course_run with organization permissions. """ url = reverse('api:v1:course_run-list') course = self.course_run.course data = {'course': course.key} self.user.is_staff = False self.user.save() # Not in org, not allowed to POST response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, 403) # Add to org org_ext = OrganizationExtensionFactory( organization=course.authoring_organizations.first()) self.user.groups.add(org_ext.group) # now allowed to POST response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, 400) # missing start, but at least we got that far @responses.activate def test_partial_update(self): """ Verify the endpoint supports partially updating a course_run's fields, provided user has permission. """ self.mock_patch_to_studio(self.course_run.key) url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) expected_min_effort = 867 expected_max_effort = 5309 data = { 'max_effort': expected_max_effort, 'min_effort': expected_min_effort, } # Update this course_run with the new info response = self.client.patch(url, data, format='json') assert response.status_code == 200 # refresh and make sure we have the new effort levels self.course_run.refresh_from_db() assert self.course_run.max_effort == expected_max_effort assert self.course_run.min_effort == expected_min_effort def test_partial_update_no_studio_url(self): """ Verify we skip pushing when no studio url is set. """ self.partner.studio_url = None self.partner.save() url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) with mock.patch( 'course_discovery.apps.api.v1.views.course_runs.log.info' ) as mock_logger: response = self.client.patch(url, {}, format='json') self.assertEqual(response.status_code, 200) mock_logger.assert_called_with( 'Not pushing course run info for %s to Studio as partner %s has no studio_url set.', self.course_run.key, self.partner.short_code, ) def test_partial_update_bad_permission(self): """ Verify partially updating will fail if user doesn't have permission. """ user = UserFactory(is_staff=False, is_superuser=False) self.client.force_authenticate(user) url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) response = self.client.patch(url, {}, format='json') assert response.status_code == 403 @ddt.data( ( { 'start': '2010-01-01T00:00:00Z', 'end': '2000-01-01T00:00:00Z' }, 'Start date cannot be after the End date', ), ( { 'key': 'course-v1:Blarg+Hello+Run' }, 'Key cannot be changed', ), ( { 'course': 'Test+Course' }, 'Course cannot be changed', ), ( { 'min_effort': 10000 }, 'Minimum effort cannot be greater than Maximum effort', ), ( { 'min_effort': 10000, 'max_effort': 10000 }, 'Minimum effort and Maximum effort cannot be the same', ), ( { 'max_effort': None }, 'Maximum effort cannot be empty', ), ) @ddt.unpack def test_partial_update_common_errors(self, data, error): """ Verify partially updating will fail depending on various validation checks. """ url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) response = self.client.patch(url, data, format='json') self.assertContains(response, error, status_code=400) def test_partial_update_staff(self): """ Verify partially updating allows staff updates. """ self.mock_patch_to_studio(self.course_run.key) p1 = PersonFactory() p2 = PersonFactory() PersonFactory() url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) response = self.client.patch(url, {'staff': [p2.uuid, p1.uuid]}, format='json') self.assertEqual(response.status_code, 200) self.course_run.refresh_from_db() self.assertListEqual(list(self.course_run.staff.all()), [p2, p1]) @responses.activate def test_partial_update_video(self): """ Verify partially updating allows video updates. """ self.mock_patch_to_studio(self.course_run.key) url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) response = self.client.patch( url, {'video': { 'src': 'https://example.com/blarg' }}, format='json') self.assertEqual(response.status_code, 200) self.course_run.refresh_from_db() self.assertEqual(self.course_run.video.src, 'https://example.com/blarg') @responses.activate def test_update_if_editor(self): """ Verify the endpoint supports updating a course_run with editor permissions. """ self.mock_patch_to_studio(self.course_run.key) url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) self.user.is_staff = False self.user.save() # Not an editor, not allowed to patch response = self.client.patch(url, {}, format='json') self.assertEqual(response.status_code, 403) # Add as editor org_ext = OrganizationExtensionFactory( organization=self.course_run.course.authoring_organizations.first( )) self.user.groups.add(org_ext.group) CourseEditorFactory(user=self.user, course=self.course_run.course) # now allowed to patch response = self.client.patch(url, {}, format='json') self.assertEqual(response.status_code, 200) @responses.activate def test_studio_update_failure(self): """ Verify we bubble up error correctly if studio is giving us static. """ self.mock_patch_to_studio(self.course_run.key, status=400) url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) response = self.client.patch(url, {'title': 'New Title'}, format='json') self.assertContains(response, 'Failed to set course run data: Client Error 400', status_code=400) self.course_run.refresh_from_db() self.assertEqual(self.course_run.title_override, None) # prove we didn't touch the course run object @responses.activate def test_full_update(self): """ Verify full updating is allowed. """ self.mock_patch_to_studio(self.course_run.key) url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) response = self.client.put( url, { 'course': self.course_run.course.key, # required, so we need for a put 'start': self.course_run.start, # required, so we need for a put 'end': self.course_run.end, # required, so we need for a put 'title': 'New Title', }, format='json') self.assertEqual(response.status_code, 200) self.course_run.refresh_from_db() self.assertEqual(self.course_run.title_override, 'New Title') @ddt.data( CourseRunStatus.LegalReview, CourseRunStatus.InternalReview, ) def test_patch_put_restrict_when_reviewing(self, status): self.course_run.status = status self.course_run.save() url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) response = self.client.put( url, { 'course': self.course_run.course.key, # required, so we need for a put 'start': self.course_run.start, # required, so we need for a put 'end': self.course_run.end, # required, so we need for a put }, format='json') assert response.status_code == 403 response = self.client.patch(url, {}, format='json') assert response.status_code == 403 @responses.activate def test_patch_put_reset_status(self): self.mock_patch_to_studio(self.course_run.key) self.course_run.status = CourseRunStatus.Reviewed self.course_run.save() url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) response = self.client.put( url, { 'course': self.course_run.course.key, # required, so we need for a put 'start': self.course_run.start, # required, so we need for a put 'end': self.course_run.end, # required, so we need for a put 'status': 'reviewed', }, format='json') assert response.status_code == 200 self.course_run.refresh_from_db() assert self.course_run.status == CourseRunStatus.Unpublished def test_list(self): """ Verify the endpoint returns a list of all course runs. """ url = reverse('api:v1:course_run-list') with self.assertNumQueries(13): response = self.client.get(url) assert response.status_code == 200 self.assertListEqual( response.data['results'], self.serialize_course_run(CourseRun.objects.all().order_by( Lower('key')), many=True)) def test_list_sorted_by_course_start_date(self): """ Verify the endpoint returns a list of all course runs sorted by start date. """ url = '{root}?ordering=start'.format( root=reverse('api:v1:course_run-list')) with self.assertNumQueries(13): response = self.client.get(url) assert response.status_code == 200 self.assertListEqual( response.data['results'], self.serialize_course_run( CourseRun.objects.all().order_by('start'), many=True)) def test_list_query(self): """ Verify the endpoint returns a filtered list of courses """ course_runs = CourseRunFactory.create_batch( 3, title='Some random title', course__partner=self.partner) CourseRunFactory(title='non-matching name') query = 'title:Some random title' url = '{root}?q={query}'.format(root=reverse('api:v1:course_run-list'), query=query) with self.assertNumQueries(39): response = self.client.get(url) actual_sorted = sorted(response.data['results'], key=lambda course_run: course_run['key']) expected_sorted = sorted(self.serialize_course_run(course_runs, many=True), key=lambda course_run: course_run['key']) self.assertListEqual(actual_sorted, expected_sorted) def assert_list_results(self, url, expected, extra_context=None): expected = sorted(expected, key=lambda course_run: course_run.key.lower()) response = self.client.get(url) assert response.status_code == 200 self.assertListEqual( response.data['results'], self.serialize_course_run(expected, many=True, extra_context=extra_context)) def test_filter_by_keys(self): """ Verify the endpoint returns a list of course runs filtered by the specified keys. """ CourseRun.objects.all().delete() expected = CourseRunFactory.create_batch(3, course__partner=self.partner) keys = ','.join([course.key for course in expected]) url = '{root}?keys={keys}'.format( root=reverse('api:v1:course_run-list'), keys=keys) self.assert_list_results(url, expected) def test_filter_by_marketable(self): """ Verify the endpoint filters course runs to those that are marketable. """ CourseRun.objects.all().delete() expected = CourseRunFactory.create_batch(3, course__partner=self.partner) for course_run in expected: SeatFactory(course_run=course_run) CourseRunFactory.create_batch(3, slug=None, course__partner=self.partner) CourseRunFactory.create_batch(3, slug='', course__partner=self.partner) url = reverse('api:v1:course_run-list') + '?marketable=1' self.assert_list_results(url, expected) def test_filter_by_hidden(self): """ Verify the endpoint filters course runs that are hidden. """ CourseRun.objects.all().delete() course_runs = CourseRunFactory.create_batch( 3, course__partner=self.partner) hidden_course_runs = CourseRunFactory.create_batch( 3, hidden=True, course__partner=self.partner) url = reverse('api:v1:course_run-list') self.assert_list_results(url, course_runs + hidden_course_runs) url = reverse('api:v1:course_run-list') + '?hidden=False' self.assert_list_results(url, course_runs) def test_filter_by_active(self): """ Verify the endpoint filters course runs to those that are active. """ CourseRun.objects.all().delete() # Create course with end date in future and enrollment_end in past. end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2) enrollment_end = datetime.datetime.now( pytz.UTC) - datetime.timedelta(days=1) CourseRunFactory(end=end, enrollment_end=enrollment_end, course__partner=self.partner) # Create course with end date in past and no enrollment_end. end = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=2) CourseRunFactory(end=end, enrollment_end=None, course__partner=self.partner) # Create course with end date in future and enrollment_end in future. end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2) enrollment_end = datetime.datetime.now( pytz.UTC) + datetime.timedelta(days=1) active_enrollment_end = CourseRunFactory(end=end, enrollment_end=enrollment_end, course__partner=self.partner) # Create course with end date in future and no enrollment_end. active_no_enrollment_end = CourseRunFactory( end=end, enrollment_end=None, course__partner=self.partner) expected = [active_enrollment_end, active_no_enrollment_end] url = reverse('api:v1:course_run-list') + '?active=1' self.assert_list_results(url, expected) def test_filter_by_license(self): CourseRun.objects.all().delete() course_runs_cc = CourseRunFactory.create_batch( 3, course__partner=self.partner, license='cc-by-sa') CourseRunFactory.create_batch(3, course__partner=self.partner, license='') url = reverse('api:v1:course_run-list') + '?license=cc-by-sa' self.assert_list_results(url, course_runs_cc) def test_list_exclude_utm(self): """ Verify the endpoint returns marketing URLs without UTM parameters. """ url = reverse('api:v1:course_run-list') + '?exclude_utm=1' self.assert_list_results(url, CourseRun.objects.all(), extra_context={'exclude_utm': 1}) def test_contains_single_course_run(self): """ Verify that a single course_run is contained in a query """ qs = urllib.parse.urlencode({ 'query': 'id:course*', 'course_run_ids': self.course_run.key, }) url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs) response = self.client.get(url) assert response.status_code == 200 self.assertEqual(response.data, {'course_runs': { self.course_run.key: True }}) def test_contains_multiple_course_runs(self): qs = urllib.parse.urlencode({ 'query': 'id:course*', 'course_run_ids': '{},{},{}'.format(self.course_run.key, self.course_run_2.key, 'abc') }) url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs) response = self.client.get(url) assert response.status_code == 200 self.assertDictEqual( response.data, { 'course_runs': { self.course_run.key: True, self.course_run_2.key: True, 'abc': False } }) @ddt.data({'params': { 'course_run_ids': 'a/b/c' }}, {'params': { 'query': 'id:course*' }}, {'params': {}}) @ddt.unpack def test_contains_missing_parameter(self, params): qs = urllib.parse.urlencode(params) url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs) response = self.client.get(url) assert response.status_code == 400 def test_options(self): url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key}) response = self.client.options(url) self.assertEqual(response.status_code, 200) data = response.data['actions']['PUT'] self.assertEqual(data['level_type']['choices'], [{ 'display_name': self.course_run.level_type.name, 'value': self.course_run.level_type.name }, { 'display_name': self.course_run_2.level_type.name, 'value': self.course_run_2.level_type.name }]) self.assertEqual(data['content_language']['choices'], [{ 'display_name': x.name, 'value': x.code } for x in LanguageTag.objects.all()]) self.assertTrue(LanguageTag.objects.count() > 0)
class AutocompleteTests(TestCase): """ Tests for autocomplete lookups.""" def setUp(self): super(AutocompleteTests, self).setUp() self.user = UserFactory(is_staff=True) self.client.login(username=self.user.username, password=USER_PASSWORD) self.courses = factories.CourseFactory.create_batch(3, title='Some random course title') for course in self.courses: factories.CourseRunFactory(course=course) self.organizations = factories.OrganizationFactory.create_batch(3) first_instructor = factories.PersonFactory(given_name="First Instructor") second_instructor = factories.PersonFactory(given_name="Second Instructor") self.instructors = [first_instructor, second_instructor] @ddt.data('dum', 'ing') def test_course_autocomplete(self, search_key): """ Verify course autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:course-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 3) # update the first course title self.courses[0].key = 'edx/dummy/key' self.courses[0].title = 'this is some thing new' self.courses[0].save() response = self.client.get( reverse('admin_metadata:course-autocomplete') + '?q={title}'.format(title=search_key) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(self.courses[0])) def test_course_autocomplete_un_authorize_user(self): """ Verify course autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:course-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) @ddt.data('ing', 'dum') def test_course_run_autocomplete(self, search_key): """ Verify course run autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:course-run-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 3) # update the first course title course = self.courses[0] course.title = 'this is some thing new' course.save() course_run = self.courses[0].course_runs.first() course_run.key = 'edx/dummy/testrun' course_run.save() response = self.client.get( reverse('admin_metadata:course-run-autocomplete') + '?q={q}'.format(q=search_key) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(course_run)) def test_course_run_autocomplete_un_authorize_user(self): """ Verify course run autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:course-run-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) @ddt.data('irc', 'ing') def test_organization_autocomplete(self, search_key): """ Verify Organization autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:organisation-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 3) self.organizations[0].key = 'Mirco' self.organizations[0].name = 'testing name' self.organizations[0].save() response = self.client.get( reverse('admin_metadata:organisation-autocomplete') + '?q={key}'.format( key=search_key ) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(self.organizations[0])) self.assertEqual(len(data['results']), 1) def test_organization_autocomplete_un_authorize_user(self): """ Verify Organization autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:organisation-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) @ddt.data('dummyurl', 'testing') def test_video_autocomplete(self, search_key): """ Verify video autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:video-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 6) self.courses[0].video.src = 'http://www.youtube.com/dummyurl' self.courses[0].video.description = 'testing description' self.courses[0].video.save() response = self.client.get( reverse('admin_metadata:video-autocomplete') + '?q={key}'.format( key=search_key ) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(self.courses[0].video)) self.assertEqual(len(data['results']), 1) def test_video_autocomplete_un_authorize_user(self): """ Verify video autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:video-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) def _make_user_non_staff(self): self.client.logout() self.user = UserFactory(is_staff=False) self.user.save() self.client.login(username=self.user.username, password=USER_PASSWORD) def test_instructor_autocomplete(self): """ Verify instructor autocomplete returns the data. """ response = self.client.get( reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='ins') ) self._assert_response(response, 2) # update first instructor's name self.instructors[0].given_name = 'dummy_name' self.instructors[0].save() response = self.client.get( reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='dummy') ) self._assert_response(response, 1) def test_instructor_autocomplete_un_authorize_user(self): """ Verify instructor autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:person-autocomplete')) self._assert_response(response, 0) def test_instructor_position_in_label(self): """ Verify that instructor label contains position of instructor if it exists.""" position_title = 'professor' PositionFactory.create(person=self.instructors[0], title=position_title, organization=self.organizations[0]) response = self.client.get( reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='ins') ) self.assertContains(response, '<p>{position} at {organization}</p>'.format( position=position_title, organization=self.organizations[0].name)) def test_instructor_image_in_label(self): """ Verify that instructor label contains profile image url.""" response = self.client.get( reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='ins') ) self.assertContains(response, self.instructors[0].get_profile_image_url) self.assertContains(response, self.instructors[1].get_profile_image_url) def _assert_response(self, response, expected_length): """ Assert autocomplete response. """ self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode('utf-8')) self.assertEqual(len(data['results']), expected_length) def test_instructor_autocomplete_with_uuid(self): """ Verify instructor autocomplete returns the data with valid uuid. """ uuid = self.instructors[0].uuid response = self.client.get( reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q=uuid) ) self._assert_response(response, 1) def test_instructor_autocomplete_with_invalid_uuid(self): """ Verify instructor autocomplete returns empty list without giving error. """ uuid = 'invalid-uuid' response = self.client.get( reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q=uuid) ) self._assert_response(response, 0)
class AutoCompletePersonTests(SiteMixin, TestCase): """ Tests for person autocomplete lookups """ def setUp(self): super(AutoCompletePersonTests, self).setUp() self.user = UserFactory(is_staff=True) self.client.login(username=self.user.username, password=USER_PASSWORD) self.courses = factories.CourseFactory.create_batch( 3, title='Some random course title') for course in self.courses: factories.CourseRunFactory(course=course) self.organizations = OrganizationFactory.create_batch(3) self.organization_extensions = [] for organization in self.organizations: self.organization_extensions.append( factories.OrganizationExtensionFactory( organization=organization)) self.user.groups.add(self.organization_extensions[0].group) first_instructor = PersonFactory(given_name="First", family_name="Instructor") second_instructor = PersonFactory(given_name="Second", family_name="Instructor") self.instructors = [first_instructor, second_instructor] for instructor in self.instructors: PositionFactory(organization=self.organizations[0], title="professor", person=instructor) def query(self, q): return self.client.get( reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q=q)) def test_instructor_autocomplete(self): """ Verify instructor autocomplete returns the data. """ response = self.query('ins') self._assert_response(response, 2) # update first instructor's name self.instructors[0].given_name = 'dummy_name' self.instructors[0].save() response = self.query('dummy') self._assert_response(response, 1) def test_instructor_autocomplete_un_authorize_user(self): """ Verify instructor autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get( reverse('admin_metadata:person-autocomplete')) self._assert_response(response, 0) def test_instructor_autocomplete_spaces(self): """ Verify instructor autocomplete allows spaces. """ response = self.query('sec ins') self._assert_response(response, 1) def test_instructor_autocomplete_no_results(self): """ Verify instructor autocomplete correctly finds no matches if string doesn't match. """ response = self.query('second nope') self._assert_response(response, 0) def test_instructor_autocomplete_last_name_first_name(self): """ Verify instructor autocomplete allows last name first. """ response = self.query('instructor first') self._assert_response(response, 1) def test_instructor_position_in_label(self): """ Verify that instructor label contains position of instructor if it exists.""" position_title = 'professor' response = self.query('ins') self.assertContains( response, '<p>{position} at {organization}</p>'.format( position=position_title, organization=self.organizations[0].name)) def test_instructor_image_in_label(self): """ Verify that instructor label contains profile image url.""" response = self.query('ins') self.assertContains(response, self.instructors[0].get_profile_image_url) self.assertContains(response, self.instructors[1].get_profile_image_url) def _assert_response(self, response, expected_length): """ Assert autocomplete response. """ assert response.status_code == 200 data = json.loads(response.content.decode('utf-8')) assert len(data['results']) == expected_length def test_instructor_autocomplete_with_uuid(self): """ Verify instructor autocomplete returns the data with valid uuid. """ uuid = self.instructors[0].uuid response = self.query(uuid) self._assert_response(response, 1) def test_instructor_autocomplete_with_invalid_uuid(self): """ Verify instructor autocomplete returns empty list without giving error. """ uuid = 'invalid-uuid' response = self.query(uuid) self._assert_response(response, 0) def test_instructor_autocomplete_without_staff_user(self): """ Verify instructor autocomplete returns the data if user is not staff. """ non_staff_user = UserFactory() non_staff_user.groups.add(self.organization_extensions[0].group) self.client.logout() self.client.login(username=non_staff_user.username, password=USER_PASSWORD) response = self.query('ins') self._assert_response(response, 2) def test_instructor_autocomplete_without_login(self): """ Verify instructor autocomplete returns the zero record if user is not logged in. """ self.client.logout() person_autocomplete_url = reverse( 'admin_metadata:person-autocomplete') + '?q={q}'.format( q=self.instructors[0].uuid) response = self.client.get(person_autocomplete_url) self.assertRedirects(response, expected_url='{url}?next={next}'.format( url=reverse('login'), next=quote(person_autocomplete_url)), status_code=302, target_status_code=302) def test_instructor_autocomplete_from_django_admin(self): """ Verify instructor autocomplete return default data from django admin. """ admin_user = UserFactory(is_staff=True, is_superuser=True) self.client.logout() self.client.login(username=admin_user.username, password=USER_PASSWORD) response = self.client.get( reverse('admin_metadata:person-autocomplete') + '?q={q}'.format(q='ins'), HTTP_REFERER=reverse('admin:publisher_courserun_add')) assert response.status_code == 200 data = json.loads(response.content.decode('utf-8')) expected_results = [{ 'id': instructor.id, 'text': str(instructor) } for instructor in self.instructors] assert data.get('results') == expected_results def _make_user_non_staff(self): self.client.logout() self.user = UserFactory(is_staff=False) self.user.save() self.client.login(username=self.user.username, password=USER_PASSWORD)
class CourseViewSetTests(SerializationMixin, APITestCase): def setUp(self): super(CourseViewSetTests, self).setUp() self.user = UserFactory(is_staff=True) self.request.user = self.user self.client.login(username=self.user.username, password=USER_PASSWORD) self.course = CourseFactory(partner=self.partner, title='Fake Test', key='edX+Fake101') self.org = OrganizationFactory(key='edX', partner=self.partner) self.course.authoring_organizations.add(self.org) # pylint: disable=no-member def test_get(self): """ Verify the endpoint returns the details for a single course. """ url = reverse('api:v1:course-detail', kwargs={'key': self.course.key}) with self.assertNumQueries(27): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data, self.serialize_course(self.course)) def test_get_uuid(self): """ Verify the endpoint returns the details for a single course with UUID. """ url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}) with self.assertNumQueries(27): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data, self.serialize_course(self.course)) def test_get_exclude_deleted_programs(self): """ Verify the endpoint returns no deleted associated programs """ ProgramFactory(courses=[self.course], status=ProgramStatus.Deleted) url = reverse('api:v1:course-detail', kwargs={'key': self.course.key}) with self.assertNumQueries(18): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data.get('programs'), []) def test_get_include_deleted_programs(self): """ Verify the endpoint returns associated deleted programs with the 'include_deleted_programs' flag set to True """ ProgramFactory(courses=[self.course], status=ProgramStatus.Deleted) url = reverse('api:v1:course-detail', kwargs={'key': self.course.key}) url += '?include_deleted_programs=1' with self.assertNumQueries(34): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual( response.data, self.serialize_course( self.course, extra_context={'include_deleted_programs': True})) def test_get_include_hidden_course_runs(self): """ Verify the endpoint returns associated hidden course runs with the 'include_hidden_course_runs' flag set to True """ CourseRunFactory(status=CourseRunStatus.Published, end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10), enrollment_start=None, enrollment_end=None, hidden=True, course=self.course) url = reverse('api:v1:course-detail', kwargs={'key': self.course.key}) url += '?include_hidden_course_runs=1' response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data, self.serialize_course(self.course)) @ddt.data(1, 0) def test_marketable_course_runs_only(self, marketable_course_runs_only): """ Verify that a client requesting marketable_course_runs_only only receives course runs that are published, have seats, and can still be enrolled in. """ # Published course run with a seat, no enrollment start or end, and an end date in the future. enrollable_course_run = CourseRunFactory( status=CourseRunStatus.Published, end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10), enrollment_start=None, enrollment_end=None, course=self.course) SeatFactory(course_run=enrollable_course_run) # Unpublished course run with a seat. unpublished_course_run = CourseRunFactory( status=CourseRunStatus.Unpublished, course=self.course) SeatFactory(course_run=unpublished_course_run) # Published course run with no seats. no_seats_course_run = CourseRunFactory( status=CourseRunStatus.Published, course=self.course) # Published course run with a seat and an end date in the past. closed_course_run = CourseRunFactory( status=CourseRunStatus.Published, end=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=10), course=self.course) SeatFactory(course_run=closed_course_run) url = reverse('api:v1:course-detail', kwargs={'key': self.course.key}) url = '{}?marketable_course_runs_only={}'.format( url, marketable_course_runs_only) response = self.client.get(url) assert response.status_code == 200 if marketable_course_runs_only: # Emulate prefetching behavior. for course_run in (unpublished_course_run, no_seats_course_run, closed_course_run): course_run.delete() assert response.data == self.serialize_course(self.course) @ddt.data(1, 0) def test_marketable_enrollable_course_runs_with_archived( self, marketable_enrollable_course_runs_with_archived): """ Verify the endpoint filters course runs to those that are marketable and enrollable, including archived course runs (with an end date in the past). """ past = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=2) future = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2) course_run = CourseRunFactory(enrollment_start=None, enrollment_end=future, course=self.course) SeatFactory(course_run=course_run) filtered_course_runs = [ CourseRunFactory(enrollment_start=None, enrollment_end=None, course=self.course), CourseRunFactory(enrollment_start=past, enrollment_end=future, course=self.course), CourseRunFactory(enrollment_start=future, course=self.course), CourseRunFactory(enrollment_end=past, course=self.course), ] url = reverse('api:v1:course-detail', kwargs={'key': self.course.key}) url = '{}?marketable_enrollable_course_runs_with_archived={}'.format( url, marketable_enrollable_course_runs_with_archived) response = self.client.get(url) assert response.status_code == 200 if marketable_enrollable_course_runs_with_archived: # Emulate prefetching behavior. for course_run in filtered_course_runs: course_run.delete() assert response.data == self.serialize_course(self.course) @ddt.data(1, 0) def test_get_include_published_course_run(self, published_course_runs_only): """ Verify the endpoint returns hides unpublished programs if the 'published_course_runs_only' flag is set to True """ CourseRunFactory(status=CourseRunStatus.Published, course=self.course) unpublished_course_run = CourseRunFactory( status=CourseRunStatus.Unpublished, course=self.course) url = reverse('api:v1:course-detail', kwargs={'key': self.course.key}) url = '{}?published_course_runs_only={}'.format( url, published_course_runs_only) response = self.client.get(url) assert response.status_code == 200 if published_course_runs_only: # Emulate prefetching behavior. unpublished_course_run.delete() assert response.data == self.serialize_course(self.course) def test_list(self): """ Verify the endpoint returns a list of all courses. """ url = reverse('api:v1:course-list') with self.assertNumQueries(35): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertListEqual( response.data['results'], self.serialize_course(Course.objects.all().order_by( Lower('key')), many=True)) def test_list_query(self): """ Verify the endpoint returns a filtered list of courses """ title = 'Some random title' courses = CourseFactory.create_batch(3, title=title) courses = sorted(courses, key=lambda course: course.key.lower()) query = 'title:' + title url = '{root}?q={query}'.format(root=reverse('api:v1:course-list'), query=query) with self.assertNumQueries(51): response = self.client.get(url) self.assertListEqual(response.data['results'], self.serialize_course(courses, many=True)) def test_list_key_filter(self): """ Verify the endpoint returns a list of courses filtered by the specified keys. """ courses = CourseFactory.create_batch(3, partner=self.partner) courses = sorted(courses, key=lambda course: course.key.lower()) keys = ','.join([course.key for course in courses]) url = '{root}?keys={keys}'.format(root=reverse('api:v1:course-list'), keys=keys) with self.assertNumQueries(51): response = self.client.get(url) self.assertListEqual(response.data['results'], self.serialize_course(courses, many=True)) def test_list_uuid_filter(self): """ Verify the endpoint returns a list of courses filtered by the specified uuid. """ courses = CourseFactory.create_batch(3, partner=self.partner) courses = sorted(courses, key=lambda course: course.key.lower()) uuids = ','.join([str(course.uuid) for course in courses]) url = '{root}?uuids={uuids}'.format(root=reverse('api:v1:course-list'), uuids=uuids) with self.assertNumQueries(51): response = self.client.get(url) self.assertListEqual(response.data['results'], self.serialize_course(courses, many=True)) def test_list_exclude_utm(self): """ Verify the endpoint returns marketing URLs without UTM parameters. """ url = reverse('api:v1:course-list') + '?exclude_utm=1' response = self.client.get(url) context = {'exclude_utm': 1} self.assertEqual( response.data['results'], self.serialize_course([self.course], many=True, extra_context=context)) @ddt.data( ('get', False, False, True), ('options', False, False, True), ('post', False, False, False), ('post', False, True, True), ('post', True, False, True), ) @ddt.unpack def test_editor_access_list_endpoint(self, method, is_staff, in_org, allowed): """ Verify we check editor access correctly when hitting the courses endpoint. """ self.user.is_staff = is_staff self.user.save() if in_org: org_ext = OrganizationExtensionFactory(organization=self.org) self.user.groups.add(org_ext.group) response = getattr(self.client, method)(reverse('api:v1:course-list'), { 'org': self.org.key }, format='json') if not allowed: self.assertEqual(response.status_code, 403) else: self.assertNotEqual(response.status_code, 403) @ddt.data( ('get', False, False, False, True), ('options', False, False, False, True), ('put', False, False, False, False), # no access ('put', True, False, False, True), # is staff ('patch', False, True, False, False), # is in org ('patch', False, False, True, False), # is editor but not in org ('put', False, True, True, True), # editor and in org ) @ddt.unpack def test_editor_access_detail_endpoint(self, method, is_staff, in_org, is_editor, allowed): """ Verify we check editor access correctly when hitting the course object endpoint. """ self.user.is_staff = is_staff self.user.save() # Add another editor, because we have some logic that allows access anyway if a course has no valid editors. # That code path is checked in test_course_without_editors below. org_ext = OrganizationExtensionFactory(organization=self.org) user2 = UserFactory() user2.groups.add(org_ext.group) CourseEditorFactory(user=user2, course=self.course) if in_org: # Editors must be in the org to get editor access self.user.groups.add(org_ext.group) if is_editor: CourseEditorFactory(user=self.user, course=self.course) response = getattr(self.client, method)(reverse('api:v1:course-detail', kwargs={'key': self.course.uuid})) if not allowed: self.assertEqual(response.status_code, 404) else: # We'll probably fail because we didn't include the right data - but at least we'll have gotten in self.assertNotEqual(response.status_code, 404) def test_editable_list_gives_drafts(self): draft = CourseFactory(partner=self.partner, uuid=self.course.uuid, key=self.course.key, draft=True) draft_course_run = CourseRunFactory( status=CourseRunStatus.Published, end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10), course=draft, draft=True, ) self.course.draft_version = draft self.course.save() extra = CourseFactory(partner=self.partner, key=self.course.key + 'Z') # set key so it sorts later response = self.client.get( reverse('api:v1:course-list') + '?editable=1') self.assertEqual(response.status_code, 200) self.assertEqual(response.data['results'], self.serialize_course([draft, extra], many=True)) self.assertEqual(len(response.data['results'][0]['course_runs']), 1) self.assertEqual(response.data['results'][0]['course_runs'][0]['uuid'], str(draft_course_run.uuid)) def test_editable_get_gives_drafts(self): draft = CourseFactory(partner=self.partner, uuid=self.course.uuid, key=self.course.key, draft=True) self.course.draft_version = draft self.course.save() extra = CourseFactory(partner=self.partner) response = self.client.get( reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}) + '?editable=1') self.assertEqual(response.status_code, 200) self.assertEqual(response.data, self.serialize_course(draft, many=False)) response = self.client.get( reverse('api:v1:course-detail', kwargs={'key': extra.uuid}) + '?editable=1') self.assertEqual(response.status_code, 200) self.assertEqual(response.data, self.serialize_course(extra, many=False)) def test_course_without_editors(self): """ Verify we can modify a course with no editors if we're in its authoring org. """ url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}) self.user.is_staff = False self.user.save() self.course.draft = True self.course.save() # Try without being in the organization nor an editor self.assertEqual(self.client.patch(url).status_code, 404) # Add to authoring org, and we should be let in org_ext = OrganizationExtensionFactory(organization=self.org) self.user.groups.add(org_ext.group) self.assertNotEqual(self.client.patch(url).status_code, 404) # Now add a random other user as an editor to the course, so that we will no longer be granted access. editor = UserFactory() CourseEditorFactory(user=editor, course=self.course) editor.groups.add(org_ext.group) self.assertEqual(self.client.patch(url).status_code, 404) # But if the editor is no longer valid (even though they exist), we're back to having access. editor.groups.remove(org_ext.group) self.assertNotEqual(self.client.patch(url).status_code, 404) # And finally, for a sanity check, confirm we have access when we become an editor also CourseEditorFactory(user=self.user, course=self.course) self.assertNotEqual(self.client.patch(url).status_code, 404) def test_delete_not_allowed(self): """ Verify we don't allow deleting a course from the API. """ response = self.client.delete( reverse('api:v1:course-detail', kwargs={'key': self.course.uuid})) self.assertEqual(response.status_code, 405) def test_create_without_authentication(self): """ Verify authentication is required when creating a course. """ self.client.logout() Course.objects.all().delete() url = reverse('api:v1:course-list') response = self.client.post(url) assert response.status_code == 403 assert Course.objects.count() == 0 def create_course(self, data=None, update=True): url = reverse('api:v1:course-list') if update: course_data = { 'title': 'Course title', 'number': 'test101', 'org': self.org.key, 'mode': 'audit', } course_data.update(data or {}) else: course_data = data or {} return self.client.post(url, course_data, format='json') @oauth_login @responses.activate def test_create_with_authentication_verified_mode(self): course_data = { 'mode': 'verified', 'price': 100, } ecom_url = self.partner.ecommerce_api_url + 'products/' ecom_entitlement_data = { 'product_class': 'Course Entitlement', 'title': 'Course title', 'price': course_data['price'], 'certificate_type': course_data['mode'], 'uuid': '00000000-0000-0000-0000-000000000000', 'stockrecords': [{ 'partner_sku': 'ABC123' }], } responses.add(responses.POST, ecom_url, body=json.dumps(ecom_entitlement_data), content_type='application/json', status=201, match_querystring=True) response = self.create_course(course_data) course = Course.everything.last() self.assertDictEqual(response.data, self.serialize_course(course)) self.assertEqual(response.status_code, 201) expected_course_key = '{org}+{number}'.format(org=self.org.key, number='test101') self.assertEqual(course.key, expected_course_key) self.assertEqual(course.title, 'Course title') self.assertListEqual(list(course.authoring_organizations.all()), [self.org]) self.assertEqual(1, CourseEntitlement.everything.count()) # pylint: disable=no-member @oauth_login def test_create_with_authentication_audit_mode(self): """ When creating with audit mode, no entitlement should be created. """ response = self.create_course() course = Course.everything.last() self.assertDictEqual(response.data, self.serialize_course(course)) self.assertEqual(response.status_code, 201) expected_course_key = '{org}+{number}'.format(org=self.org.key, number='test101') self.assertEqual(course.key, expected_course_key) self.assertEqual(course.title, 'Course title') self.assertListEqual(list(course.authoring_organizations.all()), [self.org]) self.assertEqual(0, CourseEntitlement.objects.count()) @oauth_login def test_create_makes_draft(self): """ When creating a course, it should start as a draft. """ ecom_url = self.partner.ecommerce_api_url + 'products/' ecom_entitlement_data = { 'product_class': 'Course Entitlement', 'title': 'Course title', 'price': '0.0', 'certificate_type': 'verified', 'uuid': '00000000-0000-0000-0000-000000000000', 'stockrecords': [{ 'partner_sku': 'ABC123' }], } responses.add(responses.POST, ecom_url, body=json.dumps(ecom_entitlement_data), content_type='application/json', status=201, match_querystring=True) response = self.create_course({'mode': 'verified'}) self.assertEqual(response.status_code, 201) course = Course.everything.last() self.assertTrue(course.draft) self.assertTrue(course.entitlements.first().draft) @oauth_login def test_create_fails_if_official_version_exists(self): """ When creating a course, it should not create one if an official version already exists. """ response = self.create_course({'number': 'Fake101'}) self.assertEqual(response.status_code, 400) expected_error_message = 'Failed to set course data: A course with key {key} already exists.' self.assertEqual(response.data, expected_error_message.format(key=self.course.key)) def test_create_fails_with_missing_field(self): response = self.create_course( { 'title': 'Course title', 'org': self.org.key, 'mode': 'audit', }, update=False) self.assertEqual(response.status_code, 400) expected_error_message = 'Incorrect data sent. Missing value for: [number].' self.assertEqual(response.data, expected_error_message) def test_create_fails_with_nonexistent_org(self): response = self.create_course({'org': 'fake org'}) self.assertEqual(response.status_code, 400) expected_error_message = 'Incorrect data sent. Organization does not exist.' self.assertEqual(response.data, expected_error_message) def test_create_fails_with_nonexistent_mode(self): response = self.create_course({'mode': 'fake mode'}) self.assertEqual(response.status_code, 400) expected_error_message = 'Incorrect data sent. Entitlement Track does not exist.' self.assertEqual(response.data, expected_error_message) @ddt.data( ({ 'title': 'Course title', 'number': 'test101', 'org': 'fake org', 'mode': 'fake mode' }, 'Incorrect data sent. Organization does not exist. Entitlement Track does not exist.' ), ({ 'title': 'Course title', 'org': 'edX', 'mode': 'fake mode' }, 'Incorrect data sent. Missing value for: [number]. Entitlement Track does not exist.' ), ({ 'title': 'Course title', 'org': 'fake org', 'mode': 'audit' }, 'Incorrect data sent. Missing value for: [number]. Organization does not exist.' ), ({ 'number': 'test101', 'org': 'fake org', 'mode': 'fake mode' }, 'Incorrect data sent. Missing value for: [title]. Organization does not exist. ' 'Entitlement Track does not exist.'), ) @ddt.unpack def test_create_fails_with_multiple_errors(self, course_data, expected_error_message): response = self.create_course(course_data, update=False) self.assertEqual(response.status_code, 400) self.assertEqual(response.data, expected_error_message) def test_create_with_api_exception(self): with mock.patch( # We are using get_course_key because it is called prior to tryig to contact the # e-commerce service and still gives the effect of an api exception. 'course_discovery.apps.api.v1.views.courses.CourseViewSet.get_course_key', side_effect=IntegrityError): with LogCapture(course_logger.name) as log_capture: response = self.create_course() self.assertEqual(response.status_code, 400) log_capture.check(( course_logger.name, 'ERROR', 'An error occurred while setting Course data.', )) @oauth_login @responses.activate def test_create_with_ecom_api_exception(self): ecom_url = self.partner.ecommerce_api_url + 'products/' expected_error_message = 'Missing or bad value for: [title].' responses.add( responses.POST, ecom_url, body=expected_error_message, status=400, ) with LogCapture(course_logger.name) as log_capture: response = self.create_course({'mode': 'verified', 'price': 100}) self.assertEqual(response.status_code, 400) log_capture.check(( course_logger.name, 'ERROR', 'The following error occurred while setting the Course Entitlement data in E-commerce: ' '{ecom_error}'.format(ecom_error=expected_error_message))) def test_update_without_authentication(self): """ Verify authentication is required when updating a course. """ self.client.logout() Course.objects.all().delete() url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}) response = self.client.patch(url) assert response.status_code == 403 assert Course.objects.count() == 0 @ddt.data('put', 'patch') @oauth_login @responses.activate def test_update_success(self, method): entitlement = CourseEntitlementFactory(course=self.course) url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}) course_data = { 'title': 'Course title', 'partner': self.partner.id, 'key': self.course.key, 'entitlements': [ { 'mode': entitlement.mode.slug, 'price': 1000, 'sku': entitlement.sku, 'expires': entitlement.expires, }, ], # The API is expecting the image to be base64 encoded. We are simulating that here. 'image': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY' '42YAAAAASUVORK5CYII=', 'video': { 'src': 'https://link.to.video.for.testing/watch?t_s=5' }, } ecom_url = '{0}stockrecords/{1}/'.format( self.partner.ecommerce_api_url, entitlement.sku) responses.add( responses.PUT, ecom_url, status=200, ) response = getattr(self.client, method)(url, course_data, format='json') self.assertEqual(response.status_code, 200) course = Course.everything.get(uuid=self.course.uuid, draft=True) self.assertEqual(course.title, 'Course title') self.assertEqual(course.entitlements.first().price, 1000) self.assertDictEqual(response.data, self.serialize_course(course)) @oauth_login @responses.activate def test_update_operates_on_drafts(self): CourseEntitlementFactory(course=self.course) self.assertFalse( Course.everything.filter(uuid=self.course.uuid, draft=True).exists()) # sanity check url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}) response = self.client.patch(url, {'title': 'Title'}, format='json') self.assertEqual(response.status_code, 200) course = Course.everything.get(uuid=self.course.uuid, draft=True) self.assertTrue(course.entitlements.first().draft) self.assertEqual(course.title, 'Title') self.course.refresh_from_db() self.assertFalse(self.course.draft) self.assertFalse(self.course.entitlements.first().draft) self.assertEqual(self.course.title, 'Fake Test') @ddt.data( ( { 'entitlements': [{}] }, 'Entitlements must have a mode specified.', ), ({ 'entitlements': [{ 'mode': 'NOPE' }] }, 'Entitlement mode NOPE not found.'), ({ 'entitlements': [{ 'mode': 'mode2' }] }, 'Existing entitlement not found for mode mode2 in course Org/Course/Number.' ), ({ 'entitlements': [{ 'mode': 'mode1' }] }, 'Entitlement does not have a valid SKU assigned.'), ) @ddt.unpack def test_update_fails_with_multiple_errors(self, course_data, expected_error_message): course = CourseFactory(partner=self.partner, key='Org/Course/Number') url = reverse('api:v1:course-detail', kwargs={'key': course.uuid}) mode1 = SeatTypeFactory(name='Mode1') SeatTypeFactory(name='Mode2') CourseEntitlementFactory(course=course, mode=mode1, sku=None) response = self.client.patch(url, course_data, format='json') self.assertEqual(response.status_code, 400) self.assertEqual(response.data, expected_error_message) def test_update_with_api_exception(self): url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}) course_data = { 'title': 'Course title', 'entitlements': [ { 'price': 1000, }, ], } with mock.patch( 'course_discovery.apps.api.v1.views.courses.CourseViewSet.update_entitlement', side_effect=IntegrityError): with LogCapture(course_logger.name) as log_capture: response = self.client.patch(url, course_data, format='json') self.assertEqual(response.status_code, 400) log_capture.check(( course_logger.name, 'ERROR', 'An error occurred while setting Course data.', )) @oauth_login @responses.activate def test_update_with_ecom_api_exception(self): entitlement = CourseEntitlementFactory(course=self.course) url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}) course_data = { 'title': 'Course title', 'entitlements': [ { 'mode': entitlement.mode.slug, 'price': 1000, }, ], } ecom_url = '{0}stockrecords/{1}/'.format( self.partner.ecommerce_api_url, entitlement.sku) expected_error_message = 'Nope' responses.add( responses.PUT, ecom_url, body=expected_error_message, status=400, ) with LogCapture(course_logger.name) as log_capture: response = self.client.patch(url, course_data, format='json') self.assertEqual(response.status_code, 400) log_capture.check(( course_logger.name, 'ERROR', 'The following error occurred while setting the Course Entitlement data in E-commerce: ' '{ecom_error}'.format(ecom_error=expected_error_message))) @oauth_login @responses.activate def test_options(self): SubjectFactory(name='Subject1') CourseEntitlementFactory(course=self.course, mode=SeatType.objects.get(slug='verified')) url = reverse('api:v1:course-detail', kwargs={'key': self.course.uuid}) response = self.client.options(url) self.assertEqual(response.status_code, 200) data = response.data['actions']['PUT'] self.assertEqual(data['level_type']['choices'], [{ 'display_name': self.course.level_type.name, 'value': self.course.level_type.name }]) self.assertEqual( data['entitlements']['child']['children']['mode']['choices'], [{ 'display_name': 'Audit', 'value': 'audit' }, { 'display_name': 'Credit', 'value': 'credit' }, { 'display_name': 'Professional', 'value': 'professional' }, { 'display_name': 'Verified', 'value': 'verified' }]) self.assertEqual(data['subjects']['child']['choices'], [{ 'display_name': 'Subject1', 'value': 'subject1' }]) self.assertFalse( 'choices' in data['partner']) # we don't whitelist partner to show its choices
class RateLimitingExceededTest(SiteMixin, APITestCase): """ Testing rate limiting of API calls. """ def setUp(self): super().setUp() self.url = reverse('api_docs') self.user = UserFactory() self.client.login(username=self.user.username, password=USER_PASSWORD) def tearDown(self): super().tearDown() throttling_cache().clear() def _make_requests(self, count=None): """ Make multiple requests until the throttle's limit is exceeded. Returns Response: Response of the last request. """ count = count or 6 user_rates = {'user': '******'} with patch('rest_framework.views.APIView.throttle_classes', (OverridableUserRateThrottle,)): with patch.object(OverridableUserRateThrottle, 'THROTTLE_RATES', user_rates): for __ in range(count - 1): response = self.client.get(self.url) assert response.status_code == 200 response = self.client.get(self.url) return response def assert_rate_limit_successfully_exceeded(self): """ Asserts that the throttle's rate limit can be exceeded without encountering an error. """ response = self._make_requests() assert response.status_code == 200 def assert_rate_limited(self, count=None): """ Asserts that the throttle's rate limit was exceeded and we were denied. """ response = self._make_requests(count) assert response.status_code == 429 def test_rate_limiting(self): """ Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """ self.assert_rate_limited() def test_user_throttle_rate(self): """ Verify the UserThrottleRate can be used to override the default rate limit. """ UserThrottleRate.objects.create(user=self.user, rate='10/hour') self.assert_rate_limited(11) def test_superuser_throttling(self): """ Verify superusers are not throttled. """ self.user.is_superuser = True self.user.save() self.assert_rate_limit_successfully_exceeded() def test_staff_throttling(self): """ Verify staff users are not throttled. """ self.user.is_staff = True self.user.save() self.assert_rate_limit_successfully_exceeded() def test_publisher_user_throttling(self): """ Verify publisher users are not throttled. """ self.user.groups.add(GroupFactory()) self.assert_rate_limit_successfully_exceeded() def test_staff_with_user_throttle_rate(self): """ Verify the UserThrottleRate kicks in even for staff. """ self.user.is_staff = True self.user.save() UserThrottleRate.objects.create(user=self.user, rate='10/hour') self.assert_rate_limited(11)
class CommentViewSetTests(OAuth2Mixin, APITestCase): @factory.django.mute_signals(m2m_changed) def setUp(self): super().setUp() self.salesforce_config = SalesforceConfigurationFactory(partner=self.partner) self.user = UserFactory(is_staff=True) self.request.user = self.user self.request.site.partner = self.partner self.client.login(username=self.user.username, password=USER_PASSWORD) self.course = CourseFactoryNoSignals(partner=self.partner, title='Fake Test', key='edX+Fake101', draft=True) self.org = OrganizationFactoryNoSignals(key='edX', partner=self.partner) self.course.authoring_organizations.add(self.org) def tearDown(self): super().tearDown() # Zero out the instances that are created during testing SalesforceUtil.instances = {} def test_list_no_salesforce_case_id_set(self): user_orgs_path = 'course_discovery.apps.course_metadata.models.Organization.user_organizations' with mock.patch('course_discovery.apps.course_metadata.salesforce.Salesforce'): with mock.patch(user_orgs_path, return_value=[self.org]): url = '{}?course_uuid={}'.format(reverse('api:v1:comment-list'), self.course.uuid) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data, []) def test_list_salesforce_case_id_set(self): self.course.salesforce_id = 'TestSalesforceId' with factory.django.mute_signals(post_save): self.course.save() salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce' get_comments_path = 'course_discovery.apps.api.v1.views.comments.SalesforceUtil.get_comments_for_course' user_orgs_path = 'course_discovery.apps.course_metadata.models.Organization.user_organizations' return_value = [ { 'user': { 'first_name': 'TestFirst', 'last_name': 'TestLast', 'email': '*****@*****.**', 'username': '******', }, 'course_run_key': None, 'created': '2000-01-01T00:00:00.000+0000', 'comment': 'This is a test comment', } ] with mock.patch(salesforce_path): with mock.patch(user_orgs_path, return_value=[self.org]): with mock.patch(get_comments_path, return_value=return_value) as mock_get_comments: url = '{}?course_uuid={}'.format(reverse('api:v1:comment-list'), self.course.uuid) response = self.client.get(url) mock_get_comments.assert_called_with(self.course) self.assertEqual(response.status_code, 200) self.assertEqual(response.data, return_value) def test_list_400s_without_course_uuid(self): with mock.patch('course_discovery.apps.course_metadata.salesforce.Salesforce'): url = reverse('api:v1:comment-list') response = self.client.get(url) self.assertEqual(response.status_code, 400) def test_list_404s_without_finding_course(self): fake_uuid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' # Needs to resemble a uuid to pass validation with mock.patch('course_discovery.apps.course_metadata.salesforce.Salesforce'): url = '{}?course_uuid={}'.format(reverse('api:v1:comment-list'), fake_uuid) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_list_403s_without_permissions(self): salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce' user_orgs_path = 'course_discovery.apps.course_metadata.models.Organization.user_organizations' self.user.is_staff = False self.user.save() with mock.patch(salesforce_path): with mock.patch(user_orgs_path, return_value=[]): url = '{}?course_uuid={}'.format(reverse('api:v1:comment-list'), self.course.uuid) response = self.client.get(url) self.assertEqual(response.status_code, 403) def test_list_200s_as_staff(self): salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce' user_orgs_path = 'course_discovery.apps.course_metadata.models.Organization.user_organizations' with mock.patch(salesforce_path): with mock.patch(user_orgs_path, return_value=[]): url = '{}?course_uuid={}'.format(reverse('api:v1:comment-list'), self.course.uuid) response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_create(self): body = { 'course_uuid': self.course.uuid, 'comment': 'Test comment', 'course_run_key': 'test-key', } salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce' create_comment_path = ('course_discovery.apps.api.v1.views.comments.' 'SalesforceUtil.create_comment_for_course_case') with mock.patch(salesforce_path): with mock.patch(create_comment_path, return_value={ 'user': { 'username': self.user.username, 'email': self.user.email, 'first_name': self.user.first_name, 'last_name': self.user.last_name, }, 'comment': 'Comment body', 'created': datetime.datetime.now(datetime.timezone.utc).isoformat(), }) as mock_create_comment: url = reverse('api:v1:comment-list') response = self.client.post(url, body, format='json') mock_create_comment.assert_called_with( self.course, self.request.user, body.get('comment'), course_run_key=body.get('course_run_key'), ) self.assertEqual(response.status_code, 201) def test_create_400s_without_data(self): body = {} salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce' with mock.patch(salesforce_path): url = reverse('api:v1:comment-list') response = self.client.post(url, body, format='json') self.assertEqual(response.status_code, 400) def test_create_403s_without_permissions(self): body = { 'course_uuid': self.course.uuid, 'comment': 'Test comment', 'course_run_key': 'test-key', } salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce' is_editable_path = 'course_discovery.apps.api.v1.views.comments.CourseEditor.is_course_editable' with mock.patch(salesforce_path): with mock.patch(is_editable_path, return_value=False): url = reverse('api:v1:comment-list') response = self.client.post(url, body, format='json') self.assertEqual(response.status_code, 403) def test_create_404s_without_finding_course(self): body = { 'course_uuid': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', # Needs to resemble a uuid to pass validation 'comment': 'Test comment', 'course_run_key': 'test-key', } salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce' with mock.patch(salesforce_path): url = reverse('api:v1:comment-list') response = self.client.post(url, body, format='json') self.assertEqual(response.status_code, 404) def test_create_404s_without_a_config(self): body = { 'course_uuid': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', # Needs to resemble a uuid to pass validation 'comment': 'Test comment', 'course_run_key': 'test-key', } salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce' with mock.patch(salesforce_path): url = reverse('api:v1:comment-list') response = self.client.post(url, body, format='json') self.assertEqual(response.status_code, 404) def test_create_500s_without_a_successful_case_create(self): body = { 'course_uuid': self.course.uuid, 'comment': 'Test comment', 'course_run_key': 'test-key', } salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce' create_comment_path = ('course_discovery.apps.api.v1.views.comments.' 'SalesforceUtil.create_comment_for_course_case') with mock.patch(salesforce_path): with mock.patch(create_comment_path, side_effect=SalesforceMissingCaseException('Error')): url = reverse('api:v1:comment-list') response = self.client.post(url, body, format='json') self.assertEqual(response.status_code, 500) def test_list_404s_without_a_config(self): self.salesforce_config.delete() body = { 'course_uuid': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', # Needs to resemble a uuid to pass validation 'comment': 'Test comment', 'course_run_key': 'test-key', } salesforce_path = 'course_discovery.apps.course_metadata.salesforce.Salesforce' with mock.patch(salesforce_path): url = reverse('api:v1:comment-list') response = self.client.post(url, body, format='json') self.assertEqual(response.status_code, 404)
class AdminTests(TestCase): """ Tests Admin page.""" def setUp(self): super(AdminTests, self).setUp() self.user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=self.user.username, password=USER_PASSWORD) self.course_runs = factories.CourseRunFactory.create_batch(3) self.courses = [course_run.course for course_run in self.course_runs] self.excluded_course_run = factories.CourseRunFactory(course=self.courses[0]) self.program = factories.ProgramFactory( courses=self.courses, excluded_course_runs=[self.excluded_course_run] ) def _post_data(self, status=ProgramStatus.Unpublished, marketing_slug='/foo'): return { 'title': 'some test title', 'courses': [self.courses[0].id], 'type': self.program.type.id, 'status': status, 'marketing_slug': marketing_slug, 'partner': self.program.partner.id } def assert_form_valid(self, data, files): form = ProgramAdminForm(data=data, files=files) self.assertTrue(form.is_valid()) program = form.save() response = self.client.get(reverse('admin:course_metadata_program_change', args=(program.id,))) self.assertEqual(response.status_code, 200) def assert_form_invalid(self, data, files): form = ProgramAdminForm(data=data, files=files) self.assertFalse(form.is_valid()) self.assertEqual( form.errors['__all__'], ['Programs can only be activated if they have a marketing slug and a banner image.'] ) with self.assertRaises(ValueError): form.save() def test_program_detail_form(self): """ Verify in admin panel program detail form load successfully. """ response = self.client.get(reverse('admin:course_metadata_program_change', args=(self.program.id,))) self.assertEqual(response.status_code, 200) def test_custom_course_selection_page(self): """ Verify that course selection page loads successfully. """ response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,))) self.assertEqual(response.status_code, 200) self.assertContains(response, reverse('admin:course_metadata_program_change', args=(self.program.id,))) self.assertContains(response, reverse('admin:course_metadata_program_changelist')) def test_custom_course_selection_page_with_invalid_id(self): """ Verify that course selection page will return 404 for invalid program id. """ response = self.client.get(reverse('admin_metadata:update_course_runs', args=(10,))) self.assertEqual(response.status_code, 404) def test_custom_course_selection_page_with_non_staff(self): """ Verify that course selection page will return 404 for non authorized user. """ self.client.logout() self.user.is_superuser = False self.user.is_staff = False self.user.save() self.client.login(username=self.user.username, password=USER_PASSWORD) response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,))) self.assertEqual(response.status_code, 404) def test_page_loads_only_course_related_runs(self): """ Verify that course selection page loads only all course runs. Also marked checkboxes with excluded courses runs only. """ # add some new courses and course runs factories.CourseRunFactory.create_batch(2) response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,))) html = '<input checked="checked" id="id_excluded_course_runs_0" ' html += 'name="excluded_course_runs" type="checkbox" value="{id}" />'.format( id=self.excluded_course_run.id ) self.assertContains(response, html) for run in self.course_runs: self.assertContains(response, run.key) def test_page_with_post_new_course_run(self): """ Verify that course selection page with posting the data. """ self.assertEqual(1, self.program.excluded_course_runs.all().count()) self.assertEqual(3, sum(1 for _ in self.program.course_runs)) params = { 'excluded_course_runs': [self.excluded_course_run.id, self.course_runs[0].id], } post_url = reverse('admin_metadata:update_course_runs', args=(self.program.id,)) response = self.client.post(post_url, params) self.assertRedirects( response, expected_url=reverse('admin:course_metadata_program_change', args=(self.program.id,)), status_code=302, target_status_code=200 ) self.assertEqual(2, self.program.excluded_course_runs.all().count()) self.assertEqual(2, sum(1 for _ in self.program.course_runs)) def test_page_with_post_without_course_run(self): """ Verify that course selection page without posting any selected excluded check run. """ self.assertEqual(1, self.program.excluded_course_runs.all().count()) params = { 'excluded_course_runs': [], } post_url = reverse('admin_metadata:update_course_runs', args=(self.program.id,)) response = self.client.post(post_url, params) self.assertRedirects( response, expected_url=reverse('admin:course_metadata_program_change', args=(self.program.id,)), status_code=302, target_status_code=200 ) self.assertEqual(0, self.program.excluded_course_runs.all().count()) self.assertEqual(4, sum(1 for _ in self.program.course_runs)) response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,))) self.assertNotContains(response, '<input checked="checked")') @ddt.data( *itertools.product( ( (False, False, False), (True, False, False), (False, True, False), (True, True, True) ), ProgramStatus.labels ) ) @ddt.unpack def test_program_activation_restrictions(self, booleans, label): """Verify that program activation requires both a marketing slug and a banner image.""" has_marketing_slug, has_banner_image, can_be_activated = booleans status = getattr(ProgramStatus, label) marketing_slug = '/foo' if has_marketing_slug else '' banner_image = make_image_file('test_banner.jpg') if has_banner_image else '' data = self._post_data(status=status, marketing_slug=marketing_slug) files = {'banner_image': banner_image} if status == ProgramStatus.Active: if can_be_activated: # Transitioning to an active status should require a marketing slug and banner image. self.assert_form_valid(data, files) else: self.assert_form_invalid(data, files) else: # All other status transitions should be valid regardless of marketing slug and banner image. self.assert_form_valid(data, files) def test_new_program_without_courses(self): """ Verify that new program can be added without `courses`.""" data = self._post_data() data['courses'] = [] form = ProgramAdminForm(data) self.assertTrue(form.is_valid()) program = form.save() self.assertEqual(0, program.courses.all().count()) response = self.client.get(reverse('admin:course_metadata_program_change', args=(program.id,))) self.assertEqual(response.status_code, 200)
class RateLimitingExceededTest(SiteMixin, APITestCase): """ Testing rate limiting of API calls. """ def setUp(self): super(RateLimitingExceededTest, self).setUp() self.url = reverse('api_docs') self.user = UserFactory() self.client.login(username=self.user.username, password=USER_PASSWORD) self.default_rate_patcher = mock.patch.dict( settings.REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'], {'user': '******'} ) self.default_rate_patcher.start() def tearDown(self): super(RateLimitingExceededTest, self).tearDown() throttling_cache().clear() self.user.is_superuser = False self.user.is_staff = False self.user.save() self.default_rate_patcher.stop() def _make_requests(self): """ Make multiple requests until the throttle's limit is exceeded. Returns Response: Response of the last request. """ default_num_requests = OverridableUserRateThrottle().num_requests for __ in range(default_num_requests + 1): response = self.client.get(self.url) return response def test_rate_limiting(self): """ Verify the API responds with HTTP 429 if a normal user exceeds the rate limit. """ response = self._make_requests() assert response.status_code == 429 def test_user_throttle_rate(self): """ Verify the UserThrottleRate can be used to override the default rate limit. """ UserThrottleRate.objects.create(user=self.user, rate='20/hour') self.assert_rate_limit_successfully_exceeded() def assert_rate_limit_successfully_exceeded(self): """ Asserts that the throttle's rate limit can be exceeded without encountering an error. """ response = self._make_requests() assert response.status_code == 200 def test_superuser_throttling(self): """ Verify superusers are not throttled. """ self.user.is_superuser = True self.user.save() self.assert_rate_limit_successfully_exceeded() def test_staff_throttling(self): """ Verify staff users are not throttled. """ self.user.is_staff = True self.user.save() self.assert_rate_limit_successfully_exceeded() def test_publisher_user_throttling(self): """ Verify publisher users are not throttled. """ self.user.groups.add(GroupFactory()) self.assert_rate_limit_successfully_exceeded()
class AutocompleteTests(TestCase): """ Tests for autocomplete lookups.""" def setUp(self): super(AutocompleteTests, self).setUp() self.user = UserFactory(is_staff=True) self.client.login(username=self.user.username, password=USER_PASSWORD) self.courses = CourseFactory.create_batch(3, title='Some random course title') for course in self.courses: CourseRunFactory(course=course) self.organizations = OrganizationFactory.create_batch(3) first_instructor = PersonFactory(given_name="First Instructor") second_instructor = PersonFactory(given_name="Second Instructor") self.instructors = [first_instructor, second_instructor] @ddt.data('dum', 'ing') def test_course_autocomplete(self, search_key): """ Verify course autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:course-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 3) # update the first course title self.courses[0].key = 'edx/dummy/key' self.courses[0].title = 'this is some thing new' self.courses[0].save() response = self.client.get( reverse('admin_metadata:course-autocomplete') + '?q={title}'.format(title=search_key) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(self.courses[0])) def test_course_autocomplete_un_authorize_user(self): """ Verify course autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:course-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) @ddt.data('ing', 'dum') def test_course_run_autocomplete(self, search_key): """ Verify course run autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:course-run-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 3) # update the first course title course = self.courses[0] course.title = 'this is some thing new' course.save() course_run = self.courses[0].course_runs.first() course_run.key = 'edx/dummy/testrun' course_run.save() response = self.client.get( reverse('admin_metadata:course-run-autocomplete') + '?q={q}'.format(q=search_key) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(course_run)) def test_course_run_autocomplete_un_authorize_user(self): """ Verify course run autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:course-run-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) @ddt.data('irc', 'ing') def test_organization_autocomplete(self, search_key): """ Verify Organization autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:organisation-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 3) self.organizations[0].key = 'Mirco' self.organizations[0].name = 'testing name' self.organizations[0].save() response = self.client.get( reverse('admin_metadata:organisation-autocomplete') + '?q={key}'.format( key=search_key ) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(self.organizations[0])) self.assertEqual(len(data['results']), 1) def test_organization_autocomplete_un_authorize_user(self): """ Verify Organization autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:organisation-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) def _make_user_non_staff(self): self.client.logout() self.user = UserFactory(is_staff=False) self.user.save() self.client.login(username=self.user.username, password=USER_PASSWORD)
class AdminTests(SiteMixin, TestCase): """ Tests Admin page.""" def setUp(self): super(AdminTests, self).setUp() self.user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=self.user.username, password=USER_PASSWORD) self.course_runs = factories.CourseRunFactory.create_batch(3) self.courses = [course_run.course for course_run in self.course_runs] self.excluded_course_run = factories.CourseRunFactory(course=self.courses[0]) self.program = factories.ProgramFactory( courses=self.courses, excluded_course_runs=[self.excluded_course_run] ) def _post_data(self, status=ProgramStatus.Unpublished, marketing_slug='/foo'): return { 'title': 'some test title', 'courses': [self.courses[0].id], 'type': self.program.type.id, 'status': status, 'marketing_slug': marketing_slug, 'partner': self.program.partner.id } def assert_form_valid(self, data, files): form = ProgramAdminForm(data=data, files=files) self.assertTrue(form.is_valid()) program = form.save() response = self.client.get(reverse('admin:course_metadata_program_change', args=(program.id,))) self.assertEqual(response.status_code, 200) def assert_form_invalid(self, data, files): form = ProgramAdminForm(data=data, files=files) self.assertFalse(form.is_valid()) self.assertEqual( form.errors['__all__'], ['Programs can only be activated if they have a banner image.'] ) with self.assertRaises(ValueError): form.save() def test_program_detail_form(self): """ Verify in admin panel program detail form load successfully. """ response = self.client.get(reverse('admin:course_metadata_program_change', args=(self.program.id,))) self.assertEqual(response.status_code, 200) def test_custom_course_selection_page(self): """ Verify that course selection page loads successfully. """ response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,))) self.assertEqual(response.status_code, 200) self.assertContains(response, reverse('admin:course_metadata_program_change', args=(self.program.id,))) self.assertContains(response, reverse('admin:course_metadata_program_changelist')) def test_custom_course_selection_page_with_invalid_id(self): """ Verify that course selection page will return 404 for invalid program id. """ response = self.client.get(reverse('admin_metadata:update_course_runs', args=(10,))) self.assertEqual(response.status_code, 404) def test_custom_course_selection_page_with_non_staff(self): """ Verify that course selection page will return 404 for non authorized user. """ self.client.logout() self.user.is_superuser = False self.user.is_staff = False self.user.save() self.client.login(username=self.user.username, password=USER_PASSWORD) response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,))) self.assertEqual(response.status_code, 404) def test_page_loads_only_course_related_runs(self): """ Verify that course selection page loads only all course runs. Also marked checkboxes with excluded courses runs only. """ # add some new courses and course runs factories.CourseRunFactory.create_batch(2) response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,))) response_content = BeautifulSoup(response.content) attribute = response_content.find( "input", {"value": self.excluded_course_run.id, "type": "checkbox", "name": "excluded_course_runs"} ) assert attribute is not None for run in self.course_runs: self.assertContains(response, run.key) def test_page_with_post_new_course_run(self): """ Verify that course selection page with posting the data. """ self.assertEqual(1, self.program.excluded_course_runs.all().count()) self.assertEqual(3, sum(1 for _ in self.program.course_runs)) params = { 'excluded_course_runs': [self.excluded_course_run.id, self.course_runs[0].id], } post_url = reverse('admin_metadata:update_course_runs', args=(self.program.id,)) response = self.client.post(post_url, params) self.assertRedirects( response, expected_url=reverse('admin:course_metadata_program_change', args=(self.program.id,)), status_code=302, target_status_code=200 ) self.assertEqual(2, self.program.excluded_course_runs.all().count()) self.assertEqual(2, sum(1 for _ in self.program.course_runs)) def test_page_with_post_without_course_run(self): """ Verify that course selection page without posting any selected excluded check run. """ self.assertEqual(1, self.program.excluded_course_runs.all().count()) params = { 'excluded_course_runs': [], } post_url = reverse('admin_metadata:update_course_runs', args=(self.program.id,)) response = self.client.post(post_url, params) self.assertRedirects( response, expected_url=reverse('admin:course_metadata_program_change', args=(self.program.id,)), status_code=302, target_status_code=200 ) self.assertEqual(0, self.program.excluded_course_runs.all().count()) self.assertEqual(4, sum(1 for _ in self.program.course_runs)) response = self.client.get(reverse('admin_metadata:update_course_runs', args=(self.program.id,))) self.assertNotContains(response, '<input checked="checked")') @ddt.data( *itertools.product( ( (False, False), (True, True) ), sorted(ProgramStatus.labels) # We need a consistent ordering to distribute tests with pytest-xdist ) ) @ddt.unpack def test_program_activation_restrictions(self, booleans, label): """Verify that program activation requires both a marketing slug and a banner image.""" has_banner_image, can_be_activated = booleans status = getattr(ProgramStatus, label) banner_image = make_image_file('test_banner.jpg') if has_banner_image else '' data = self._post_data(status=status, marketing_slug='/foo') files = {'banner_image': banner_image} if status == ProgramStatus.Active: if can_be_activated: # Transitioning to an active status should require a marketing slug and banner image. self.assert_form_valid(data, files) else: self.assert_form_invalid(data, files) else: # All other status transitions should be valid regardless of marketing slug and banner image. self.assert_form_valid(data, files) def test_new_program_without_courses(self): """ Verify that new program can be added without `courses`.""" data = self._post_data() data['courses'] = [] form = ProgramAdminForm(data) self.assertTrue(form.is_valid()) program = form.save() self.assertEqual(0, program.courses.all().count()) response = self.client.get(reverse('admin:course_metadata_program_change', args=(program.id,))) self.assertEqual(response.status_code, 200)
class CourseEditorsViewSetTests(SerializationMixin, APITestCase): list_path = reverse('api:v1:course_editor-list') def setUp(self): super(CourseEditorsViewSetTests, self).setUp() self.staff_user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=self.staff_user.username, password=USER_PASSWORD) self.user = UserFactory() partner = Partner.objects.first() self.course = CourseFactory(draft=True, partner=partner) self.org_ext = OrganizationExtensionFactory() self.course.authoring_organizations.add(self.org_ext.organization) # pylint: disable=no-member def test_list(self): """Verify GET endpoint returns list of editors""" CourseEditorFactory() response = self.client.get(self.list_path) assert len(response.data['results']) == 1 # Test for non staff user self.client.login(username=self.user.username, password=USER_PASSWORD) response = self.client.get(self.list_path) self.assertFalse(response.data['results']) def test_course_query_param(self): """Verify GET endpoint with course query param returns editors relative to that course""" CourseEditorFactory(course=self.course) CourseEditorFactory() response = self.client.get(self.list_path) assert len(response.data['results']) == 2 response = self.client.get(self.list_path, {'course': self.course.uuid}) assert len(response.data['results']) == 1 assert response.data['results'][0]['course'] == self.course.uuid @ddt.data( (True, True), # Staff User on Draft Course (True, False), # Staff User on Official Course (False, True), # Non-staff User on Draft Course (False, False), # Non-staff User on Official Course ) @ddt.unpack def test_create_for_self_and_draft_course(self, is_staff, is_draft): """Verify can make self an editor. Test cases: as staff and non-staff, on official and draft course""" self.user.is_staff = is_staff self.user.save() partner = Partner.objects.first() course = CourseFactory(draft=is_draft, partner=partner) self.user.groups.add(self.org_ext.group) course.authoring_organizations.add(self.org_ext.organization) self.client.login(username=self.user.username, password=USER_PASSWORD) self.client.post(self.list_path, {'course': course.uuid}, format='json') course_editor = CourseEditor.objects.first() assert course_editor.course == course assert course_editor.user == self.user def test_create_for_self_as_non_staff_with_invalid_course(self): """Verify non staff user cannot make them self an editor of a course they dont belong to""" self.client.login(username=self.user.username, password=USER_PASSWORD) response = self.client.post(self.list_path, {'course': self.course.uuid}, format='json') assert response.status_code == 403 def test_create_for_other_user_as_staff(self): """Verify staff user can make another user an editor""" self.user.groups.add(self.org_ext.group) self.client.post(self.list_path, {'course': self.course.uuid, 'user_id': self.user.id}, format='json') course_editor = CourseEditor.objects.first() assert course_editor.course == self.course assert course_editor.user == self.user def test_create_for_other_user_as_non_staff(self): """Verify non staff can make another user an editor""" user2 = UserFactory() self.user.groups.add(self.org_ext.group) user2.groups.add(self.org_ext.group) self.client.login(username=self.user.username, password=USER_PASSWORD) self.client.post(self.list_path, {'course': self.course.uuid, 'user_id': user2.id}, format='json') course_editor = CourseEditor.objects.first() assert course_editor.course == self.course assert course_editor.user == user2 def test_create_for_invalid_other_user(self): """Verify a user can't be made an editor of a course if both are not under the same organization""" response = self.client.post( self.list_path, {'course': self.course.uuid, 'user_id': self.user.id}, format='json' ) assert response.status_code == 403
class AutoCompletePersonTests(mixins.APITestCase): """ Tests for person autocomplete lookups """ def setUp(self): super(AutoCompletePersonTests, self).setUp() self.user = UserFactory(is_staff=True) self.client.login(username=self.user.username, password=USER_PASSWORD) first_instructor = PersonFactory(given_name="First", family_name="Instructor") second_instructor = PersonFactory(given_name="Second", family_name="Instructor") self.instructors = [first_instructor, second_instructor] self.organizations = OrganizationFactory.create_batch(3) self.organization_extensions = [] for instructor in self.instructors: PositionFactory(organization=self.organizations[0], title="professor", person=instructor) for organization in self.organizations: org_ex = publisher_factories.OrganizationExtensionFactory( organization=organization) self.organization_extensions.append(org_ex) disco_course = CourseFactory( authoring_organizations=[self.organizations[0]]) disco_course2 = CourseFactory( authoring_organizations=[self.organizations[1]]) CourseRunFactory(course=disco_course, staff=[first_instructor]) CourseRunFactory(course=disco_course2, staff=[second_instructor]) self.user.groups.add(self.organization_extensions[0].group) def query(self, q): query_params = '?q={q}'.format(q=q) path = reverse('api:v1:person-search-typeahead') return self.client.get(path + query_params) def test_instructor_autocomplete(self): """ Verify instructor autocomplete returns the data. """ response = self.query('ins') self._assert_response(response, 2) # update first instructor's name self.instructors[0].given_name = 'dummy_name' self.instructors[0].save() response = self.query('dummy') self._assert_response(response, 1) def test_instructor_autocomplete_non_staff_user(self): """ Verify instructor autocomplete works for non-staff users. """ self._make_user_non_staff() response = self.query('dummy') self._assert_response(response, 0) def test_instructor_autocomplete_no_query_param(self): """ Verify instructor autocomplete returns bad response for request with no query. """ self._make_user_non_staff() response = self.client.get(reverse('api:v1:person-search-typeahead')) self._assert_error_response( response, ["The 'q' querystring parameter is required for searching."], 400) def test_instructor_autocomplete_spaces(self): """ Verify instructor autocomplete allows spaces. """ response = self.query('sec ins') self._assert_response(response, 1) def test_instructor_autocomplete_no_results(self): """ Verify instructor autocomplete correctly finds no matches if string doesn't match. """ response = self.query('second nope') self._assert_response(response, 0) def test_instructor_autocomplete_last_name_first_name(self): """ Verify instructor autocomplete allows last name first. """ response = self.query('instructor first') self._assert_response(response, 1) def test_instructor_position_in_label(self): """ Verify that instructor label contains position of instructor if it exists.""" position_title = 'professor' response = self.query('ins') self.assertContains(response, position_title) def test_instructor_image_in_label(self): """ Verify that instructor label contains profile image url.""" response = self.query('ins') self.assertContains(response, self.instructors[0].get_profile_image_url) self.assertContains(response, self.instructors[1].get_profile_image_url) def _assert_response(self, response, expected_length): """ Assert autocomplete response. """ assert response.status_code == 200 data = json.loads(response.content.decode('utf-8')) assert len(data) == expected_length def _assert_error_response(self, response, expected_response, expected_response_code=200): """ Assert autocomplete response. """ assert response.status_code == expected_response_code data = json.loads(response.content.decode('utf-8')) assert data == expected_response def test_instructor_autocomplete_with_uuid(self): """ Verify instructor autocomplete returns the data with valid uuid. """ uuid = self.instructors[0].uuid response = self.query(uuid) self._assert_response(response, 1) def test_instructor_autocomplete_with_invalid_uuid(self): """ Verify instructor autocomplete returns empty list without giving error. """ uuid = 'invalid-uuid' response = self.query(uuid) self._assert_response(response, 0) def test_instructor_autocomplete_without_staff_user(self): """ Verify instructor autocomplete returns the data if user is not staff. """ non_staff_user = UserFactory() non_staff_user.groups.add(self.organization_extensions[0].group) self.client.logout() self.client.login(username=non_staff_user.username, password=USER_PASSWORD) response = self.query('ins') self._assert_response(response, 2) def test_instructor_autocomplete_without_login(self): """ Verify instructor autocomplete returns a forbidden code if user is not logged in. """ self.client.logout() person_autocomplete_url = reverse( 'api:v1:person-search-typeahead') + '?q={q}'.format( q=self.instructors[0].uuid) response = self.client.get(person_autocomplete_url) self._assert_error_response( response, {'detail': 'Authentication credentials were not provided.'}, 401) def test_autocomplete_limit_by_org(self): org = self.organizations[0] person_autocomplete_url = reverse( 'api:v1:person-search-typeahead') + '?q=ins' single_autocomplete_url = person_autocomplete_url + '&org={key}'.format( key=org.key) response = self.client.get(single_autocomplete_url) self._assert_response(response, 1) org2 = self.organizations[1] multiple_autocomplete_url = single_autocomplete_url + '&org={key}'.format( key=org2.key) response = self.client.get(multiple_autocomplete_url) self._assert_response(response, 2) def _make_user_non_staff(self): self.client.logout() self.user = UserFactory(is_staff=False) self.user.save() self.client.login(username=self.user.username, password=USER_PASSWORD)
class AutocompleteTests(TestCase): """ Tests for autocomplete lookups.""" def setUp(self): super(AutocompleteTests, self).setUp() self.user = UserFactory(is_staff=True) self.client.login(username=self.user.username, password=USER_PASSWORD) self.courses = factories.CourseFactory.create_batch(3, title='Some random course title') for course in self.courses: factories.CourseRunFactory(course=course) self.organizations = factories.OrganizationFactory.create_batch(3) @ddt.data('dum', 'ing') def test_course_autocomplete(self, search_key): """ Verify course autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:course-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 3) # update the first course title self.courses[0].key = 'edx/dummy/key' self.courses[0].title = 'this is some thing new' self.courses[0].save() response = self.client.get( reverse('admin_metadata:course-autocomplete') + '?q={title}'.format(title=search_key) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(self.courses[0])) def test_course_autocomplete_un_authorize_user(self): """ Verify course autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:course-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) @ddt.data('ing', 'dum') def test_course_run_autocomplete(self, search_key): """ Verify course run autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:course-run-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 3) # update the first course title course = self.courses[0] course.title = 'this is some thing new' course.save() course_run = self.courses[0].course_runs.first() course_run.key = 'edx/dummy/testrun' course_run.save() response = self.client.get( reverse('admin_metadata:course-run-autocomplete') + '?q={q}'.format(q=search_key) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(course_run)) def test_course_run_autocomplete_un_authorize_user(self): """ Verify course run autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:course-run-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) @ddt.data('irc', 'ing') def test_organization_autocomplete(self, search_key): """ Verify Organization autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:organisation-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 3) self.organizations[0].key = 'Mirco' self.organizations[0].name = 'testing name' self.organizations[0].save() response = self.client.get( reverse('admin_metadata:organisation-autocomplete') + '?q={key}'.format( key=search_key ) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(self.organizations[0])) self.assertEqual(len(data['results']), 1) def test_organization_autocomplete_un_authorize_user(self): """ Verify Organization autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:organisation-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) @ddt.data('dummyurl', 'testing') def test_video_autocomplete(self, search_key): """ Verify video autocomplete returns the data. """ response = self.client.get(reverse('admin_metadata:video-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(response.status_code, 200) self.assertEqual(len(data['results']), 6) self.courses[0].video.src = 'http://www.youtube.com/dummyurl' self.courses[0].video.description = 'testing description' self.courses[0].video.save() response = self.client.get( reverse('admin_metadata:video-autocomplete') + '?q={key}'.format( key=search_key ) ) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'][0]['text'], str(self.courses[0].video)) self.assertEqual(len(data['results']), 1) def test_video_autocomplete_un_authorize_user(self): """ Verify video autocomplete returns empty list for un-authorized users. """ self._make_user_non_staff() response = self.client.get(reverse('admin_metadata:video-autocomplete')) data = json.loads(response.content.decode('utf-8')) self.assertEqual(data['results'], []) def _make_user_non_staff(self): self.client.logout() self.user = UserFactory(is_staff=False) self.user.save() self.client.login(username=self.user.username, password=USER_PASSWORD)