def test_unpublished_sessions_for_entitlement_when_enrolled(self, mock_get_edx_api_data): """ Test unpublished course runs are part of visible session entitlements when the user is enrolled. """ catalog_course_run = CourseRunFactory.create(status=COURSE_UNPUBLISHED) catalog_course = CourseFactory(course_runs=[catalog_course_run]) mock_get_edx_api_data.return_value = catalog_course course_key = CourseKey.from_string(catalog_course_run.get('key')) course_overview = CourseOverviewFactory.create(id=course_key, start=self.tomorrow) CourseModeFactory.create( mode_slug=CourseMode.VERIFIED, min_price=100, course_id=course_overview.id, expiration_datetime=now() - timedelta(days=1) ) course_enrollment = CourseEnrollmentFactory( user=self.user, course_id=unicode(course_overview.id), mode=CourseMode.VERIFIED ) entitlement = CourseEntitlementFactory( user=self.user, enrollment_course_run=course_enrollment, mode=CourseMode.VERIFIED ) session_entitlements = get_visible_sessions_for_entitlement(entitlement) self.assertEqual(session_entitlements, [catalog_course_run])
def test_get_course_runs_by_course(self, mock_get_edx_api_data): """ Test retrievals of run from a Course. """ catalog_course_runs = CourseRunFactory.create_batch(10) catalog_course = CourseFactory(course_runs=catalog_course_runs) mock_get_edx_api_data.return_value = catalog_course data = get_course_runs_for_course(course_uuid=str(catalog_course['uuid'])) self.assertTrue(mock_get_edx_api_data.called) self.assertEqual(data, catalog_course_runs)
def test_get_course_runs(self, mock_get_edx_api_data): """ Test retrieval of course runs. """ catalog_course_runs = CourseRunFactory.create_batch(10) mock_get_edx_api_data.return_value = catalog_course_runs data = get_course_runs() self.assertTrue(mock_get_edx_api_data.called) self.assert_contract(mock_get_edx_api_data.call_args) self.assertEqual(data, catalog_course_runs)
def test_get_course_owners_by_course(self, mock_get_edx_api_data): """ Test retrieval of course runs. """ catalog_course_runs = CourseRunFactory.create_batch(10) catalog_course = CourseFactory(course_runs=catalog_course_runs) mock_get_edx_api_data.return_value = catalog_course data = get_owners_for_course(course_uuid=str(catalog_course['uuid'])) assert mock_get_edx_api_data.called assert data == catalog_course['owners']
def setUpClass(cls): super(TestProgramListing, cls).setUpClass() cls.course = ModuleStoreCourseFactory() course_run = CourseRunFactory(key=six.text_type(cls.course.id)) course = CourseFactory(course_runs=[course_run]) cls.first_program = ProgramFactory(courses=[course]) cls.second_program = ProgramFactory(courses=[course]) cls.data = sorted([cls.first_program, cls.second_program], key=cls.program_sort_key)
def setUpClass(cls): super().setUpClass() cls.course = ModuleStoreCourseFactory() course_run = CourseRunFactory(key=str(cls.course.id)) # lint-amnesty, pylint: disable=no-member course = CourseFactory(course_runs=[course_run]) cls.first_program = ProgramFactory(courses=[course]) cls.second_program = ProgramFactory(courses=[course]) cls.data = sorted([cls.first_program, cls.second_program], key=cls.program_sort_key)
def test_get_course_owners_by_course(self, mock_get_edx_api_data): """ Test retrieval of course runs. """ catalog_course_runs = CourseRunFactory.create_batch(10) catalog_course = CourseFactory(course_runs=catalog_course_runs) mock_get_edx_api_data.return_value = catalog_course data = get_owners_for_course(course_uuid=str(catalog_course['uuid'])) self.assertTrue(mock_get_edx_api_data.called) self.assertEqual(data, catalog_course['owners'])
def setUp(self): super(TestProgramDataExtender, self).setUp() self.course = ModuleStoreCourseFactory() self.course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1) self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1) self.course = self.update_course(self.course, self.user.id) self.course_run = CourseRunFactory(key=unicode(self.course.id)) self.catalog_course = CourseFactory(course_runs=[self.course_run]) self.program = ProgramFactory(courses=[self.catalog_course])
def test_program_uuid_args(self, mock_get_programs, mock_task): course_1_id = 'course-v1:edX+Test+1' course_2_id = 'course-v1:edX+Test+2' program = ProgramFactory( courses=[ CourseFactory( course_runs=[ CourseRunFactory(key=course_1_id), CourseRunFactory(key=course_2_id) ] ) ], curricula=[], ) self.expected_options['program_uuids'] = [program['uuid']] mock_get_programs.return_value = [program] call_command(Command(), '--program_uuids', program['uuid']) assert mock_task.called assert mock_task.call_args[0][0] == self.expected_options assert mock_task.call_args[0][1].sort() == [course_1_id, course_2_id].sort()
def setUp(self): super(RelatedProgramsTests, self).setUp() self.url = reverse('dashboard') self.create_programs_config() self.client.login(username=self.user.username, password=self.password) course_run = CourseRunFactory(key=unicode(self.course.id)) # pylint: disable=no-member course = CatalogCourseFactory(course_runs=[course_run]) self.programs = [ProgramFactory(courses=[course]) for __ in range(2)]
def test_multiple_published_course_runs(self): """ Learner should not be eligible for one click purchase if: - program has a course with more than one published course run """ course_run_1 = CourseRunFactory( key=str(ModuleStoreCourseFactory().id), status='published' ) course_run_2 = CourseRunFactory( key=str(ModuleStoreCourseFactory().id), status='published' ) course = CourseFactory(course_runs=[course_run_1, course_run_2]) program = ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory( key=str(ModuleStoreCourseFactory().id), status='published' ) ]), course, CourseFactory(course_runs=[ CourseRunFactory( key=str(ModuleStoreCourseFactory().id), status='published' ) ]) ], is_program_eligible_for_one_click_purchase=True, applicable_seat_types=['verified'] ) data = ProgramDataExtender(program, self.user).extend() self.assertFalse(data['is_learner_eligible_for_one_click_purchase']) course_run_2['status'] = 'unpublished' data = ProgramDataExtender(program, self.user).extend() self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
def test_shared_enrollment_engagement(self, mock_get_programs): """ Verify that correct programs are returned when the user is enrolled in a single course run appearing in multiple programs. """ shared_course_run_key, solo_course_run_key = ( generate_course_run_key() for __ in range(2)) batch = [ ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=shared_course_run_key), ]), ]) for __ in range(2) ] joint_programs = sorted(batch, key=lambda program: program['title']) data = joint_programs + [ ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=solo_course_run_key), ]), ]), ProgramFactory(), ] mock_get_programs.return_value = data # Enrollment for the shared course run created last (most recently). self._create_enrollments(solo_course_run_key, shared_course_run_key) meter = ProgramProgressMeter(self.user) self._attach_detail_url(data) programs = data[:3] self.assertEqual(meter.engaged_programs, programs) self._assert_progress( meter, *(ProgressFactory(uuid=program['uuid'], in_progress=self._extract_titles(program, 0)) for program in programs)) self.assertEqual(meter.completed_programs, [])
def setUp(self): super(TestSyncCourseRunsCommand, self).setUp() # create mongo course self.course = CourseFactory.create() # load this course into course overview CourseOverview.get_from_id(self.course.id) # create a catalog course run with the same course id. self.catalog_course_run = CourseRunFactory( key=unicode(self.course.id), marketing_url='test_marketing_url', eligible_for_financial_aid=False )
def setUpClass(cls): super(TestProgramDetails, cls).setUpClass() modulestore_course = ModuleStoreCourseFactory() course_run = CourseRunFactory(key=six.text_type(modulestore_course.id)) course = CourseFactory(course_runs=[course_run]) cls.program_data = ProgramFactory(uuid=cls.program_uuid, courses=[course]) cls.pathway_data = PathwayFactory() cls.program_data['pathway_ids'] = [cls.pathway_data['id']] cls.pathway_data['program_uuids'] = [cls.program_data['uuid']] del cls.pathway_data['programs']
def setUpClass(cls): super(TestProgramDetails, cls).setUpClass() modulestore_course = ModuleStoreCourseFactory() course_run = CourseRunFactory(key=unicode(modulestore_course.id)) # pylint: disable=no-member course = CourseFactory(course_runs=[course_run]) cls.program_data = ProgramFactory(uuid=cls.program_uuid, courses=[course]) cls.pathway_data = CreditPathwayFactory() cls.program_data['pathway_ids'] = [cls.pathway_data['id']] cls.pathway_data['program_uuids'] = [cls.program_data['uuid']] del cls.pathway_data['programs']
def setUpClass(cls): super().setUpClass() modulestore_course = ModuleStoreCourseFactory() course_run = CourseRunFactory(key=str(modulestore_course.id)) # lint-amnesty, pylint: disable=no-member course = CourseFactory(course_runs=[course_run]) cls.program_data = ProgramFactory(uuid=cls.program_uuid, courses=[course]) cls.pathway_data = PathwayFactory() cls.program_data['pathway_ids'] = [cls.pathway_data['id']] cls.pathway_data['program_uuids'] = [cls.program_data['uuid']] del cls.pathway_data['programs'] # lint-amnesty, pylint: disable=unsupported-delete-operation
def setUp(self): super(TestSyncCourseRunsCommand, self).setUp() # create mongo course self.course = CourseFactory.create() # load this course into course overview self.course_overview = CourseOverview.get_from_id(self.course.id) # create a catalog course run with the same course id. self.catalog_course_run = CourseRunFactory( key=unicode(self.course.id), marketing_url='test_marketing_url', eligible_for_financial_aid=False )
def test_unrelated_program_not_listed(self, mock_get_programs): """Verify that unrelated programs don't appear in the listing.""" nonexistent_course_run_id = generate_course_run_key() course_run = CourseRunFactory(key=nonexistent_course_run_id) course = CatalogCourseFactory(course_runs=[course_run]) unrelated_program = ProgramFactory(courses=[course]) mock_get_programs.return_value = self.programs + [unrelated_program] response = self.client.get(self.url) self.assert_related_programs(response) self.assertNotContains(response, unrelated_program['title'])
def test_get_course_run_details(self, mock_get_course_run_details): """ Test for Python API `get_course_run_details` function. """ course_run = CourseRunFactory() mock_get_course_run_details.return_value = { 'title': course_run['title'], } results = get_course_run_details(course_run['key'], ['title']) assert results['title'] == course_run['title']
def test_mutiple_program_engagement(self, mock_get_programs): """ Verify that correct programs are returned in the correct order when the user is enrolled in course runs appearing in programs. """ newer_course_run_key, older_course_run_key = (generate_course_run_key() for __ in range(2)) data = [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=newer_course_run_key), ]), ] ), ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=older_course_run_key), ]), ] ), ProgramFactory(), ] mock_get_programs.return_value = data # The creation time of the enrollments matters to the test. We want # the first_course_run_key to represent the newest enrollment. self._create_enrollments(older_course_run_key, newer_course_run_key) meter = ProgramProgressMeter(self.user) self._attach_detail_url(data) programs = data[:2] self.assertEqual(meter.engaged_programs, programs) self._assert_progress( meter, *(ProgressFactory(uuid=program['uuid'], in_progress=1) for program in programs) ) self.assertEqual(meter.completed_programs, [])
def test_get_course_run_details(self, mock_get_edx_api_data): """ Test retrieval of details about a specific course run """ course_run = CourseRunFactory() course_run_details = { 'content_language': course_run['content_language'], 'weeks_to_complete': course_run['weeks_to_complete'], 'max_effort': course_run['max_effort'] } mock_get_edx_api_data.return_value = course_run_details data = get_course_run_details(course_run['key'], ['content_language', 'weeks_to_complete', 'max_effort']) self.assertTrue(mock_get_edx_api_data.called) self.assertEqual(data, course_run_details)
def test_course_pricing_when_all_course_runs_have_no_seats(self): # Create three seatless course runs and add them to the program course_runs = [] for __ in range(3): course = ModuleStoreCourseFactory() course = self.update_course(course, self.user.id) course_runs.append(CourseRunFactory(key=unicode(course.id), seats=[])) program = ProgramFactory(courses=[CourseFactory(course_runs=course_runs)]) data = ProgramMarketingDataExtender(program, self.user).extend() self.assertEqual(data['number_of_courses'], len(program['courses'])) self.assertEqual(data['full_program_price'], 0.0) self.assertEqual(data['avg_price_per_course'], 0.0)
def test_get_pseudo_course_overview(self, mock_get_course_run_details): """ Test for the `get_pseudo_course_overview` function that creates a temporary course overview for courses that have been deleted. """ course_run = CourseRunFactory() mock_get_course_run_details.return_value = { 'title': course_run['title'], } course_key = CourseKey.from_string(course_run['key']) result = get_pseudo_course_overview(course_key) assert result.display_name == course_run['title'] assert result.display_org_with_default == course_key.org assert result.certificates_show_before_end
def test_course_overview_does_not_exist(self, mock_log_info, mock_catalog_course_runs): """ Verify no error in case if a course run is not found in course overview. """ nonexistent_course_run = CourseRunFactory() mock_catalog_course_runs.return_value = [self.catalog_course_run, nonexistent_course_run] call_command('sync_course_runs') mock_log_info.assert_any_call( '[sync_course_runs] course overview record not found for course run: %s', nonexistent_course_run['key'], ) updated_marketing_url = CourseOverview.objects.get(id=self.course.id).marketing_url self.assertEqual(updated_marketing_url, 'test_marketing_url')
def test_credit_course_counted_complete_for_verified(self, mock_completed_course_runs, mock_get_programs): """ Verify that 'credit' course certificate type are treated as if they were "verified" when checking for course completion status. """ course_run_key = generate_course_run_key() course = CourseFactory(course_runs=[ CourseRunFactory(key=course_run_key, type='credit'), ]) program = ProgramFactory(courses=[course]) mock_get_programs.return_value = [program] self._create_enrollments(course_run_key) meter = ProgramProgressMeter(self.user) mock_completed_course_runs.return_value = [{'course_run_id': course_run_key, 'type': 'verified'}] self.assertEqual(meter._is_course_complete(course), True)
def _create_course(self, course_price): """ Creates the course in mongo and update it with the instructor data. Also creates catalog course with respect to course run. Returns: Catalog course dict. """ course = ModuleStoreCourseFactory() course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1) course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1) course.instructor_info = self.instructors course = self.update_course(course, self.user.id) course_run = CourseRunFactory(key=unicode(course.id), seats=[SeatFactory(price=course_price)]) return CourseFactory(course_runs=[course_run])
def test_unpublished_sessions_for_entitlement(self, mock_get_edx_api_data): """ Test unpublished course runs are not part of visible session entitlements when the user is not enrolled. """ catalog_course_run = CourseRunFactory.create(status=COURSE_UNPUBLISHED) catalog_course = CourseFactory(course_runs=[catalog_course_run]) mock_get_edx_api_data.return_value = catalog_course course_key = CourseKey.from_string(catalog_course_run.get('key')) course_overview = CourseOverviewFactory.create(id=course_key, start=self.tomorrow) CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, min_price=100, course_id=course_overview.id) entitlement = CourseEntitlementFactory( user=self.user, mode=CourseMode.VERIFIED ) session_entitlements = get_visible_sessions_for_entitlement(entitlement) self.assertEqual(session_entitlements, [])
def test_starting_and_ending_logs(self, mock_log_info, mock_catalog_course_runs): """ Verify logging at start and end of the command. """ mock_catalog_course_runs.return_value = [self.catalog_course_run, CourseRunFactory(), CourseRunFactory()] call_command('sync_course_runs') # Assert the logs at the start of the command. mock_log_info.assert_any_call('[sync_course_runs] Fetching course runs from catalog service.') # Assert the log metrics at it's completion. mock_log_info.assert_any_call( ('[sync_course_runs] course runs retrieved: %d, course runs found in course overview: %d,' ' course runs not found in course overview: %d, course overviews metadata updated: %d,'), 3, 1, 2, 1, )
def test_handle_enqueue_failure(self, mock_log, mock_task, mock_get_programs): """Verify that failure to enqueue a task doesn't halt execution.""" def side_effect(username): """Simulate failure to enqueue a task.""" if username == self.alice.username: raise Exception mock_task.side_effect = side_effect data = [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=self.course_run_key), ]), ] ), ] mock_get_programs.return_value = data GeneratedCertificateFactory( user=self.alice, course_id=self.course_run_key, mode=MODES.verified, status=CertificateStatuses.downloadable, ) GeneratedCertificateFactory( user=self.bob, course_id=self.course_run_key, mode=MODES.verified, status=CertificateStatuses.downloadable, ) call_command('backpopulate_program_credentials', commit=True) self.assertTrue(mock_log.called) calls = [ mock.call(self.alice.username), mock.call(self.bob.username) ] mock_task.assert_has_calls(calls, any_order=True)
def test_get_visible_sessions_for_entitlement(self, mock_get_edx_api_data): """ Test retrieval of visible session entitlements. """ catalog_course_run = CourseRunFactory.create() catalog_course = CourseFactory(course_runs=[catalog_course_run]) mock_get_edx_api_data.return_value = catalog_course course_key = CourseKey.from_string(catalog_course_run.get('key')) course_overview = CourseOverviewFactory.create(id=course_key, start=self.tomorrow) CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, min_price=100, course_id=course_overview.id) course_enrollment = CourseEnrollmentFactory( user=self.user, course_id=unicode(course_overview.id), mode=CourseMode.VERIFIED ) entitlement = CourseEntitlementFactory( user=self.user, enrollment_course_run=course_enrollment, mode=CourseMode.VERIFIED ) session_entitlements = get_visible_sessions_for_entitlement(entitlement) self.assertEqual(session_entitlements, [catalog_course_run])
def test_in_progress_course_upgrade_deadline_check(self, offset, mock_get_programs): """ Verify that if the user's enrollment is not of the same type as the course run, the course will only count as in progress if there is another available seat with the right type for which the upgrade deadline has not passed. """ course_run_key = generate_course_run_key() now = datetime.datetime.now(utc) upgrade_deadline = None if not offset else str(now + datetime.timedelta( days=offset)) required_seat = SeatFactory(type='verified', upgrade_deadline=upgrade_deadline) enrolled_seat = SeatFactory(type='audit') seats = [required_seat, enrolled_seat] data = [ ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory( key=course_run_key, type='verified', seats=seats), ]), ]) ] mock_get_programs.return_value = data CourseEnrollmentFactory(user=self.user, course_id=course_run_key, mode='audit') meter = ProgramProgressMeter(self.site, self.user) program = data[0] expected = [ ProgressFactory(uuid=program['uuid'], completed=0, in_progress=1 if offset in [None, 1] else 0, not_started=1 if offset in [-1] else 0) ] self.assertEqual(meter.progress(count_only=True), expected)
def _get_programs_data(self, hierarchy_type): """ Generate a mock response for get_programs() with the given type of course hierarchy. Dramatically simplifies (and makes consistent between test runs) the ddt-generated test_flatten methods. """ if hierarchy_type == self.SEPARATE_PROGRAMS: return [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=self.course_run_key), ]), ] ), ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=self.alternate_course_run_key), ]), ] ), ] elif hierarchy_type == self.SEPARATE_COURSES: return [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=self.course_run_key), ]), CourseFactory(course_runs=[ CourseRunFactory(key=self.alternate_course_run_key), ]), ] ), ] else: # SAME_COURSE return [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=self.course_run_key), CourseRunFactory(key=self.alternate_course_run_key), ]), ] ), ]
def setUpClass(cls): """ Set up test data """ super().setUpClass() catalog_org = CatalogOrganizationFactory.create( key=cls.organization_key) cls.program = ProgramFactory.create( uuid=cls.program_uuid, authoring_organizations=[catalog_org]) organization = OrganizationFactory.create( short_name=cls.organization_key) SAMLProviderConfigFactory.create(organization=organization) catalog_course_id_str = 'course-v1:edX+ToyX' course_run_id_str = f'{catalog_course_id_str}+Toy_Course' cls.course_id = CourseKey.from_string(course_run_id_str) CourseOverviewFactory(id=cls.course_id) course_run = CourseRunFactory(key=course_run_id_str) cls.course = CourseFactory(key=catalog_course_id_str, course_runs=[course_run]) cls.student_1 = UserFactory(username='******') cls.student_2 = UserFactory(username='******')
def setUp(self): super(TestProgramDataExtender, self).setUp() self.user = UserFactory() self.client.login(username=self.user.username, password=self.password) self.course = ModuleStoreCourseFactory() self.course.start = datetime.datetime.now(utc) - datetime.timedelta( days=1) self.course.end = datetime.datetime.now(utc) + datetime.timedelta( days=1) self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member organization = OrganizationFactory() course_run = CourseRunFactory(key=unicode(self.course.id)) # pylint: disable=no-member course = CourseFactory(course_runs=[course_run]) program = ProgramFactory(authoring_organizations=[organization], courses=[course]) self.program = munge_catalog_program(program) self.course_code = self.program['course_codes'][0] self.run_mode = self.course_code['run_modes'][0]
def test_program_completion_with_no_id_professional(self, mock_get_certificates_for_user, mock_get_programs): """ Verify that 'no-id-professional' certificates are treated as if they were 'professional' certificates when determining program completion. """ # Create serialized course runs like the ones we expect to receive from # the discovery service's API. These runs are of type 'professional'. course_runs = CourseRunFactory.create_batch(2, type='professional') program = ProgramFactory(courses=[CourseFactory(course_runs=course_runs)]) mock_get_programs.return_value = [program] # Verify that the test program is not complete. meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, []) # Grant a 'no-id-professional' certificate for one of the course runs, # thereby completing the program. mock_get_certificates_for_user.return_value = [ self._make_certificate_result(status='downloadable', type='no-id-professional', course_key=course_runs[0]['key']) ] # Verify that the program is complete. meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, [program['uuid']])
class TestSyncCourseRunsCommand(ModuleStoreTestCase): """ Test for the sync course runs management command. """ def setUp(self): super(TestSyncCourseRunsCommand, self).setUp() # create mongo course self.course = CourseFactory.create() # load this course into course overview self.course_overview = CourseOverview.get_from_id(self.course.id) # create a catalog course run with the same course id. self.catalog_course_run = CourseRunFactory( key=unicode(self.course.id), marketing_url='test_marketing_url', eligible_for_financial_aid=False ) def test_course_run_sync(self, mock_catalog_course_runs): """ Verify on executing management command course overview data is updated with course run data from course discovery. """ mock_catalog_course_runs.return_value = [self.catalog_course_run] call_command('sync_course_runs') updated_course_overview = CourseOverview.objects.get(id=self.course.id) # assert fields have updated for field in sync_command.course_run_fields: course_overview_field_name = field.course_overview_name catalog_field_name = field.catalog_name previous_course_overview_value = getattr(self.course_overview, course_overview_field_name), updated_course_overview_value = getattr(updated_course_overview, course_overview_field_name) # course overview value matches catalog value self.assertEqual( updated_course_overview_value, self.catalog_course_run.get(catalog_field_name), ) # new value doesn't match old value self.assertNotEqual( updated_course_overview_value, previous_course_overview_value, ) @mock.patch(COMMAND_MODULE + '.log.info') def test_course_overview_does_not_exist(self, mock_log_info, mock_catalog_course_runs): """ Verify no error in case if a course run is not found in course overview. """ nonexistent_course_run = CourseRunFactory() mock_catalog_course_runs.return_value = [self.catalog_course_run, nonexistent_course_run] call_command('sync_course_runs') mock_log_info.assert_any_call( '[sync_course_runs] course overview record not found for course run: %s', nonexistent_course_run['key'], ) updated_marketing_url = CourseOverview.objects.get(id=self.course.id).marketing_url self.assertEqual(updated_marketing_url, 'test_marketing_url') @mock.patch(COMMAND_MODULE + '.log.info') def test_starting_and_ending_logs(self, mock_log_info, mock_catalog_course_runs): """ Verify logging at start and end of the command. """ def _assert_logs(num_updates): mock_log_info.assert_any_call('[sync_course_runs] Fetching course runs from catalog service.') mock_log_info.assert_any_call( '[sync_course_runs] course runs found in catalog: %d, course runs found in course overview: %d,' ' course runs not found in course overview: %d, course overviews updated: %d', 3, 1, 2, num_updates, ) mock_log_info.reset_mock() mock_catalog_course_runs.return_value = [self.catalog_course_run, CourseRunFactory(), CourseRunFactory()] call_command('sync_course_runs') _assert_logs(num_updates=1) call_command('sync_course_runs') _assert_logs(num_updates=0)
class TestProgramDataExtender(ModuleStoreTestCase): """Tests of the program data extender utility class.""" maxDiff = None sku = 'abc123' checkout_path = '/basket' instructors = { 'instructors': [ { 'name': 'test-instructor1', 'organization': 'TextX', }, { 'name': 'test-instructor2', 'organization': 'TextX', } ] } def setUp(self): super(TestProgramDataExtender, self).setUp() self.course = ModuleStoreCourseFactory() self.course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1) self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1) self.course = self.update_course(self.course, self.user.id) self.course_run = CourseRunFactory(key=unicode(self.course.id)) self.catalog_course = CourseFactory(course_runs=[self.course_run]) self.program = ProgramFactory(courses=[self.catalog_course]) self.course_price = 100 def _assert_supplemented(self, actual, **kwargs): """DRY helper used to verify that program data is extended correctly.""" self.course_run.update( dict( { 'certificate_url': None, 'course_url': reverse('course_root', args=[self.course.id]), 'enrollment_open_date': strftime_localized(DEFAULT_ENROLLMENT_START_DATE, 'SHORT_DATE'), 'is_course_ended': self.course.end < datetime.datetime.now(utc), 'is_enrolled': False, 'is_enrollment_open': True, 'upgrade_url': None, 'advertised_start': None, }, **kwargs ) ) self.catalog_course['course_runs'] = [self.course_run] self.program['courses'] = [self.catalog_course] self.assertEqual(actual, self.program) @ddt.data(-1, 0, 1) def test_is_enrollment_open(self, days_offset): """ Verify that changes to the course run end date do not affect our assessment of the course run being open for enrollment. """ self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=days_offset) self.course = self.update_course(self.course, self.user.id) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented(data) @ddt.data( (False, None, False), (True, MODES.audit, True), (True, MODES.verified, False), ) @ddt.unpack @mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course') def test_student_enrollment_status(self, is_enrolled, enrolled_mode, is_upgrade_required, mock_get_mode): """Verify that program data is supplemented with the student's enrollment status.""" expected_upgrade_url = '{root}/{path}?sku={sku}'.format( root=ECOMMERCE_URL_ROOT, path=self.checkout_path.strip('/'), sku=self.sku, ) update_commerce_config(enabled=True, checkout_page=self.checkout_path) mock_mode = mock.Mock() mock_mode.sku = self.sku mock_get_mode.return_value = mock_mode if is_enrolled: CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=enrolled_mode) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented( data, is_enrolled=is_enrolled, upgrade_url=expected_upgrade_url if is_upgrade_required else None ) @ddt.data(MODES.audit, MODES.verified) def test_inactive_enrollment_no_upgrade(self, enrolled_mode): """ Verify that a student with an inactive enrollment isn't encouraged to upgrade. """ update_commerce_config(enabled=True, checkout_page=self.checkout_path) CourseEnrollmentFactory( user=self.user, course_id=self.course.id, mode=enrolled_mode, is_active=False, ) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented(data) @mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course') def test_ecommerce_disabled(self, mock_get_mode): """ Verify that the utility can operate when the ecommerce service is disabled. """ update_commerce_config(enabled=False, checkout_page=self.checkout_path) mock_mode = mock.Mock() mock_mode.sku = self.sku mock_get_mode.return_value = mock_mode CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=MODES.audit) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented(data, is_enrolled=True, upgrade_url=None) @ddt.data( (1, 1, False), (1, -1, True), ) @ddt.unpack def test_course_run_enrollment_status(self, start_offset, end_offset, is_enrollment_open): """ Verify that course run enrollment status is reflected correctly. """ self.course.enrollment_start = datetime.datetime.now(utc) - datetime.timedelta(days=start_offset) self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=end_offset) self.course = self.update_course(self.course, self.user.id) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented( data, is_enrollment_open=is_enrollment_open, enrollment_open_date=strftime_localized(self.course.enrollment_start, 'SHORT_DATE'), ) def test_no_enrollment_start_date(self): """ Verify that a closed course run with no explicit enrollment start date doesn't cause an error. Regression test for ECOM-4973. """ self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=1) self.course = self.update_course(self.course, self.user.id) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented( data, is_enrollment_open=False, ) @ddt.data(True, False) @mock.patch(UTILS_MODULE + '.certificate_api.certificate_downloadable_status') @mock.patch(CERTIFICATES_API_MODULE + '.has_html_certificates_enabled') def test_certificate_url_retrieval(self, is_uuid_available, mock_html_certs_enabled, mock_get_cert_data): """ Verify that the student's run mode certificate is included, when available. """ test_uuid = uuid.uuid4().hex mock_get_cert_data.return_value = {'uuid': test_uuid} if is_uuid_available else {} mock_html_certs_enabled.return_value = True data = ProgramDataExtender(self.program, self.user).extend() expected_url = reverse( 'certificates:render_cert_by_uuid', kwargs={'certificate_uuid': test_uuid} ) if is_uuid_available else None self._assert_supplemented(data, certificate_url=expected_url) @ddt.data(True, False) def test_may_certify_attached(self, may_certify): """ Verify that the `may_certify` is included during data extension. """ self.course.certificates_show_before_end = may_certify self.course = self.update_course(self.course, self.user.id) data = ProgramDataExtender(self.program, self.user).extend() self.assertEqual(may_certify, data['courses'][0]['course_runs'][0]['may_certify']) self._assert_supplemented(data) def test_learner_eligibility_for_one_click_purchase(self): """ Learner should be eligible for one click purchase if: - program is eligible for one click purchase - There are courses remaining that have not been purchased and enrolled in. """ data = ProgramDataExtender(self.program, self.user).extend() self.assertFalse(data['is_learner_eligible_for_one_click_purchase']) courses = [_create_course(self, self.course_price)] program = ProgramFactory( courses=courses, is_program_eligible_for_one_click_purchase=False ) data = ProgramDataExtender(program, self.user).extend() self.assertFalse(data['is_learner_eligible_for_one_click_purchase']) course1 = _create_course(self, self.course_price) course2 = _create_course(self, self.course_price) CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode='verified') CourseEnrollmentFactory(user=self.user, course_id=course2['course_runs'][0]['key'], mode='audit') program2 = ProgramFactory( courses=[course1, course2], is_program_eligible_for_one_click_purchase=True, applicable_seat_types=['verified'], ) data = ProgramDataExtender(program2, self.user).extend() self.assertTrue(data['is_learner_eligible_for_one_click_purchase']) def test_learner_eligibility_for_one_click_purchase_with_unpublished(self): """ Learner should be eligible for one click purchase if: - program is eligible for one click purchase - There are courses remaining that have not been purchased and enrolled in. """ course1 = _create_course(self, self.course_price, course_run_count=2) course2 = _create_course(self, self.course_price) CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode='verified') course1['course_runs'][0]['status'] = 'unpublished' program2 = ProgramFactory( courses=[course1, course2], is_program_eligible_for_one_click_purchase=True, applicable_seat_types=['verified'], ) data = ProgramDataExtender(program2, self.user).extend() self.assertEqual(len(data['skus']), 1) self.assertTrue(data['is_learner_eligible_for_one_click_purchase']) def test_learner_eligibility_for_one_click_purchase_professional_no_id(self): """ Learner should not be eligible for one click purchase if: - There are no courses remaining that have not been purchased and enrolled in. This test is primarily for the case of no-id-professional enrollment modes """ course1 = _create_course(self, self.course_price) CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode='no-id-professional') program2 = ProgramFactory( courses=[course1], is_program_eligible_for_one_click_purchase=True, applicable_seat_types=['professional'], # There is no seat type for no-id-professional, it # instead uses professional ) data = ProgramDataExtender(program2, self.user).extend() self.assertFalse(data['is_learner_eligible_for_one_click_purchase']) def test_multiple_published_course_runs(self): """ Learner should not be eligible for one click purchase if: - program has a course with more than one published course run """ course_run_1 = CourseRunFactory( key=str(ModuleStoreCourseFactory().id), status='published' ) course_run_2 = CourseRunFactory( key=str(ModuleStoreCourseFactory().id), status='published' ) course = CourseFactory(course_runs=[course_run_1, course_run_2]) program = ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory( key=str(ModuleStoreCourseFactory().id), status='published' ) ]), course, CourseFactory(course_runs=[ CourseRunFactory( key=str(ModuleStoreCourseFactory().id), status='published' ) ]) ], is_program_eligible_for_one_click_purchase=True, applicable_seat_types=['verified'] ) data = ProgramDataExtender(program, self.user).extend() self.assertFalse(data['is_learner_eligible_for_one_click_purchase']) course_run_2['status'] = 'unpublished' data = ProgramDataExtender(program, self.user).extend() self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
class TestProgramDataExtender(ModuleStoreTestCase): """Tests of the program data extender utility class.""" maxDiff = None sku = 'abc123' checkout_path = '/basket' def setUp(self): super(TestProgramDataExtender, self).setUp() self.course = ModuleStoreCourseFactory() self.course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1) self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1) self.course = self.update_course(self.course, self.user.id) self.course_run = CourseRunFactory(key=unicode(self.course.id)) self.catalog_course = CourseFactory(course_runs=[self.course_run]) self.program = ProgramFactory(courses=[self.catalog_course]) def _assert_supplemented(self, actual, **kwargs): """DRY helper used to verify that program data is extended correctly.""" self.course_run.update( dict( { 'certificate_url': None, 'course_url': reverse('course_root', args=[self.course.id]), 'enrollment_open_date': strftime_localized(DEFAULT_ENROLLMENT_START_DATE, 'SHORT_DATE'), 'is_course_ended': self.course.end < datetime.datetime.now(utc), 'is_enrolled': False, 'is_enrollment_open': True, 'upgrade_url': None, 'advertised_start': None, }, **kwargs ) ) self.catalog_course['course_runs'] = [self.course_run] self.program['courses'] = [self.catalog_course] self.assertEqual(actual, self.program) @ddt.data(-1, 0, 1) def test_is_enrollment_open(self, days_offset): """ Verify that changes to the course run end date do not affect our assessment of the course run being open for enrollment. """ self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=days_offset) self.course = self.update_course(self.course, self.user.id) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented(data) @ddt.data( (False, None, False), (True, MODES.audit, True), (True, MODES.verified, False), ) @ddt.unpack @mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course') def test_student_enrollment_status(self, is_enrolled, enrolled_mode, is_upgrade_required, mock_get_mode): """Verify that program data is supplemented with the student's enrollment status.""" expected_upgrade_url = '{root}/{path}?sku={sku}'.format( root=ECOMMERCE_URL_ROOT, path=self.checkout_path.strip('/'), sku=self.sku, ) update_commerce_config(enabled=True, checkout_page=self.checkout_path) mock_mode = mock.Mock() mock_mode.sku = self.sku mock_get_mode.return_value = mock_mode if is_enrolled: CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=enrolled_mode) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented( data, is_enrolled=is_enrolled, upgrade_url=expected_upgrade_url if is_upgrade_required else None ) @ddt.data(MODES.audit, MODES.verified) def test_inactive_enrollment_no_upgrade(self, enrolled_mode): """ Verify that a student with an inactive enrollment isn't encouraged to upgrade. """ update_commerce_config(enabled=True, checkout_page=self.checkout_path) CourseEnrollmentFactory( user=self.user, course_id=self.course.id, mode=enrolled_mode, is_active=False, ) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented(data) @mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course') def test_ecommerce_disabled(self, mock_get_mode): """ Verify that the utility can operate when the ecommerce service is disabled. """ update_commerce_config(enabled=False, checkout_page=self.checkout_path) mock_mode = mock.Mock() mock_mode.sku = self.sku mock_get_mode.return_value = mock_mode CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=MODES.audit) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented(data, is_enrolled=True, upgrade_url=None) @ddt.data( (1, 1, False), (1, -1, True), ) @ddt.unpack def test_course_run_enrollment_status(self, start_offset, end_offset, is_enrollment_open): """ Verify that course run enrollment status is reflected correctly. """ self.course.enrollment_start = datetime.datetime.now(utc) - datetime.timedelta(days=start_offset) self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=end_offset) self.course = self.update_course(self.course, self.user.id) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented( data, is_enrollment_open=is_enrollment_open, enrollment_open_date=strftime_localized(self.course.enrollment_start, 'SHORT_DATE'), ) def test_no_enrollment_start_date(self): """ Verify that a closed course run with no explicit enrollment start date doesn't cause an error. Regression test for ECOM-4973. """ self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=1) self.course = self.update_course(self.course, self.user.id) data = ProgramDataExtender(self.program, self.user).extend() self._assert_supplemented( data, is_enrollment_open=False, ) @ddt.data(True, False) @mock.patch(UTILS_MODULE + '.certificate_api.certificate_downloadable_status') @mock.patch(CERTIFICATES_API_MODULE + '.has_html_certificates_enabled') def test_certificate_url_retrieval(self, is_uuid_available, mock_html_certs_enabled, mock_get_cert_data): """ Verify that the student's run mode certificate is included, when available. """ test_uuid = uuid.uuid4().hex mock_get_cert_data.return_value = {'uuid': test_uuid} if is_uuid_available else {} mock_html_certs_enabled.return_value = True data = ProgramDataExtender(self.program, self.user).extend() expected_url = reverse( 'certificates:render_cert_by_uuid', kwargs={'certificate_uuid': test_uuid} ) if is_uuid_available else None self._assert_supplemented(data, certificate_url=expected_url)