def _define_course_metadata(self): # Add course runs, courses, and programs to DB to copy data into courses = {} # Course runs map to courses in the way opaque keys would (without actually using opaque keys code) course_to_run_mapping = { '00test/00test/00test': '00test/00test', '00test/00test/01test': '00test/00test', '00test/01test/00test': '00test/01test', '00test/01test/01test': '00test/01test', '00test/01test/02test': '00test/01test', '00test/02test/00test': '00test/02test' } for course_summary in self.mocked_data: course_run_key = course_summary['course_id'] course_key = course_to_run_mapping[course_run_key] if course_key in courses: course = courses[course_key] course_run = CourseRunFactory(key=course_summary['course_id'], course=course) course_run.save() else: course = CourseFactory(key=course_key) course.save() course_run = CourseRunFactory(key=course_summary['course_id'], course=course) course_run.save() courses[course_key] = course_run.course # Create a program with all of the courses we created program = ProgramFactory() program.courses.set(courses.values()) # pylint: disable=no-member
def test_available(self): """ Verify the method filters Courses to those which contain at least one CourseRun that can be enrolled in immediately, is ongoing or yet to start, and appears on the marketing site. """ for state in self.states(): Course.objects.all().delete() course_run = CourseRunFactory() for function in state: function(course_run) course_run.save() if state in self.available_states: course = course_run.course # This course is available, so should be returned by the # available() query. assert list(Course.objects.available()) == [course] # This run has no seats, but we still expect its parent course # to be included. CourseRunFactory(course=course) assert list(Course.objects.available()) == [course] # Generate another course run with available seats. # Only one instance of the course should be included in the result. other_course_run = CourseRunFactory(course=course) for function in state: function(other_course_run) other_course_run.save() assert list(Course.objects.available()) == [course] else: assert list(Course.objects.available()) == []
def test_populate_official_with_existing_draft(self): course_run = CourseRunFactory(draft=True, course=CourseFactory(draft=True)) course_run.status = CourseRunStatus.Reviewed course_run.save() salesforce_id = 'SomeSalesforceId' course_run_2 = CourseRunFactory(draft=True, course=CourseFactory( draft=True, salesforce_id=salesforce_id), salesforce_id=salesforce_id) course_run_2.status = CourseRunStatus.Reviewed course_run_2.save() # Need to modify state of the instance passed in def new_create_instance(instance): instance.salesforce_id = salesforce_id instance.save() with mock.patch( 'course_discovery.apps.course_metadata.tests.test_salesforce.CourseRun.save' ): with mock.patch( 'course_discovery.apps.course_metadata.tests.test_salesforce.Course.save' ): with mock.patch( self.salesforce_util_path) as mock_salesforce_util: with mock.patch(self.salesforce_util_path + '.create_course_run', new=new_create_instance): with mock.patch(self.salesforce_util_path + '.create_course', new=new_create_instance): created = populate_official_with_existing_draft( course_run.official_version, mock_salesforce_util) assert created assert course_run.official_version.salesforce_id == salesforce_id created = populate_official_with_existing_draft( course_run.official_version.course, mock_salesforce_util) assert created assert course_run.official_version.course.salesforce_id == salesforce_id created = populate_official_with_existing_draft( course_run_2.official_version, mock_salesforce_util) assert not created assert course_run_2.official_version.salesforce_id == salesforce_id created = populate_official_with_existing_draft( course_run_2.official_version.course, mock_salesforce_util) assert not created assert course_run_2.official_version.course.salesforce_id == salesforce_id
def test_marketable_exclusions(self): """ Verify the method excludes CourseRuns without a slug. """ course_run = CourseRunFactory() SeatFactory(course_run=course_run) course_run.slug = '' # blank out auto-generated slug course_run.save() self.assertEqual(CourseRun.objects.marketable().exists(), False)
def test_marketable_unpublished_exclusions(self, is_published): """ Verify the method excludes CourseRuns with Unpublished status. """ course_run = CourseRunFactory(status=CourseRunStatus.Unpublished) SeatFactory(course_run=course_run) if is_published: course_run.status = CourseRunStatus.Published course_run.save() assert CourseRun.objects.marketable().exists() == is_published
def test_ignores_drafts(self, mock_publish): # Draft run doesn't get published run = CourseRunFactory(draft=True, status=CourseRunStatus.Reviewed, go_live_date=self.past) self.handle() self.assertEqual(mock_publish.call_count, 0) # But sanity check by confirming that if it *is* an official version, it does. run.draft = False run.save() self.handle() self.assertEqual(mock_publish.call_count, 1)
def create_discovery_course_run_with_metadata(course, metadata): """ Creates and returns a Discovery CourseRun object with course and fields specified in metadata dictionary. Arguments: course: a Course object to assign to the created Discovery CourseRun.course field metadata: a dictionary where the keys are field names and values are field values For example, metadata could be {'pacing_type': 'Instructor-paced'}. Returns: a Discovery CourseRun object """ discovery_course_run = DiscoveryCourseRunFactory(course=course, **metadata) discovery_course_run.save() return discovery_course_run
def test_courses(self): """ 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}) for state in self.states(): 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 self.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(19): 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: with self.assertNumQueries(3): response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == []
class AffiliateWindowViewSetTests(ElasticsearchTestMixin, SerializationMixin, APITestCase): """ Tests for the AffiliateWindowViewSet. """ 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_without_authentication(self): """ Verify authentication is required when accessing the endpoint. """ self.client.logout() response = self.client.get(self.affiliate_url) self.assertEqual(response.status_code, 403) def test_affiliate_with_supported_seats(self): """ Verify that endpoint returns course runs for verified and professional seats only. """ response = self.client.get(self.affiliate_url) self.assertEqual(response.status_code, 200) root = ET.fromstring(response.content) self.assertEqual(1, len(root.findall('product'))) self.assert_product_xml( root.findall('product/[pid="{}-{}"]'.format(self.course_run.key, self.seat_verified.type))[0], self.seat_verified ) # Add professional seat. seat_professional = SeatFactory(course_run=self.course_run, type=Seat.PROFESSIONAL) response = self.client.get(self.affiliate_url) root = ET.fromstring(response.content) self.assertEqual(2, len(root.findall('product'))) self.assert_product_xml( root.findall('product/[pid="{}-{}"]'.format(self.course_run.key, self.seat_verified.type))[0], self.seat_verified ) self.assert_product_xml( root.findall('product/[pid="{}-{}"]'.format(self.course_run.key, seat_professional.type))[0], seat_professional ) @ddt.data(Seat.CREDIT, Seat.HONOR, Seat.AUDIT) def test_with_non_supported_seats(self, non_supporting_seat): """ Verify that endpoint returns no data for honor, credit and audit seats. """ self.seat_verified.type = non_supporting_seat self.seat_verified.save() response = self.client.get(self.affiliate_url) self.assertEqual(response.status_code, 200) root = ET.fromstring(response.content) self.assertEqual(0, len(root.findall('product'))) def test_with_closed_enrollment(self): """ Verify that endpoint returns no data if enrollment is close. """ self.course_run.enrollment_end = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=100) self.course_run.end = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=100) self.course_run.save() # new course run with future end date and no enrollment_date. CourseRunFactory(end=self.course_end, course=self.course, enrollment_end=None) response = self.client.get(self.affiliate_url) self.assertEqual(response.status_code, 200) root = ET.fromstring(response.content) self.assertEqual(0, len(root.findall('product'))) def assert_product_xml(self, content, seat): """ Helper method to verify product data in xml format. """ self.assertEqual(content.find('pid').text, '{}-{}'.format(self.course_run.key, seat.type)) self.assertEqual(content.find('name').text, self.course_run.title) self.assertEqual(content.find('desc').text, self.course_run.short_description) self.assertEqual(content.find('purl').text, self.course_run.marketing_url) self.assertEqual(content.find('imgurl').text, self.course_run.image.src) self.assertEqual(content.find('price/actualp').text, str(seat.price)) self.assertEqual(content.find('currency').text, seat.currency.code) self.assertEqual(content.find('category').text, AffiliateWindowSerializer.CATEGORY) def test_dtd_with_valid_data(self): """ Verify the XML data produced by the endpoint conforms to the DTD file. """ response = self.client.get(self.affiliate_url) self.assertEqual(response.status_code, 200) filename = abspath(join(dirname(dirname(__file__)), 'affiliate_window_product_feed.1.4.dtd')) dtd = etree.DTD(open(filename)) root = etree.XML(response.content) self.assertTrue(dtd.validate(root)) 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)
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 AffiliateWindowViewSetTests(ElasticsearchTestMixin, SerializationMixin, APITestCase): """ Tests for the AffiliateWindowViewSet. """ 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=SeatTypeFactory.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_without_authentication(self): """ Verify authentication is required when accessing the endpoint. """ self.client.logout() response = self.client.get(self.affiliate_url) self.assertEqual(response.status_code, 401) def test_affiliate_with_supported_seats(self): """ Verify that endpoint returns course runs for verified and professional seats only. """ response = self.client.get(self.affiliate_url) self.assertEqual(response.status_code, 200) root = ET.fromstring(response.content) self.assertEqual(1, len(root.findall('product'))) self.assert_product_xml( root.findall('product/[pid="{}-{}"]'.format( self.course_run.key, self.seat_verified.type.slug))[0], self.seat_verified) # Add professional seat seat_professional = SeatFactory(course_run=self.course_run, type=SeatTypeFactory.professional()) response = self.client.get(self.affiliate_url) root = ET.fromstring(response.content) self.assertEqual(2, len(root.findall('product'))) self.assert_product_xml( root.findall('product/[pid="{}-{}"]'.format( self.course_run.key, self.seat_verified.type.slug))[0], self.seat_verified) self.assert_product_xml( root.findall('product/[pid="{}-{}"]'.format( self.course_run.key, seat_professional.type.slug))[0], seat_professional) @ddt.data(Seat.CREDIT, Seat.HONOR, Seat.AUDIT) def test_with_non_supported_seats(self, non_supporting_seat): """ Verify that endpoint returns no data for honor, credit and audit seats. """ self.seat_verified.type = SeatType.objects.get_or_create( slug=non_supporting_seat)[0] self.seat_verified.save() response = self.client.get(self.affiliate_url) self.assertEqual(response.status_code, 200) root = ET.fromstring(response.content) self.assertEqual(0, len(root.findall('product'))) def test_with_closed_enrollment(self): """ Verify that endpoint returns no data if enrollment is close. """ self.course_run.enrollment_end = datetime.datetime.now( pytz.UTC) - datetime.timedelta(days=100) self.course_run.end = datetime.datetime.now( pytz.UTC) - datetime.timedelta(days=100) self.course_run.save() # new course run with future end date and no enrollment_date. CourseRunFactory(end=self.course_end, course=self.course, enrollment_end=None) response = self.client.get(self.affiliate_url) self.assertEqual(response.status_code, 200) root = ET.fromstring(response.content) self.assertEqual(0, len(root.findall('product'))) def assert_product_xml(self, content, seat): """ Helper method to verify product data in xml format. """ assert content.find('pid').text == '{}-{}'.format( self.course_run.key, seat.type.slug) assert content.find('name').text == self.course_run.title assert content.find('desc').text == self.course_run.full_description assert content.find('purl').text == self.course_run.marketing_url assert content.find('imgurl').text == self.course_run.image_url assert content.find('price/actualp').text == str(seat.price) assert content.find('currency').text == seat.currency.code assert content.find( 'category').text == AffiliateWindowSerializer.CATEGORY def test_dtd_with_valid_data(self): """ Verify the XML data produced by the endpoint conforms to the DTD file. """ response = self.client.get(self.affiliate_url) assert response.status_code == 200 filename = abspath( join(dirname(dirname(__file__)), 'affiliate_window_product_feed.1.4.dtd')) dtd = etree.DTD(open(filename)) root = etree.XML(response.content) assert dtd.validate(root) 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(6, threshold=1): # travis is often 7 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(9, threshold=1): # travis is often 10 response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_unpublished_status(self): """ Verify the endpoint does not return CourseRuns in a non-published state. """ self.course_run.status = CourseRunStatus.Unpublished self.course_run.save() CourseRunFactory(course=self.course, status=CourseRunStatus.Unpublished) response = self.client.get(self.affiliate_url) self.assertEqual(response.status_code, 200) root = ET.fromstring(response.content) self.assertEqual(0, len(root.findall('product')))
class TestLoadProgramFixture(TestCase): oauth_host = 'http://example.com' catalog_host = 'http://discovery-example.com' def setUp(self): super().setUp() self.pk_generator = itertools.count(1) stored_site, created = Site.objects.get_or_create( # pylint: disable=unused-variable domain='example.com') self.default_partner = Partner.objects.create(site=stored_site, name='edX', short_code='edx') SeatType.objects.all().delete() ProgramType.objects.all().delete() self.partner = PartnerFactory(name='Test') self.organization = OrganizationFactory(partner=self.partner) self.seat_type_verified = SeatTypeFactory(name='Verified', slug='verified') self.program_type_masters = ProgramTypeFactory( name='Masters', slug='masters', applicable_seat_types=[self.seat_type_verified]) self.program_type_mm = ProgramTypeFactory( name='MicroMasters', slug='micromasters', applicable_seat_types=[self.seat_type_verified]) self.course = CourseFactory( partner=self.partner, authoring_organizations=[self.organization]) self.course_run = CourseRunFactory(course=self.course) self.program = ProgramFactory( type=self.program_type_masters, partner=self.partner, authoring_organizations=[self.organization]) self.course_mm = CourseFactory( partner=self.partner, authoring_organizations=[self.organization]) self.course_run_mm = CourseRunFactory(course=self.course) self.program_mm = ProgramFactory( type=self.program_type_mm, partner=self.partner, authoring_organizations=[self.organization], courses=[self.course_mm]) self.curriculum = CurriculumFactory(program=self.program) self.curriculum_course_membership = CurriculumCourseMembershipFactory( course=self.course, curriculum=self.curriculum) self.curriculum_program_membership = CurriculumProgramMembershipFactory( program=self.program_mm, curriculum=self.curriculum) self.program_2 = ProgramFactory( type=self.program_type_masters, partner=self.partner, authoring_organizations=[self.organization]) self._mock_oauth_request() def _mock_oauth_request(self): responses.add( responses.POST, f'{self.oauth_host}/oauth2/access_token', json={ 'access_token': 'abcd', 'expires_in': 60 }, status=200, ) def _mock_fixture_response(self, fixture): url = re.compile( '{catalog_host}/extensions/api/v1/program-fixture/'.format( catalog_host=self.catalog_host, )) responses.add(responses.GET, url, body=fixture, status=200) def _call_load_program_fixture(self, program_uuids): call_command( 'load_program_fixture', ','.join(program_uuids), '--catalog-host', self.catalog_host, '--oauth-host', self.oauth_host, '--client-id', 'foo', '--client-secret', 'bar', ) def _set_up_masters_program_type(self): """ Set DB to have a conflicting program type on load. """ seat_type = SeatTypeFactory( name='Something', slug='something', ) existing_program_type = ProgramTypeFactory( name='Masters', name_t='Masters', slug='masters', applicable_seat_types=[seat_type]) return existing_program_type def reset_db_state(self): Partner.objects.all().exclude(short_code='edx').delete() SeatType.objects.all().delete() Course.objects.all().delete() CourseRun.objects.all().delete() Curriculum.objects.all().delete() CurriculumCourseMembership.objects.all().delete() CurriculumProgramMembership.objects.all().delete() ProgramType.objects.all().delete() Organization.objects.all().delete() Program.objects.all().delete() @responses.activate def test_load_programs(self): fixture = json_serializer.Serializer().serialize([ self.program_type_masters, self.program_type_mm, self.organization, self.seat_type_verified, self.program, self.program_2, self.program_mm, self.curriculum_program_membership, self.curriculum_course_membership, self.curriculum, self.course, self.course_mm, self.course_run, self.course_run_mm, ]) self._mock_fixture_response(fixture) requested_programs = [ str(self.program.uuid), str(self.program_2.uuid), ] self.reset_db_state() self._call_load_program_fixture(requested_programs) # walk through program structure to validate correct # objects have been created stored_program = Program.objects.get(uuid=self.program.uuid) stored_program_2 = Program.objects.get(uuid=self.program_2.uuid) self.assertEqual(stored_program.title, self.program.title) self.assertEqual(stored_program_2.title, self.program_2.title) stored_organization = stored_program.authoring_organizations.first() self.assertEqual(stored_organization.name, self.organization.name) # partner should use existing edx value self.assertEqual(stored_program.partner, self.default_partner) self.assertEqual(stored_organization.partner, self.default_partner) stored_program_type = stored_program.type self.assertEqual(stored_program_type.name_t, self.program_type_masters.name) stored_seat_type = stored_program_type.applicable_seat_types.first() self.assertEqual(stored_seat_type.name, self.seat_type_verified.name) stored_curriculum = stored_program.curricula.first() self.assertEqual(stored_curriculum.uuid, self.curriculum.uuid) stored_course = stored_curriculum.course_curriculum.first() self.assertEqual(stored_course.key, self.course.key) stored_mm = stored_curriculum.program_curriculum.first() self.assertEqual(stored_mm.uuid, self.program_mm.uuid) stored_course_run = stored_course.course_runs.first() self.assertEqual(stored_course_run.key, self.course_run.key) @responses.activate def test_update_existing_program_type(self): fixture = json_serializer.Serializer().serialize([ self.organization, self.seat_type_verified, self.program_type_masters, self.program, ]) self._mock_fixture_response(fixture) self.reset_db_state() existing_program_type = self._set_up_masters_program_type() self._call_load_program_fixture([str(self.program.uuid)]) stored_program = Program.objects.get(uuid=self.program.uuid) # assert existing DB value is used stored_program_type = stored_program.type self.assertEqual(stored_program_type, existing_program_type) # assert existing DB value is updated to match fixture stored_seat_types = list( stored_program_type.applicable_seat_types.all()) self.assertEqual(len(stored_seat_types), 1) self.assertEqual(stored_seat_types[0].name, self.seat_type_verified.name) @responses.activate def test_remapping_courserun_programtype(self): """ Tests whether the remapping of program types works for the course run field that points to them """ self.course_run.expected_program_type = self.program_type_masters self.course_run.save() fixture = json_serializer.Serializer().serialize([ self.program_type_masters, self.program_type_mm, self.organization, self.seat_type_verified, self.program, self.program_mm, self.curriculum_program_membership, self.curriculum_course_membership, self.curriculum, self.course, self.course_mm, self.course_run, ]) self._mock_fixture_response(fixture) self.reset_db_state() existing_program_type = self._set_up_masters_program_type() self._call_load_program_fixture([str(self.program.uuid)]) stored_courserun = CourseRun.objects.get(key=self.course_run.key) stored_program_type = stored_courserun.expected_program_type self.assertEqual(existing_program_type, stored_program_type) @responses.activate def test_existing_seat_types(self): fixture = json_serializer.Serializer().serialize([ self.organization, self.seat_type_verified, self.program_type_masters, self.program, ]) self._mock_fixture_response(fixture) self.reset_db_state() # create existing verified seat with different pk than fixture and # a second seat type with the same pk but different values new_pk = self.seat_type_verified.id + 1 SeatType.objects.create(id=new_pk, name='Verified', slug='verified') SeatType.objects.create(id=self.seat_type_verified.id, name='Test', slug='test') self._call_load_program_fixture([str(self.program.uuid)]) stored_program = Program.objects.get(uuid=self.program.uuid) stored_seat_type = stored_program.type.applicable_seat_types.first() self.assertEqual(stored_seat_type.id, new_pk) self.assertEqual(stored_seat_type.name, self.seat_type_verified.name) @responses.activate def test_fail_on_save_error(self): fixture = json_serializer.Serializer().serialize([ self.organization, ]) # Should not be able to save an organization without uuid fixture_json = json.loads(fixture) fixture_json[0]['fields']['uuid'] = None fixture = json.dumps(fixture_json) self._mock_fixture_response(fixture) self.reset_db_state() with pytest.raises(IntegrityError) as err: self._call_load_program_fixture([str(self.program.uuid)]) expected_msg = fr'Failed to save course_metadata.Organization\(pk={self.organization.id}\):' assert re.match(expected_msg, str(err.value)) @responses.activate def test_fail_on_constraint_error(self): # duplicate programs should successfully save but fail final constraint check fixture = json_serializer.Serializer().serialize([ self.program, self.program, self.seat_type_verified, self.program_type_masters, ]) self._mock_fixture_response(fixture) self.reset_db_state() with pytest.raises(IntegrityError) as err: self._call_load_program_fixture([str(self.program.uuid)]) expected_msg = ( r'Checking database constraints failed trying to load fixtures. Unable to save program\(s\):' ).format(pk=self.organization.id) assert re.match(expected_msg, str(err.value)) @responses.activate def test_ignore_program_external_key(self): fixture = json_serializer.Serializer().serialize([ self.organization, self.seat_type_verified, self.program_type_masters, self.program, ]) self._mock_fixture_response(fixture) self.reset_db_state() self._call_load_program_fixture([ '{uuid}:{external_key}'.format(uuid=str(self.program.uuid), external_key='CS-104-FALL-2019') ]) Program.objects.get(uuid=self.program.uuid) @responses.activate def test_update_existing_data(self): fixture = json_serializer.Serializer().serialize([ self.organization, self.seat_type_verified, self.program_type_masters, self.program, self.curriculum, self.course, self.course_run, self.curriculum_course_membership, ]) self._mock_fixture_response(fixture) self._call_load_program_fixture([str(self.program.uuid)]) self.program.title = 'program-title-modified' self.course.title = 'course-title-modified' new_course = CourseFactory(partner=self.partner, authoring_organizations=[self.organization]) new_course_run = CourseRunFactory(course=new_course) new_course_membership = CurriculumCourseMembershipFactory( course=new_course, curriculum=self.curriculum) fixture = json_serializer.Serializer().serialize([ self.organization, self.seat_type_verified, self.program_type_masters, self.program, self.curriculum, self.course, self.course_run, self.curriculum_course_membership, new_course_membership, new_course, new_course_run, ]) responses.reset() self._mock_oauth_request() self._mock_fixture_response(fixture) self.reset_db_state() self._call_load_program_fixture([str(self.program.uuid)]) stored_program = Program.objects.get(uuid=self.program.uuid) self.assertEqual(stored_program.title, 'program-title-modified') stored_program_courses = stored_program.curricula.first( ).course_curriculum.all() modified_existing_course = stored_program_courses.get( uuid=self.course.uuid) stored_new_course = stored_program_courses.get(uuid=new_course.uuid) self.assertEqual(len(stored_program_courses), 2) self.assertEqual(modified_existing_course.title, 'course-title-modified') self.assertEqual(stored_new_course.key, new_course.key)
class TestLoadDrupalData(TestCase): def setUp(self): super(TestLoadDrupalData, self).setUp() self.command_name = 'load_drupal_data' self.partner = PartnerFactory() self.course_run = CourseRunFactory(course__partner=self.partner) self.course_run.course.canonical_course_run = self.course_run self.course_run.course.save() def mock_access_token_api(self, requests_mock=None): body = {'access_token': ACCESS_TOKEN, 'expires_in': 30} requests_mock = requests_mock or responses url = self.partner.oidc_url_root.strip('/') + '/access_token' requests_mock.add_callback(responses.POST, url, callback=mock_api_callback( url, body, results_key=False), content_type='application/json') return body def test_load_drupal_data_with_partner(self): with responses.RequestsMock() as rsps: self.mock_access_token_api(rsps) with mock.patch( 'course_discovery.apps.publisher.management.commands.' 'load_drupal_data.execute_loader') as mock_executor: config = DrupalLoaderConfigFactory.create( course_run_ids='course-v1:SC+BreadX+3T2015', partner_code=self.partner.short_code, load_unpublished_course_runs=False) call_command('load_drupal_data') expected_calls = [ mock.call(DrupalCourseMarketingSiteDataLoader, self.partner, self.partner.marketing_site_url_root, ACCESS_TOKEN, 'JWT', 1, False, set(config.course_run_ids.split(',')), config.load_unpublished_course_runs, username=jwt.decode( ACCESS_TOKEN, verify=False)['preferred_username']) ] mock_executor.assert_has_calls(expected_calls) def test_process_node(self): # Set the end date in the future data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0] data['field_course_end_date'] = datetime.datetime.max.strftime('%s') OrganizationFactory.create( uuid=data.get('field_course_school_node', {})[0].get('uuid')) config = DrupalLoaderConfigFactory.create( course_run_ids=data.get('field_course_id'), partner_code=self.partner.short_code, load_unpublished_course_runs=False) data_loader = DrupalCourseMarketingSiteDataLoader( self.partner, self.partner.marketing_site_url_root, ACCESS_TOKEN, 'JWT', 1, # Make this a constant of 1 for no concurrency False, set(config.course_run_ids.split(',')), config.load_unpublished_course_runs) # Need to mock this method so that the GET isn't sent out to the test data server with mock.patch( 'course_discovery.apps.publisher.dataloader.create_courses.' 'transfer_course_image'): data_loader.process_node( mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]) course_metadata_course_run = CourseMetadataCourseRun.objects.get( key=data.get('field_course_id')) self.assertIsNotNone(course_metadata_course_run) self.assertIsNotNone(course_metadata_course_run.course) publisher_course_run = PublisherCourseRun.objects.get( lms_course_id=course_metadata_course_run.key) self.assertIsNotNone(publisher_course_run) self.assertIsNotNone(publisher_course_run.course) def test_process_node_archived(self): # Set the end date in the past data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0] data['field_course_end_date'] = datetime.datetime.min.strftime('%s') OrganizationFactory.create( uuid=data.get('field_course_school_node', {})[0].get('uuid')) config = DrupalLoaderConfigFactory.create( course_run_ids=data.get('field_course_id'), partner_code=self.partner.short_code, load_unpublished_course_runs=False) data_loader = DrupalCourseMarketingSiteDataLoader( self.partner, self.partner.marketing_site_url_root, ACCESS_TOKEN, 'JWT', 1, # Make this a constant of 1 for no concurrency False, set(config.course_run_ids.split(',')), config.load_unpublished_course_runs) # Need to mock this method so that the GET isn't sent out to the test data server with mock.patch( 'course_discovery.apps.publisher.dataloader.create_courses.' 'transfer_course_image'): data_loader.process_node( mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]) course_metadata_course_run = CourseMetadataCourseRun.objects.filter( key=data.get('field_course_id')) self.assertEqual(course_metadata_course_run.count(), 0) def test_process_node_not_whitelisted(self): # Set the end date in the future data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0] data['field_course_end_date'] = datetime.datetime.max.strftime('%s') OrganizationFactory.create( uuid=data.get('field_course_school_node', {})[0].get('uuid')) config = DrupalLoaderConfigFactory.create( course_run_ids='SomeFakeCourseRunId', partner_code=self.partner.short_code, load_unpublished_course_runs=False) data_loader = DrupalCourseMarketingSiteDataLoader( self.partner, self.partner.marketing_site_url_root, ACCESS_TOKEN, 'JWT', 1, # Make this a constant of 1 for no concurrency False, set(config.course_run_ids.split(',')), config.load_unpublished_course_runs) # Need to mock this method so that the GET isn't sent out to the test data server with mock.patch( 'course_discovery.apps.publisher.dataloader.create_courses.' 'transfer_course_image'): for body in mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES: data_loader.process_node(body) # Even after looping through all course bodies no new rows should be created course_metadata_course_run = CourseMetadataCourseRun.objects.filter( key=data.get('field_course_id')) self.assertEqual(course_metadata_course_run.count(), 0) def test_process_node_run_created(self): # Set the end date in the future data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0] data['field_course_end_date'] = datetime.datetime.max.strftime('%s') data['status'] = '1' OrganizationFactory.create( uuid=data.get('field_course_school_node', {})[0].get('uuid')) self.course_run.key = data.get('field_course_id') self.course_run.save() config = DrupalLoaderConfigFactory.create( course_run_ids=data.get('field_course_id'), partner_code=self.partner.short_code, load_unpublished_course_runs=False) data_loader = DrupalCourseMarketingSiteDataLoader( self.partner, self.partner.marketing_site_url_root, ACCESS_TOKEN, 'JWT', 1, # Make this a constant of 1 for no concurrency False, set(config.course_run_ids.split(',')), config.load_unpublished_course_runs) with mock.patch( 'course_discovery.apps.publisher.signals.create_course_run_in_studio_receiver' ) as mock_signal: # Need to mock this method so that the GET isn't sent out to the test data server with mock.patch( 'course_discovery.apps.publisher.dataloader.create_courses.' 'transfer_course_image'): data_loader.process_node( mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]) mock_signal.assert_not_called() course_metadata_course_run = CourseMetadataCourseRun.objects.get( key=data.get('field_course_id')) self.assertIsNotNone(course_metadata_course_run) self.assertIsNotNone(course_metadata_course_run.course) publisher_course_run = PublisherCourseRun.objects.get( lms_course_id=course_metadata_course_run.key) self.assertIsNotNone(publisher_course_run) self.assertIsNotNone(publisher_course_run.course) def test_load_unpublished_course_runs_with_flag_enabled(self): # Set the end date in the future data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0] data['field_course_end_date'] = datetime.datetime.max.strftime('%s') # Set the status to unpublished data['status'] = '0' OrganizationFactory.create( uuid=data.get('field_course_school_node', {})[0].get('uuid')) self.course_run.key = data.get('field_course_id') self.course_run.save() PublisherCourseFactory.create( course_metadata_pk=self.course_run.course.id) load_unpublished_course_runs = True config = DrupalLoaderConfigFactory.create( course_run_ids=data.get('field_course_id'), partner_code=self.partner.short_code, load_unpublished_course_runs=load_unpublished_course_runs) data_loader = DrupalCourseMarketingSiteDataLoader( self.partner, self.partner.marketing_site_url_root, ACCESS_TOKEN, 'JWT', 1, # Make this a constant of 1 for no concurrency False, set(config.course_run_ids.split(',')), load_unpublished_course_runs) # Need to mock this method so that the GET isn't sent out to the test data server with mock.patch( 'course_discovery.apps.publisher.dataloader.create_courses.' 'transfer_course_image'): data_loader.process_node( mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]) course_metadata_course_run = CourseMetadataCourseRun.objects.get( key=data.get('field_course_id')) self.assertIsNotNone(course_metadata_course_run) self.assertIsNotNone(course_metadata_course_run.course) publisher_course_run = PublisherCourseRun.objects.get( lms_course_id=course_metadata_course_run.key) self.assertIsNotNone(publisher_course_run) self.assertIsNotNone(publisher_course_run.course) def test_load_unpublished_course_runs_with_flag_enabled_no_course_found( self): # Set the end date in the future data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0] data['field_course_end_date'] = datetime.datetime.max.strftime('%s') # Set the status to unpublished data['status'] = '0' OrganizationFactory.create( uuid=data.get('field_course_school_node', {})[0].get('uuid')) self.course_run.key = data.get('field_course_id') self.course_run.save() load_unpublished_course_runs = True config = DrupalLoaderConfigFactory.create( course_run_ids=data.get('field_course_id'), partner_code=self.partner.short_code, load_unpublished_course_runs=load_unpublished_course_runs) data_loader = DrupalCourseMarketingSiteDataLoader( self.partner, self.partner.marketing_site_url_root, ACCESS_TOKEN, 'JWT', 1, # Make this a constant of 1 for no concurrency False, set(config.course_run_ids.split(',')), load_unpublished_course_runs) logger_target = 'course_discovery.apps.publisher.management.commands.load_drupal_data.logger' with mock.patch(logger_target) as mock_logger: # Need to mock this method so that the GET isn't sent out to the test data server with mock.patch( 'course_discovery.apps.publisher.dataloader.create_courses.' 'transfer_course_image'): data_loader.process_node( mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]) expected_calls = [ mock.call('No Publisher Course found for Course Run [%s]', self.course_run.key) ] mock_logger.info.assert_has_calls(expected_calls)
class CourseRunSerializerTests(TestCase): serializer_class = CourseRunSerializer def setUp(self): super(CourseRunSerializerTests, self).setUp() self.course_run = CourseRunFactory() self.course_run.lms_course_id = 'course-v1:edX+DemoX+Demo_Course' self.person = PersonFactory() self.discovery_course_run = DiscoveryCourseRunFactory( key=self.course_run.lms_course_id, staff=[self.person]) self.request = RequestFactory() self.user = UserFactory() self.request.user = self.user self.course_state = CourseRunStateFactory( course_run=self.course_run, owner_role=PublisherUserRole.Publisher) def get_expected_data(self): """ Helper method which will return expected serialize data. """ return { 'lms_course_id': self.course_run.lms_course_id, 'changed_by': self.user, 'preview_url': self.course_run.preview_url } def test_validate_lms_course_id(self): """ Verify that serializer raises error if 'lms_course_id' has invalid format. """ self.course_run.lms_course_id = 'invalid-course-id' self.course_run.save() serializer = self.serializer_class(self.course_run) with self.assertRaises(ValidationError): serializer.validate_lms_course_id(self.course_run.lms_course_id) def test_validate_preview_url(self): """ Verify that serializer raises error if 'preview_url' has invalid format. """ serializer = self.serializer_class(self.course_run, context={'request': self.request}) with self.assertRaises(ValidationError): serializer.validate({'preview_url': 'invalid-url'}) def test_serializer_with_valid_data(self): """ Verify that serializer validate course_run object. """ serializer = self.serializer_class(self.course_run, context={'request': self.request}) self.assertEqual(self.get_expected_data(), serializer.validate(serializer.data)) def test_update_preview_url(self): """ Verify that course 'owner_role' will be changed to course_team after updating course run with preview url. """ self.discovery_course_run.slug = '' self.discovery_course_run.save(suppress_publication=True) serializer = self.serializer_class(self.course_run, context={'request': self.request}) serializer.update(self.course_run, {'preview_url': 'https://example.com/abc/course'}) self.assertEqual(self.course_state.owner_role, PublisherUserRole.CourseTeam) self.assertEqual( self.course_run.preview_url.rsplit('/', 1)[-1], 'course') def test_update_preview_url_no_op(self): """ Verify we don't push to marketing if no change required """ self.discovery_course_run.slug = '' self.discovery_course_run.save(suppress_publication=True) toggle_switch('publish_course_runs_to_marketing_site') serializer = self.serializer_class(self.course_run, context={'request': self.request}) mock_path = 'course_discovery.apps.course_metadata.publishers.CourseRunMarketingSitePublisher.publish_obj' with mock.patch(mock_path) as mock_save: serializer.update( self.course_run, {'preview_url': 'https://example.com/abc/course'}) self.assertEqual(mock_save.call_count, 1) # Now when we update a second time, there should be nothing to do, call count should remain at 1 serializer.update( self.course_run, {'preview_url': 'https://example.com/abc/course'}) self.assertEqual(mock_save.call_count, 1) def test_update_preview_url_slug_exists(self): """ Verify we don't push to marketing if no change required """ DiscoveryCourseRunFactory( title='course') # will create the slug 'course' serializer = self.serializer_class(self.course_run, context={'request': self.request}) with self.assertRaises(Exception) as cm: serializer.update( self.course_run, {'preview_url': 'https://example.com/abc/course'}) self.assertEqual(cm.exception.args[0], 'Preview URL already in use for another course') def test_update_lms_course_id(self): """ Verify that 'changed_by' also updated after updating course_run's lms_course_id.""" serializer = self.serializer_class(self.course_run, context={'request': self.request}) serializer.update(self.course_run, serializer.validate(serializer.data)) self.assertEqual(self.course_run.lms_course_id, serializer.data['lms_course_id']) self.assertEqual(self.course_run.changed_by, self.user) def test_update_with_transaction_rollback(self): """ Verify that transaction roll backed if an error occurred. """ serializer = self.serializer_class(self.course_run) with self.assertRaises(Exception): serializer.update(self.course_run, {'preview_url': 'invalid_url'}) self.assertFalse(self.course_run.preview_url) def test_transaction_roll_back_with_error_on_email(self): """ Verify that transaction is roll backed if error occurred during email sending. """ toggle_switch('enable_publisher_email_notifications', True) serializer = self.serializer_class(self.course_run) self.assertEqual(self.course_run.course_run_state.owner_role, PublisherUserRole.Publisher) with self.assertRaises(Exception): serializer.update( self.course_run, {'preview_url': 'https://example.com/abc/course'}) self.course_run = CourseRun.objects.get(id=self.course_run.id) self.assertFalse(self.course_run.preview_url) # Verify that owner role not changed. self.assertEqual(self.course_run.course_run_state.owner_role, PublisherUserRole.Publisher)