def setUp(self): super().setUp() self.user = UserFactory(is_staff=True, is_superuser=True) self.request.user = self.user self.client.force_authenticate(self.user) self.catalog = CatalogFactory(query='title:abc*') enrollment_end = datetime.datetime.now( pytz.UTC) + datetime.timedelta(days=30) course_end = datetime.datetime.now( pytz.UTC) + datetime.timedelta(days=60) self.course_run = CourseRunFactory(enrollment_end=enrollment_end, end=course_end, course__title='ABC Test Course') self.course = self.course_run.course self.refresh_index()
def test_data(self): user = UserFactory() CatalogFactory(query='*:*', viewers=[user]) course_run = CourseRunFactory() seat = SeatFactory(course_run=course_run) serializer = AffiliateWindowSerializer(seat) # Verify none of the course run attributes are empty; otherwise, Affiliate Window will report errors. # pylint: disable=no-member self.assertTrue( all((course_run.title, course_run.short_description, course_run.marketing_url))) expected = { 'pid': '{}-{}'.format(course_run.key, seat.type), 'name': course_run.title, 'desc': course_run.short_description, 'purl': course_run.marketing_url, 'price': { 'actualp': seat.price }, 'currency': seat.currency.code, 'imgurl': course_run.card_image_url, 'category': 'Other Experiences' } self.assertDictEqual(serializer.data, expected)
def test_courses_with_different_catalog_queries_but_the_same_meaning( self, query): catalog = CatalogFactory(query=query) course_run_1 = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), course__title='Science at the Polls: Biology for Voters, Part 1', status=CourseRunStatus.Published, type__is_marketable=True, ) course_run_2 = CourseRunFactory( start=datetime.datetime(2015, 10, 13, tzinfo=pytz.UTC), course__title="DNA: Biology's Genetic Code", status=CourseRunStatus.Published, type__is_marketable=True, ) course_run_3 = CourseRunFactory( status=CourseRunStatus.Published, start=datetime.datetime(2015, 1, 1, tzinfo=pytz.UTC), course__title="AP Biology", type__is_marketable=True, ) SeatFactory.create(course_run=course_run_1) SeatFactory.create(course_run=course_run_2) SeatFactory.create(course_run=course_run_3) url = reverse('api:v1:catalog-courses', kwargs={'id': catalog.id}) response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == self.serialize_catalog_course( [course_run_1.course, course_run_2.course, course_run_3.course], many=True)
def setUp(self): super().setUp() self.user = UserFactory() self.client.force_authenticate(self.user) self.catalog = CatalogFactory(query='*:*', program_query='*:*', viewers=[self.user]) self.enrollment_end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30) self.course_end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=60) self.course_run = CourseRunFactory(enrollment_end=self.enrollment_end, end=self.course_end) self.course = self.course_run.course # Generate test programs self.test_image = make_image_file('test_banner.jpg') self.masters_program_type = ProgramType.objects.get(slug=ProgramType.MASTERS) self.microbachelors_program_type = ProgramType.objects.get(slug=ProgramType.MICROBACHELORS) self.ms_program = ProgramFactory( type=self.masters_program_type, courses=[self.course], banner_image=self.test_image, ) self.program = ProgramFactory( type=self.microbachelors_program_type, courses=[self.course], banner_image=self.test_image, ) self.affiliate_url = reverse('api:v1:partners:programs_affiliate_window-detail', kwargs={'pk': self.catalog.id})
def test_permissions(self): """ Verify only users with the appropriate permissions can access the endpoint. """ catalog = CatalogFactory() superuser = UserFactory(is_superuser=True) url = reverse('api:v1:partners:affiliate_window-detail', kwargs={'pk': catalog.id}) # Superusers can view all catalogs self.client.force_authenticate(superuser) response = self.client.get(url) self.assertEqual(response.status_code, 200) # Regular users can only view catalogs belonging to them self.client.force_authenticate(self.user) response = self.client.get(url) self.assertEqual(response.status_code, 403) catalog.viewers = [self.user] response = self.client.get(url) self.assertEqual(response.status_code, 200)
def test_data(self): catalog = CatalogFactory() path = reverse('api:v1:catalog-detail', kwargs={'id': catalog.id}) request = RequestFactory().get(path) serializer = CatalogSerializer(catalog, context={'request': request}) expected = { 'id': catalog.id, 'name': catalog.name, 'query': catalog.query, 'url': request.build_absolute_uri(), } self.assertDictEqual(serializer.data, expected)
def test_permissions(self): """ Verify only users with the appropriate permissions can access the endpoint. """ catalog = CatalogFactory() superuser = UserFactory(is_superuser=True) url = reverse('api:v1:partners:affiliate_window-detail', kwargs={'pk': catalog.id}) # Superusers can view all catalogs self.client.force_authenticate(superuser) with self.assertNumQueries(5): response = self.client.get(url) self.assertEqual(response.status_code, 200) # Regular users can only view catalogs belonging to them self.client.force_authenticate(self.user) response = self.client.get(url) self.assertEqual(response.status_code, 403) catalog.viewers = [self.user] with self.assertNumQueries(8): response = self.client.get(url) self.assertEqual(response.status_code, 200)
def setUp(self): super(AffiliateWindowViewSetTests, self).setUp() self.user = UserFactory() self.client.force_authenticate(self.user) self.catalog = CatalogFactory(query='*:*', viewers=[self.user]) self.enrollment_end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30) self.course_end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=60) self.course_run = CourseRunFactory(enrollment_end=self.enrollment_end, end=self.course_end) self.seat_verified = SeatFactory(course_run=self.course_run, type=Seat.VERIFIED) self.course = self.course_run.course self.affiliate_url = reverse('api:v1:partners:affiliate_window-detail', kwargs={'pk': self.catalog.id}) self.refresh_index()
def test_username_filter_as_staff_user(self): """ Verify a list of Catalogs accessible by the given user is returned when filtering by username as a staff user. """ user = UserFactory(is_staff=False, is_superuser=False) catalog = CatalogFactory() path = '{root}?username={username}'.format(root=self.catalog_list_url, username=user.username) response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertListEqual(response.data['results'], []) self.grant_catalog_permission_to_user(user, 'view', catalog) response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertListEqual(response.data['results'], self.serialize_catalog([catalog], many=True))
def test_data(self): user = UserFactory() catalog = CatalogFactory( query='*:*', viewers=[user]) # We intentionally use a query for all Courses. courses = CourseFactory.create_batch(10) serializer = CatalogSerializer(catalog) expected = { 'id': catalog.id, 'name': catalog.name, 'query': catalog.query, 'courses_count': len(courses), 'viewers': [user.username] } self.assertDictEqual(serializer.data, expected)
def setUp(self): super(CatalogViewSetTests, self).setUp() self.user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=self.user.username, password=USER_PASSWORD) query = { 'query': { 'bool': { 'must': [{ 'wildcard': { 'course.name': 'abc*' } }] } } } self.catalog = CatalogFactory(query=json.dumps(query)) self.course = CourseFactory(id='a/b/c', name='ABC Test Course') self.refresh_index()
def test_catalog_if_query_is_incorrect(self): catalog = CatalogFactory(query='title:') CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), course__title='Science at the Polls: Biology for Voters, Part 1', status=CourseRunStatus.Published, type__is_marketable=True, ) CourseRunFactory( start=datetime.datetime(2015, 10, 13, tzinfo=pytz.UTC), course__title="DNA: Biology's Genetic Code", status=CourseRunStatus.Published, type__is_marketable=True, ) url = reverse('api:v1:catalog-courses', kwargs={'id': catalog.id}) response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == []
def test_courses_with_time_range_query(self): catalog = CatalogFactory(query='start:[2015-01-01 TO 2015-12-01]') course_run_1 = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, ) course_run_2 = CourseRunFactory( start=datetime.datetime(2015, 10, 13, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, ) SeatFactory.create(course_run=course_run_1) SeatFactory.create(course_run=course_run_2) call_command('search_index', '--rebuild', '-f') url = reverse('api:v1:catalog-courses', kwargs={'id': catalog.id}) response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == self.serialize_catalog_course( [course_run_1.course, course_run_2.course], many=True)
class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixin, APITestCase): """ Tests for the catalog resource. """ catalog_list_url = reverse('api:v1:catalog-list') def setUp(self): super(CatalogViewSetTests, self).setUp() self.user = UserFactory(is_staff=True, is_superuser=True) self.request.user = self.user self.client.force_authenticate(self.user) self.catalog = CatalogFactory(query='title:abc*') enrollment_end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30) course_end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=60) self.course_run = CourseRunFactory( enrollment_end=enrollment_end, end=course_end, course__title='ABC Test Course' ) self.course = self.course_run.course self.refresh_index() cache.clear() def assert_catalog_created(self, **headers): name = 'The Kitchen Sink' query = '*.*' viewer = UserFactory() data = { 'name': name, 'query': query, 'viewers': [viewer.username] } response = self.client.post(self.catalog_list_url, data, format='json', **headers) self.assertEqual(response.status_code, 201) catalog = Catalog.objects.latest() self.assertDictEqual(response.data, self.serialize_catalog(catalog)) self.assertEqual(catalog.name, name) self.assertEqual(catalog.query, query) self.assertListEqual(list(catalog.viewers), [viewer]) def assert_catalog_contains_query_string(self, query_string_kwargs, course_key): """ Helper method to validate the provided course key or course run key in the catalog contains endpoint. """ query_string = urllib.parse.urlencode(query_string_kwargs) url = '{base_url}?{query_string}'.format( base_url=reverse('api:v1:catalog-contains', kwargs={'id': self.catalog.id}), query_string=query_string ) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data, {'courses': {course_key: True}}) def grant_catalog_permission_to_user(self, user, action, catalog=None): """ Grant the user access to view `self.catalog`. """ catalog = catalog or self.catalog perm = '{action}_catalog'.format(action=action) user.add_obj_perm(perm, catalog) self.assertTrue(user.has_perm('catalogs.' + perm, catalog)) def test_create_without_authentication(self): """ Verify authentication is required when creating, updating, or deleting a catalog. """ self.client.logout() Catalog.objects.all().delete() response = self.client.post(self.catalog_list_url, {}, format='json') self.assertEqual(response.status_code, 403) self.assertEqual(Catalog.objects.count(), 0) @ddt.data('put', 'patch', 'delete') def test_modify_without_authentication(self, http_method): """ Verify authentication is required to modify a catalog. """ self.client.logout() url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) response = getattr(self.client, http_method)(url, {}, format='json') self.assertEqual(response.status_code, 403) def test_create_with_session_authentication(self): """ Verify the endpoint creates a new catalog when the client is authenticated via session authentication. """ self.assert_catalog_created() def test_create_with_jwt_authentication(self): """ Verify the endpoint creates a new catalog when the client is authenticated via JWT authentication. """ self.client.logout() self.assert_catalog_created(HTTP_AUTHORIZATION=generate_jwt_header_for_user(self.user)) @responses.activate def test_create_with_oauth2_authentication(self): self.client.logout() self.mock_user_info_response(self.user) self.assert_catalog_created(HTTP_AUTHORIZATION=self.generate_oauth2_token_header(self.user)) def test_create_with_new_user(self): """ Verify that new users are created if the list of viewers includes the usernames of non-existent users. """ new_viewer_username = '******' existing_viewer = UserFactory() viewers = [new_viewer_username, existing_viewer.username] data = { 'name': 'Test Catalog', 'query': '*:*', 'viewers': ','.join(viewers) } # NOTE: We explicitly avoid using the JSON data type so that we properly test string parsing. response = self.client.post(self.catalog_list_url, data) self.assertEqual(response.status_code, 201) catalog = Catalog.objects.latest() latest_user = User.objects.latest() assert latest_user.username == new_viewer_username assert set(catalog.viewers) == {existing_viewer, latest_user} def test_create_with_new_user_error(self): """ Verify no users are created if an error occurs while processing a create request. """ # The missing name and query fields should trigger an error data = { 'viewers': ['new-guy'] } original_user_count = User.objects.count() response = self.client.post(self.catalog_list_url, data) self.assertEqual(response.status_code, 400) self.assertEqual(User.objects.count(), original_user_count) @ddt.data( *STATES() ) def test_courses(self, state): """ Verify the endpoint returns the list of available courses contained in the catalog, and that courses appearing in the response always have at least one serialized run. """ url = reverse('api:v1:catalog-courses', kwargs={'id': self.catalog.id}) Course.objects.all().delete() course_run = CourseRunFactory(course__title='ABC Test Course') for function in state: function(course_run) course_run.save() if state in AVAILABLE_STATES: course = course_run.course # This run has no seats, but we still expect its parent course # to be included. filtered_course_run = CourseRunFactory(course=course) with self.assertNumQueries(FuzzyInt(23, 2)): response = self.client.get(url) assert response.status_code == 200 # Emulate prefetching behavior. filtered_course_run.delete() assert response.data['results'] == self.serialize_catalog_course([course], many=True) # Any course appearing in the response must have at least one serialized run. assert len(response.data['results'][0]['course_runs']) > 0 else: response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == [] def test_courses_with_include_archived(self): """ Verify the endpoint returns the list of available and archived courses if include archived is True in catalog. """ url = reverse('api:v1:catalog-courses', kwargs={'id': self.catalog.id}) Course.objects.all().delete() now = datetime.datetime.now(pytz.UTC) future = now + datetime.timedelta(days=30) past = now - datetime.timedelta(days=30) course_run = CourseRunFactory.create( course__title='ABC Test Course With Archived', end=future, enrollment_end=future ) SeatFactory.create(course_run=course_run) # Create an archived course run CourseRunFactory.create(course=course_run.course, end=past) response = self.client.get(url) assert response.status_code == 200 # The course appearing in response should have on 1 course run assert len(response.data['results'][0]['course_runs']) == 1 # Mark include archived True in catalog self.catalog.include_archived = True self.catalog.save() response = self.client.get(url) assert response.status_code == 200 # The course appearing in response should include archived course run assert len(response.data['results'][0]['course_runs']) == 2 def test_contains_for_course_key(self): """ Verify the endpoint returns a filtered list of courses contained in the catalog for course keys with the format "org+course". """ course_key = self.course.key query_string_kwargs = {'course_id': course_key} self.assert_catalog_contains_query_string(query_string_kwargs, course_key) def test_contains_for_course_run_key(self): """ Verify the endpoint returns a filtered list of courses contained in the catalog for course run keys with the format "org/course/run" or "course-v1:org+course+key". """ course_run_key = self.course_run.key query_string_kwargs = {'course_run_id': course_run_key} self.assert_catalog_contains_query_string(query_string_kwargs, course_run_key) def test_csv(self): SeatFactory(type='audit', course_run=self.course_run) SeatFactory(type='verified', course_run=self.course_run) SeatFactory(type='credit', course_run=self.course_run, credit_provider='ASU', credit_hours=9) SeatFactory(type='credit', course_run=self.course_run, credit_provider='Hogwarts', credit_hours=4) url = reverse('api:v1:catalog-csv', kwargs={'id': self.catalog.id}) with self.assertNumQueries(20): response = self.client.get(url) course_run = self.serialize_catalog_flat_course_run(self.course_run) expected = [ course_run['key'], course_run['title'], course_run['pacing_type'], course_run['start'], course_run['end'], course_run['enrollment_start'], course_run['enrollment_end'], course_run['announcement'], course_run['full_description'], course_run['short_description'], course_run['marketing_url'], course_run['image']['src'], '', '', '', course_run['video']['src'], course_run['video']['description'], course_run['video']['image']['src'], course_run['video']['image']['description'], str(course_run['video']['image']['height']), str(course_run['video']['image']['width']), course_run['content_language'], str(course_run['level_type']), str(course_run['max_effort']), str(course_run['min_effort']), course_run['subjects'], course_run['expected_learning_items'], course_run['prerequisites'], course_run['owners'], course_run['sponsors'], course_run['seats']['audit']['type'], course_run['seats']['honor']['type'], course_run['seats']['professional']['type'], str(course_run['seats']['professional']['price']), course_run['seats']['professional']['currency'], course_run['seats']['professional']['upgrade_deadline'], course_run['seats']['verified']['type'], str(course_run['seats']['verified']['price']), course_run['seats']['verified']['currency'], course_run['seats']['verified']['upgrade_deadline'], '{}'.format(course_run['seats']['credit']['type']), '{}'.format(str(course_run['seats']['credit']['price'])), '{}'.format(course_run['seats']['credit']['currency']), '{}'.format(course_run['seats']['credit']['upgrade_deadline']), '{}'.format(course_run['seats']['credit']['credit_provider']), '{}'.format(course_run['seats']['credit']['credit_hours']), course_run['modified'], course_run['course_key'], ] # collect streamed content received_content = b'' for item in response.streaming_content: received_content += item # convert received content to csv for comparison f = StringIO(received_content.decode('utf-8')) reader = csv.reader(f) content = list(reader) self.assertEqual(response.status_code, 200) self.assertEqual(set(expected), set(content[1])) def test_get(self): """ Verify the endpoint returns the details for a single catalog. """ url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data, self.serialize_catalog(self.catalog)) def test_list(self): """ Verify the endpoint returns a list of all catalogs. """ response = self.client.get(self.catalog_list_url) self.assertEqual(response.status_code, 200) self.assertListEqual(response.data['results'], self.serialize_catalog(Catalog.objects.all(), many=True)) def test_destroy(self): """ Verify the endpoint deletes a catalog. """ url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) response = self.client.delete(url) self.assertEqual(response.status_code, 204) self.assertFalse(Catalog.objects.filter(id=self.catalog.id).exists()) def test_update(self): """ Verify the endpoint updates a catalog. """ url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) name = 'Updated Catalog' query = 'so-not-real' data = { 'name': name, 'query': query } response = self.client.put(url, data, format='json') self.assertEqual(response.status_code, 200) catalog = Catalog.objects.get(id=self.catalog.id) self.assertEqual(catalog.name, name) self.assertEqual(catalog.query, query) def test_partial_update(self): """ Verify the endpoint supports partially updating a catalog's fields. """ url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) name = 'Updated Catalog' query = self.catalog.query data = { 'name': name } response = self.client.patch(url, data, format='json') self.assertEqual(response.status_code, 200) catalog = Catalog.objects.get(id=self.catalog.id) self.assertEqual(catalog.name, name) self.assertEqual(catalog.query, query) def test_retrieve_permissions(self): """ Verify only users with the correct permissions can create, read, or modify a Catalog. """ # Use an unprivileged user user = UserFactory(is_staff=False, is_superuser=False) self.client.force_authenticate(user) url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) # A user with no permissions should NOT be able to view a Catalog. self.assertFalse(user.has_perm('catalogs.view_catalog', self.catalog)) response = self.client.get(url) self.assertEqual(response.status_code, 403) # The permitted user should be able to view the Catalog. self.grant_catalog_permission_to_user(user, 'view') response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_list_permissions(self): """ Verify only catalogs accessible to the user are returned in the list view. """ user = UserFactory(is_staff=False, is_superuser=False) self.client.force_authenticate(user) # An user with no permissions should not see any catalogs response = self.client.get(self.catalog_list_url) self.assertEqual(response.status_code, 200) self.assertListEqual(response.data['results'], []) # The client should be able to see permissions for which it has access self.grant_catalog_permission_to_user(user, 'view') response = self.client.get(self.catalog_list_url) self.assertEqual(response.status_code, 200) self.assertListEqual(response.data['results'], self.serialize_catalog([self.catalog], many=True)) def test_write_permissions(self): """ Verify only authorized users can update or delete Catalogs. """ url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) user = UserFactory(is_staff=False, is_superuser=False) self.client.force_authenticate(user) # Unprivileged users cannot modify Catalogs response = self.client.put(url) self.assertEqual(response.status_code, 403) response = self.client.delete(url) self.assertEqual(response.status_code, 403) # With the right permissions, the user can perform the specified actions self.grant_catalog_permission_to_user(user, 'change') response = self.client.patch(url, {'query': '*:*'}) self.assertEqual(response.status_code, 200) self.grant_catalog_permission_to_user(user, 'delete') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_username_filter_as_non_staff_user(self): """ Verify HTTP 403 is returned when a non-staff user attempts to filter the Catalog list by username. """ user = UserFactory(is_staff=False, is_superuser=False) self.client.force_authenticate(user) response = self.client.get(self.catalog_list_url + '?username=jack') self.assertEqual(response.status_code, 403) expected = {'detail': 'Only staff users are permitted to filter by username. Remove the username parameter.'} self.assertDictEqual(response.data, expected) def test_username_filter_as_staff_user(self): """ Verify a list of Catalogs accessible by the given user is returned when filtering by username as a staff user. """ user = UserFactory(is_staff=False, is_superuser=False) catalog = CatalogFactory() path = '{root}?username={username}'.format(root=self.catalog_list_url, username=user.username) response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertListEqual(response.data['results'], []) self.grant_catalog_permission_to_user(user, 'view', catalog) response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertListEqual(response.data['results'], self.serialize_catalog([catalog], many=True)) def test_username_filter_as_staff_user_with_invalid_username(self): """ Verify HTTP 404 is returned if the given username does not correspond to an actual user. """ username = '******' path = '{root}?username={username}'.format(root=self.catalog_list_url, username=username) response = self.client.get(path) self.assertEqual(response.status_code, 404) expected = {'detail': 'No user with the username [{username}] exists.'.format(username=username)} self.assertDictEqual(response.data, expected)
class CatalogViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixin, APITestCase): """ Tests for the catalog resource. """ catalog_list_url = reverse('api:v1:catalog-list') def setUp(self): super().setUp() self.user = UserFactory(is_staff=True, is_superuser=True) self.request.user = self.user self.client.force_authenticate(self.user) self.catalog = CatalogFactory(query='title:abc*') enrollment_end = datetime.datetime.now( pytz.UTC) + datetime.timedelta(days=30) course_end = datetime.datetime.now( pytz.UTC) + datetime.timedelta(days=60) self.course_run = CourseRunFactory(enrollment_end=enrollment_end, end=course_end, course__title='ABC Test Course') self.course = self.course_run.course self.refresh_index() def assert_catalog_created(self, **headers): name = 'The Kitchen Sink' query = '*.*' viewer = UserFactory() data = {'name': name, 'query': query, 'viewers': [viewer.username]} response = self.client.post(self.catalog_list_url, data, format='json', **headers) assert response.status_code == 201 catalog = Catalog.objects.latest() self.assertDictEqual(response.data, self.serialize_catalog(catalog)) assert catalog.name == name assert catalog.query == query self.assertListEqual(list(catalog.viewers), [viewer]) def assert_catalog_contains_query_string(self, query_string_kwargs, course_key): """ Helper method to validate the provided course key or course run key in the catalog contains endpoint. """ query_string = urllib.parse.urlencode(query_string_kwargs) url = '{base_url}?{query_string}'.format(base_url=reverse( 'api:v1:catalog-contains', kwargs={'id': self.catalog.id}), query_string=query_string) response = self.client.get(url) assert response.status_code == 200 assert response.data == {'courses': {course_key: True}} def grant_catalog_permission_to_user(self, user, action, catalog=None): """ Grant the user access to view `self.catalog`. """ catalog = catalog or self.catalog perm = f'{action}_catalog' user.add_obj_perm(perm, catalog) assert user.has_perm(('catalogs.' + perm), catalog) def test_create_without_authentication(self): """ Verify authentication is required when creating, updating, or deleting a catalog. """ self.client.logout() Catalog.objects.all().delete() response = self.client.post(self.catalog_list_url, {}, format='json') assert response.status_code == 401 assert Catalog.objects.count() == 0 @ddt.data('put', 'patch', 'delete') def test_modify_without_authentication(self, http_method): """ Verify authentication is required to modify a catalog. """ self.client.logout() url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) response = getattr(self.client, http_method)(url, {}, format='json') assert response.status_code == 401 def test_create_with_session_authentication(self): """ Verify the endpoint creates a new catalog when the client is authenticated via session authentication. """ self.assert_catalog_created() def test_create_with_jwt_authentication(self): """ Verify the endpoint creates a new catalog when the client is authenticated via JWT authentication. """ self.client.logout() self.assert_catalog_created( HTTP_AUTHORIZATION=generate_jwt_header_for_user(self.user)) def test_create_with_new_user(self): """ Verify that new users are created if the list of viewers includes the usernames of non-existent users. """ new_viewer_username = '******' existing_viewer = UserFactory() viewers = [new_viewer_username, existing_viewer.username] data = { 'name': 'Test Catalog', 'query': '*:*', 'viewers': ','.join(viewers) } # NOTE: We explicitly avoid using the JSON data type so that we properly test string parsing. response = self.client.post(self.catalog_list_url, data) assert response.status_code == 201 catalog = Catalog.objects.latest() latest_user = User.objects.latest() assert latest_user.username == new_viewer_username assert set(catalog.viewers) == {existing_viewer, latest_user} def test_create_with_new_user_error(self): """ Verify no users are created if an error occurs while processing a create request. """ # The missing name and query fields should trigger an error data = {'viewers': ['new-guy']} original_user_count = User.objects.count() response = self.client.post(self.catalog_list_url, data) assert response.status_code == 400 assert User.objects.count() == original_user_count def test_catalog_if_query_is_incorrect(self): catalog = CatalogFactory(query='title:') CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), course__title='Science at the Polls: Biology for Voters, Part 1', status=CourseRunStatus.Published, type__is_marketable=True, ) CourseRunFactory( start=datetime.datetime(2015, 10, 13, tzinfo=pytz.UTC), course__title="DNA: Biology's Genetic Code", status=CourseRunStatus.Published, type__is_marketable=True, ) url = reverse('api:v1:catalog-courses', kwargs={'id': catalog.id}) response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == [] @ddt.data('title:Biology*', 'title:(*Biology* OR Biology)') def test_courses_with_different_catalog_queries_but_the_same_meaning( self, query): catalog = CatalogFactory(query=query) course_run_1 = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), course__title='Science at the Polls: Biology for Voters, Part 1', status=CourseRunStatus.Published, type__is_marketable=True, ) course_run_2 = CourseRunFactory( start=datetime.datetime(2015, 10, 13, tzinfo=pytz.UTC), course__title="DNA: Biology's Genetic Code", status=CourseRunStatus.Published, type__is_marketable=True, ) course_run_3 = CourseRunFactory( status=CourseRunStatus.Published, start=datetime.datetime(2015, 1, 1, tzinfo=pytz.UTC), course__title="AP Biology", type__is_marketable=True, ) SeatFactory.create(course_run=course_run_1) SeatFactory.create(course_run=course_run_2) SeatFactory.create(course_run=course_run_3) url = reverse('api:v1:catalog-courses', kwargs={'id': catalog.id}) response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == self.serialize_catalog_course( [course_run_1.course, course_run_2.course, course_run_3.course], many=True) def test_courses_with_time_range_query(self): catalog = CatalogFactory(query='start:[2015-01-01 TO 2015-12-01]') course_run_1 = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, ) course_run_2 = CourseRunFactory( start=datetime.datetime(2015, 10, 13, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, ) SeatFactory.create(course_run=course_run_1) SeatFactory.create(course_run=course_run_2) call_command('search_index', '--rebuild', '-f') url = reverse('api:v1:catalog-courses', kwargs={'id': catalog.id}) response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == self.serialize_catalog_course( [course_run_1.course, course_run_2.course], many=True) def test_courses_with_subjects_and_negative_query(self): catalog = CatalogFactory( # pylint: disable=line-too-long query= 'subjects:(-"Business & Management" AND -"Economics & Finance" AND -"Data Analysis & Statistics" AND -"Math" AND -"Engineering") AND org:(-galileox AND -davidsonnext AND -microsoft AND -gtx AND -pekingx AND -asux AND -bux AND -columbiax)' ) Course.objects.all().delete() not_included_subject_names = ( 'Business & Management', 'Economics & Finance', 'Business & Management', 'Economics & Finance', 'Data Analysis & Statistics', 'math', 'Engineering', ) for name in not_included_subject_names: course_run = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, course__subjects=[SubjectFactory(name=name)], ) SeatFactory.create(course_run=course_run) included_subject_names = ('Health & Sport', 'Galaxy') desired_courses = [] for name in included_subject_names: course_run = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, course__subjects=[SubjectFactory(name=name)], ) SeatFactory.create(course_run=course_run) desired_courses.append(course_run.course) not_included_org_names = ('galileox', 'davidsonnext', 'microsoft', 'gtx', 'pekingx', 'asux', 'bux', 'ColumbiaX') for name in not_included_org_names: course_run = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, key= f'{name}/{factory.Faker("word").evaluate(None, None, {"locale":"en"})}/test', ) SeatFactory.create(course_run=course_run) included_org_names = ('apple', 'cisco') for name in included_org_names: course_run = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, key= f'{name}/{factory.Faker("word").evaluate(None, None, {"locale":"en"})}/test', ) SeatFactory.create(course_run=course_run) desired_courses.append(course_run.course) call_command('search_index', '--rebuild', '-f') url = reverse('api:v1:catalog-courses', kwargs={'id': catalog.id}) response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == self.serialize_catalog_course( desired_courses, many=True) @ddt.data(*STATES()) def test_courses(self, state): """ Verify the endpoint returns the list of available courses contained in the catalog, and that courses appearing in the response always have at least one serialized run. """ url = reverse('api:v1:catalog-courses', kwargs={'id': self.catalog.id}) Course.objects.all().delete() course_run = CourseRunFactory(course__title='ABC Test Course') for function in state: function(course_run) course_run.save() if state in AVAILABLE_STATES: course = course_run.course # This run has no seats, but we still expect its parent course # to be included. filtered_course_run = CourseRunFactory(course=course) with self.assertNumQueries(31, threshold=3): response = self.client.get(url) assert response.status_code == 200 # Emulate prefetching behavior. filtered_course_run.delete() assert response.data['results'] == self.serialize_catalog_course( [course], many=True) # Any course appearing in the response must have at least one serialized run. assert response.data['results'][0]['course_runs'] else: response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == [] def test_courses_with_include_archived(self): """ Verify the endpoint returns the list of available and archived courses if include archived is True in catalog. """ url = reverse('api:v1:catalog-courses', kwargs={'id': self.catalog.id}) Course.objects.all().delete() now = datetime.datetime.now(pytz.UTC) future = now + datetime.timedelta(days=30) past = now - datetime.timedelta(days=30) course_run = CourseRunFactory.create( course__title='ABC Test Course With Archived', end=future, enrollment_end=future) SeatFactory.create(course_run=course_run) # Create an archived course run CourseRunFactory.create(course=course_run.course, end=past) response = self.client.get(url) assert response.status_code == 200 # The course appearing in response should have on 1 course run assert len(response.data['results'][0]['course_runs']) == 1 # Mark include archived True in catalog self.catalog.include_archived = True self.catalog.save() response = self.client.get(url) assert response.status_code == 200 # The course appearing in response should include archived course run assert len(response.data['results'][0]['course_runs']) == 2 def test_contains_for_course_key(self): """ Verify the endpoint returns a filtered list of courses contained in the catalog for course keys with the format "org+course". """ course_key = self.course.key query_string_kwargs = {'course_id': course_key} self.assert_catalog_contains_query_string(query_string_kwargs, course_key) def test_contains_for_course_run_key(self): """ Verify the endpoint returns a filtered list of courses contained in the catalog for course run keys with the format "org/course/run" or "course-v1:org+course+key". """ course_run_key = self.course_run.key query_string_kwargs = {'course_run_id': course_run_key} self.assert_catalog_contains_query_string(query_string_kwargs, course_run_key) def test_csv(self): SeatFactory(type=SeatTypeFactory.audit(), course_run=self.course_run) SeatFactory(type=SeatTypeFactory.verified(), course_run=self.course_run) SeatFactory(type=SeatTypeFactory.credit(), course_run=self.course_run, credit_provider='ASU', credit_hours=9) SeatFactory(type=SeatTypeFactory.credit(), course_run=self.course_run, credit_provider='Hogwarts', credit_hours=4) url = reverse('api:v1:catalog-csv', kwargs={'id': self.catalog.id}) with self.assertNumQueries(23): response = self.client.get(url) course_run = self.serialize_catalog_flat_course_run(self.course_run) expected = [ course_run['announcement'], course_run['content_language'], course_run['course_key'], course_run['end'], course_run['enrollment_end'], course_run['enrollment_start'], course_run['expected_learning_items'], course_run['full_description'], '', # image description '', # image height course_run['image']['src'], '', # image width course_run['key'], str(course_run['level_type']), course_run['marketing_url'], str(course_run['max_effort']), str(course_run['min_effort']), course_run['modified'], course_run['owners'], course_run['pacing_type'], course_run['prerequisites'], course_run['seats']['audit']['type'], '{}'.format(course_run['seats']['credit']['credit_hours']), '{}'.format(course_run['seats']['credit']['credit_provider']), '{}'.format(course_run['seats']['credit']['currency']), '{}'.format(str(course_run['seats']['credit']['price'])), '{}'.format(course_run['seats']['credit']['type']), '{}'.format(course_run['seats']['credit']['upgrade_deadline']), course_run['seats']['honor']['type'], course_run['seats']['masters']['type'], course_run['seats']['professional']['currency'], str(course_run['seats']['professional']['price']), course_run['seats']['professional']['type'], course_run['seats']['professional']['upgrade_deadline'], course_run['seats']['verified']['currency'], str(course_run['seats']['verified']['price']), course_run['seats']['verified']['type'], course_run['seats']['verified']['upgrade_deadline'], course_run['short_description'], course_run['sponsors'], course_run['start'], course_run['subjects'], course_run['title'], course_run['video']['description'], course_run['video']['image']['description'], str(course_run['video']['image']['height']), course_run['video']['image']['src'], str(course_run['video']['image']['width']), course_run['video']['src'], ] # collect streamed content received_content = b'' for item in response.streaming_content: received_content += item # convert received content to csv for comparison f = StringIO(received_content.decode('utf-8')) reader = csv.reader(f) content = list(reader) assert response.status_code == 200 assert expected == content[1] def test_get(self): """ Verify the endpoint returns the details for a single catalog. """ url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) response = self.client.get(url) assert response.status_code == 200 assert response.data == self.serialize_catalog(self.catalog) def test_list(self): """ Verify the endpoint returns a list of all catalogs. """ response = self.client.get(self.catalog_list_url) assert response.status_code == 200 self.assertListEqual( response.data['results'], self.serialize_catalog(Catalog.objects.all(), many=True)) def test_destroy(self): """ Verify the endpoint deletes a catalog. """ url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) response = self.client.delete(url) assert response.status_code == 204 assert not Catalog.objects.filter(id=self.catalog.id).exists() def test_update(self): """ Verify the endpoint updates a catalog. """ url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) name = 'Updated Catalog' query = 'so-not-real' data = {'name': name, 'query': query} response = self.client.put(url, data, format='json') assert response.status_code == 200 catalog = Catalog.objects.get(id=self.catalog.id) assert catalog.name == name assert catalog.query == query def test_partial_update(self): """ Verify the endpoint supports partially updating a catalog's fields. """ url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) name = 'Updated Catalog' query = self.catalog.query data = {'name': name} response = self.client.patch(url, data, format='json') assert response.status_code == 200 catalog = Catalog.objects.get(id=self.catalog.id) assert catalog.name == name assert catalog.query == query def test_retrieve_permissions(self): """ Verify only users with the correct permissions can create, read, or modify a Catalog. """ # Use an unprivileged user user = UserFactory(is_staff=False, is_superuser=False) self.client.force_authenticate(user) url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) # A user with no permissions should NOT be able to view a Catalog. assert not user.has_perm('catalogs.view_catalog', self.catalog) response = self.client.get(url) assert response.status_code == 403 # The permitted user should be able to view the Catalog. self.grant_catalog_permission_to_user(user, 'view') response = self.client.get(url) assert response.status_code == 200 def test_list_permissions(self): """ Verify only catalogs accessible to the user are returned in the list view. """ user = UserFactory(is_staff=False, is_superuser=False) self.client.force_authenticate(user) # An user with no permissions should not see any catalogs response = self.client.get(self.catalog_list_url) assert response.status_code == 200 self.assertListEqual(response.data['results'], []) # The client should be able to see permissions for which it has access self.grant_catalog_permission_to_user(user, 'view') response = self.client.get(self.catalog_list_url) assert response.status_code == 200 self.assertListEqual(response.data['results'], self.serialize_catalog([self.catalog], many=True)) def test_write_permissions(self): """ Verify only authorized users can update or delete Catalogs. """ url = reverse('api:v1:catalog-detail', kwargs={'id': self.catalog.id}) user = UserFactory(is_staff=False, is_superuser=False) self.client.force_authenticate(user) # Unprivileged users cannot modify Catalogs response = self.client.put(url) assert response.status_code == 403 response = self.client.delete(url) assert response.status_code == 403 # With the right permissions, the user can perform the specified actions self.grant_catalog_permission_to_user(user, 'change') response = self.client.patch(url, {'query': '*:*'}) assert response.status_code == 200 self.grant_catalog_permission_to_user(user, 'delete') response = self.client.delete(url) assert response.status_code == 204 def test_username_filter_as_non_staff_user(self): """ Verify HTTP 403 is returned when a non-staff user attempts to filter the Catalog list by username. """ user = UserFactory(is_staff=False, is_superuser=False) self.client.force_authenticate(user) response = self.client.get(self.catalog_list_url + '?username=jack') assert response.status_code == 403 expected = { 'detail': 'Only staff users are permitted to filter by username. Remove the username parameter.' } self.assertDictEqual(response.data, expected) def test_username_filter_as_staff_user(self): """ Verify a list of Catalogs accessible by the given user is returned when filtering by username as a staff user. """ user = UserFactory(is_staff=False, is_superuser=False) catalog = CatalogFactory() path = f'{self.catalog_list_url}?username={user.username}' response = self.client.get(path) assert response.status_code == 200 self.assertListEqual(response.data['results'], []) self.grant_catalog_permission_to_user(user, 'view', catalog) response = self.client.get(path) assert response.status_code == 200 self.assertListEqual(response.data['results'], self.serialize_catalog([catalog], many=True)) def test_username_filter_as_staff_user_with_invalid_username(self): """ Verify HTTP 404 is returned if the given username does not correspond to an actual user. """ username = '******' path = f'{self.catalog_list_url}?username={username}' response = self.client.get(path) assert response.status_code == 404 expected = { 'detail': f'No user with the username [{username}] exists.' } self.assertDictEqual(response.data, expected)
def test_courses_with_subjects_and_negative_query(self): catalog = CatalogFactory( # pylint: disable=line-too-long query= 'subjects:(-"Business & Management" AND -"Economics & Finance" AND -"Data Analysis & Statistics" AND -"Math" AND -"Engineering") AND org:(-galileox AND -davidsonnext AND -microsoft AND -gtx AND -pekingx AND -asux AND -bux AND -columbiax)' ) Course.objects.all().delete() not_included_subject_names = ( 'Business & Management', 'Economics & Finance', 'Business & Management', 'Economics & Finance', 'Data Analysis & Statistics', 'math', 'Engineering', ) for name in not_included_subject_names: course_run = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, course__subjects=[SubjectFactory(name=name)], ) SeatFactory.create(course_run=course_run) included_subject_names = ('Health & Sport', 'Galaxy') desired_courses = [] for name in included_subject_names: course_run = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, course__subjects=[SubjectFactory(name=name)], ) SeatFactory.create(course_run=course_run) desired_courses.append(course_run.course) not_included_org_names = ('galileox', 'davidsonnext', 'microsoft', 'gtx', 'pekingx', 'asux', 'bux', 'ColumbiaX') for name in not_included_org_names: course_run = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, key= f'{name}/{factory.Faker("word").evaluate(None, None, {"locale":"en"})}/test', ) SeatFactory.create(course_run=course_run) included_org_names = ('apple', 'cisco') for name in included_org_names: course_run = CourseRunFactory( start=datetime.datetime(2015, 9, 1, tzinfo=pytz.UTC), status=CourseRunStatus.Published, type__is_marketable=True, key= f'{name}/{factory.Faker("word").evaluate(None, None, {"locale":"en"})}/test', ) SeatFactory.create(course_run=course_run) desired_courses.append(course_run.course) call_command('search_index', '--rebuild', '-f') url = reverse('api:v1:catalog-courses', kwargs={'id': catalog.id}) response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == self.serialize_catalog_course( desired_courses, many=True)