def test_unfulfilled_entitlement(self, mock_course_overview, mock_pseudo_session, mock_course_runs, mock_get_programs): """ When a learner has an unfulfilled entitlement, their course dashboard should have: - a hidden 'View Course' button - the text 'In order to view the course you must select a session:' - an unhidden course-entitlement-selection-container - a related programs message """ program = ProgramFactory() CourseEntitlementFactory.create( user=self.user, course_uuid=program['courses'][0]['uuid']) mock_get_programs.return_value = [program] mock_course_overview.return_value = CourseOverviewFactory.create( start=self.TOMORROW) mock_course_runs.return_value = [{ 'key': 'course-v1:FAKE+FA1-MA1.X+3T2017', 'enrollment_end': str(self.TOMORROW), 'pacing_type': 'instructor_paced', 'type': 'verified', 'status': 'published' }] mock_pseudo_session.return_value = { 'key': 'course-v1:FAKE+FA1-MA1.X+3T2017', 'type': 'verified' } response = self.client.get(self.path) self.assertIn('class="enter-course hidden"', response.content) self.assertIn('You must select a session to access the course.', response.content) self.assertIn('<div class="course-entitlement-selection-container ">', response.content) self.assertIn('Related Programs:', response.content) # If an entitlement has already been redeemed by the user for a course run, do not let the run be selectable enrollment = CourseEnrollmentFactory( user=self.user, course_id=unicode(mock_course_overview.return_value.id), mode=CourseMode.VERIFIED) CourseEntitlementFactory.create( user=self.user, course_uuid=program['courses'][0]['uuid'], enrollment_course_run=enrollment) mock_course_runs.return_value = [{ 'key': 'course-v1:edX+toy+2012_Fall', 'enrollment_end': str(self.TOMORROW), 'pacing_type': 'instructor_paced', 'type': 'verified', 'status': 'published' }] response = self.client.get(self.path) # There should be two entitlements on the course page, one prompting for a mandatory session, but no # select option for the courses as there is only the single course run which has already been redeemed self.assertEqual(response.content.count('<li class="course-item">'), 2) self.assertIn('You must select a session to access the course.', response.content) self.assertNotIn('To access the course, select a session.', response.content)
def _create_cached_program(self): """ helper method to create a cached program """ program = ProgramFactory.create() for course_key in self.course_keys: program['courses'].append(CourseFactory(id=course_key)) program['type'] = 'MicroBachelors' program['type_attrs']['coaching_supported'] = True for course in program['courses']: cache.set( CATALOG_COURSE_PROGRAMS_CACHE_KEY_TPL.format(course_uuid=course['uuid']), [program['uuid']], None ) course_run = course['course_runs'][0]['key'] cache.set( COURSE_PROGRAMS_CACHE_KEY_TPL.format(course_run_id=course_run), [program['uuid']], None ) cache.set( PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']), program, None ) return program
def test_fulfilled_entitlement(self, mock_course_key, mock_course_overview, mock_course_runs, mock_get_programs): """ When a learner has a fulfilled entitlement, their course dashboard should have: - exactly one course item, meaning it: - has an entitlement card - does NOT have a course card referencing the selected session - an unhidden Change or Leave Session button - a related programs message """ mocked_course_overview = CourseOverviewFactory( start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW ) mock_course_overview.return_value = mocked_course_overview mock_course_key.return_value = mocked_course_overview.id course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=unicode(mocked_course_overview.id)) mock_course_runs.return_value = [ { 'key': str(mocked_course_overview.id), 'enrollment_end': str(mocked_course_overview.enrollment_end), 'pacing_type': 'self_paced', 'type': 'verified', 'status': 'published' } ] entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment) program = ProgramFactory() program['courses'][0]['course_runs'] = [{'key': unicode(mocked_course_overview.id)}] program['courses'][0]['uuid'] = entitlement.course_uuid mock_get_programs.return_value = [program] response = self.client.get(self.path) self.assertEqual(response.content.count('<li class="course-item">'), 1) self.assertIn('<button class="change-session btn-link "', response.content) self.assertIn('Related Programs:', response.content)
def test_fulfilled_expired_entitlement(self, mock_course_key, mock_course_overview, mock_course_runs, mock_get_programs): """ When a learner has a fulfilled entitlement that is expired, their course dashboard should have: - exactly one course item, meaning it: - has an entitlement card - Message that the learner can no longer change sessions - a related programs message """ mocked_course_overview = CourseOverviewFactory( start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW ) mock_course_overview.return_value = mocked_course_overview mock_course_key.return_value = mocked_course_overview.id course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=unicode(mocked_course_overview.id), created=self.THREE_YEARS_AGO) mock_course_runs.return_value = [ { 'key': str(mocked_course_overview.id), 'enrollment_end': str(mocked_course_overview.enrollment_end), 'pacing_type': 'self_paced', 'type': 'verified', 'status': 'published' } ] entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment, created=self.THREE_YEARS_AGO) program = ProgramFactory() program['courses'][0]['course_runs'] = [{'key': unicode(mocked_course_overview.id)}] program['courses'][0]['uuid'] = entitlement.course_uuid mock_get_programs.return_value = [program] response = self.client.get(self.path) self.assertEqual(response.content.count('<li class="course-item">'), 1) self.assertIn('You can no longer change sessions.', response.content) self.assertIn('Related Programs:', response.content)
def test_handle_no_course_overview(self, mock_task, mock_get_programs): """ Verify that the task is not enqueued for a user whose only certificate is for a course with no CourseOverview. """ data = [ ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=self.course_run_key), CourseRunFactory( key=self.course_run_key_no_course_overview), ]), ]), ] 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_no_course_overview, mode=MODES.verified, status=CertificateStatuses.downloadable, ) call_command('backpopulate_program_credentials', commit=True) mock_task.assert_called_once_with(self.alice.username) mock_task.assert_not_called(self.bob.username)
def test_handle(self, commit, mock_task, mock_get_programs): """ Verify that relevant tasks are only enqueued when the commit option is passed. """ 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.alternate_course_run_key, mode=MODES.verified, status=CertificateStatuses.downloadable, ) call_command('backpopulate_program_credentials', commit=commit) if commit: mock_task.assert_called_once_with(self.alice.username) else: mock_task.assert_not_called()
def test_handle_username_dedup(self, mock_task, mock_get_programs): """ Verify that only one task is enqueued for a user with multiple eligible course run certificates. """ data = [ ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=self.course_run_key), CourseRunFactory(key=self.alternate_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.alice, course_id=self.alternate_course_run_key, mode=MODES.verified, status=CertificateStatuses.downloadable, ) call_command('backpopulate_program_credentials', commit=True) mock_task.assert_called_once_with(self.alice.username)
def test_handle_mode_slugs(self, mock_task, mock_get_programs): """ Verify that course run types are taken into account when identifying qualifying course run certificates. """ data = [ ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=self.course_run_key, type='honor'), ]), ]), ] mock_get_programs.return_value = data GeneratedCertificateFactory( user=self.alice, course_id=self.course_run_key, mode=MODES.honor, 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) mock_task.assert_called_once_with(self.alice.username)
def test_get_many_with_missing(self, mock_cache, mock_warning, mock_info): programs = ProgramFactory.create_batch(3) all_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs } partial_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2] } def fake_get_many(keys): if len(keys) == 1: return {PROGRAM_CACHE_KEY_TPL.format(uuid=programs[-1]['uuid']): programs[-1]} else: return partial_programs mock_cache.get.return_value = [program['uuid'] for program in programs] mock_cache.get_many.side_effect = fake_get_many with with_site_configuration_context(domain=self.site.name, configuration={'COURSE_CATALOG_API_URL': 'foo'}): actual_programs = get_programs(site=self.site) # All 3 cached programs should be returned. An info message should be # logged about the one that was initially missing, but the code should # be able to stitch together all the details. assert {program['uuid'] for program in actual_programs} ==\ {program['uuid'] for program in all_programs.values()} assert not mock_warning.called mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.') for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) assert program == all_programs[key]
def test_handle_professional(self, mock_task, mock_get_programs): """ Verify the task can handle both professional and no-id-professional modes. """ mock_get_programs.return_value = [ ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=self.course_run_key, type='professional'), ]), ]), ] GeneratedCertificateFactory( user=self.alice, course_id=self.course_run_key, mode=CourseMode.PROFESSIONAL, status=CertificateStatuses.downloadable, ) GeneratedCertificateFactory( user=self.bob, course_id=self.course_run_key, mode=CourseMode.NO_ID_PROFESSIONAL_MODE, status=CertificateStatuses.downloadable, ) call_command('backpopulate_program_credentials', commit=True) # The task should be called for both users since professional and no-id-professional are equivalent. mock_task.assert_has_calls( [mock.call(self.alice.username), mock.call(self.bob.username)], any_order=True)
def create_program(self): """DRY helper for creating test program data.""" course_run = CourseRunFactory(key=self.course_id) course = CourseFactory(course_runs=[course_run]) program_type = ProgramTypeFactory() return ProgramFactory(courses=[course], type=program_type['name'])
def setUp(self): super(ProgramPageBase, self).setUp() self.set_programs_api_configuration(is_enabled=True) self.programs = ProgramFactory.create_batch(3) self.username = None
def test_completed_programs_no_id_professional(self, mock_completed_course_runs, mock_get_programs): """ Verify the method treats no-id-professional enrollments as professional enrollments. """ course_runs = CourseRunFactory.create_batch(2, type='no-id-professional') program = ProgramFactory( courses=[CourseFactory(course_runs=course_runs)]) mock_get_programs.return_value = [program] # Verify that no programs are complete. meter = ProgramProgressMeter(self.user) self.assertEqual(meter.completed_programs, []) # Complete all programs. for course_run in course_runs: CourseEnrollmentFactory(user=self.user, course_id=course_run['key'], mode='no-id-professional') mock_completed_course_runs.return_value = [{ 'course_run_id': course_run['key'], 'type': MODES.professional } for course_run in course_runs] # Verify that all programs are complete. meter = ProgramProgressMeter(self.user) self.assertEqual(meter.completed_programs, [program['uuid']])
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) data = ProgramMarketingDataExtender(program, self.user).extend() self.assertFalse(data['is_learner_eligible_for_one_click_purchase']) course_run_2['status'] = 'unpublished' data = ProgramMarketingDataExtender(program, self.user).extend() self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
def test_course_progress(self, mock_get_programs): """ Verify that the progress meter can represent progress in terms of serialized courses. """ course_run_key = generate_course_run_key() data = [ ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=course_run_key), ]), ]) ] mock_get_programs.return_value = data self._create_enrollments(course_run_key) meter = ProgramProgressMeter(self.site, self.user) program = data[0] expected = [ ProgressFactory(uuid=program['uuid'], completed=[], in_progress=[program['courses'][0]], not_started=[]) ] self.assertEqual(meter.progress(count_only=False), expected)
def setUp(self): super(TestCachePrograms, self).setUp() httpretty.httpretty.reset() self.catalog_integration = self.create_catalog_integration() self.site_domain = 'testsite.com' self.set_up_site( self.site_domain, { 'COURSE_CATALOG_API_URL': self.catalog_integration.get_internal_api_url().rstrip('/') } ) self.list_url = self.catalog_integration.get_internal_api_url().rstrip('/') + '/programs/' self.detail_tpl = self.list_url.rstrip('/') + '/{uuid}/' self.pathway_url = self.catalog_integration.get_internal_api_url().rstrip('/') + '/pathways/' self.programs = ProgramFactory.create_batch(3) self.pathways = PathwayFactory.create_batch(3) for pathway in self.pathways: self.programs += pathway['programs'] self.uuids = [program['uuid'] for program in self.programs] # add some of the previously created programs to some pathways self.pathways[0]['programs'].extend([self.programs[0], self.programs[1]]) self.pathways[1]['programs'].append(self.programs[0])
def test_get_many_with_missing(self, mock_cache, mock_warning, mock_info): programs = ProgramFactory.create_batch(3) all_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs } partial_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2] } def fake_get_many(keys): if len(keys) == 1: return {PROGRAM_CACHE_KEY_TPL.format(uuid=programs[-1]['uuid']): programs[-1]} else: return partial_programs mock_cache.get.return_value = [program['uuid'] for program in programs] mock_cache.get_many.side_effect = fake_get_many actual_programs = get_programs(self.site) # All 3 cached programs should be returned. An info message should be # logged about the one that was initially missing, but the code should # be able to stitch together all the details. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in all_programs.values()) ) self.assertFalse(mock_warning.called) mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.') for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, all_programs[key])
def _create_catalog_program(self, catalog_org): """ helper method to create a cached catalog program """ program = ProgramFactory.create( authoring_organizations=[catalog_org] ) cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']), program, None) return program
def test_completed_programs(self, mock_completed_course_runs, mock_get_programs): """Verify that completed programs are correctly identified.""" data = ProgramFactory.create_batch(3) mock_get_programs.return_value = data program_uuids = [] course_run_keys = [] for program in data: program_uuids.append(program['uuid']) for course in program['courses']: for course_run in course['course_runs']: course_run_keys.append(course_run['key']) # Verify that no programs are complete. meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, []) # Complete all programs. self._create_enrollments(*course_run_keys) mock_completed_course_runs.return_value = [ {'course_run_id': course_run_key, 'type': MODES.verified} for course_run_key in course_run_keys ] # Verify that all programs are complete. meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, program_uuids)
def setUp(self): super(ProgramPageBase, self).setUp() self.set_programs_api_configuration(is_enabled=True) self.programs = ProgramFactory.create_batch(3) self.username = None
def test_unfulfilled_entitlement(self, mock_course_overview, mock_pseudo_session, mock_course_runs, mock_get_programs): """ When a learner has an unfulfilled entitlement, their course dashboard should have: - a hidden 'View Course' button - the text 'In order to view the course you must select a session:' - an unhidden course-entitlement-selection-container - a related programs message """ program = ProgramFactory() CourseEntitlementFactory(user=self.user, course_uuid=program['courses'][0]['uuid']) mock_get_programs.return_value = [program] mock_course_overview.return_value = CourseOverviewFactory( start=self.TOMORROW) mock_course_runs.return_value = [{ 'key': 'course-v1:FAKE+FA1-MA1.X+3T2017', 'enrollment_end': str(self.TOMORROW), 'pacing_type': 'instructor_paced', 'type': 'verified' }] mock_pseudo_session.return_value = { 'key': 'course-v1:FAKE+FA1-MA1.X+3T2017', 'type': 'verified' } response = self.client.get(self.path) self.assertIn('class="enter-course hidden"', response.content) self.assertIn('You must select a session to access the course.', response.content) self.assertIn('<div class="course-entitlement-selection-container ">', response.content) self.assertIn('Related Programs:', response.content)
def setUpClass(cls): super().setUpClass() modulestore_course = ModuleStoreCourseFactory() course_run = CourseRunFactory(key=str(modulestore_course.id)) course = CourseFactory(course_runs=[course_run]) cls.program = ProgramFactory(uuid=cls.program_uuid, courses=[course])
def setUp(self): super(TestGetCertificates, self).setUp() self.user = UserFactory() self.program = ProgramFactory() self.course_certificate_url = 'fake-course-certificate-url' self.program_certificate_url = 'fake-program-certificate-url'
def test_get_many_with_missing(self, mock_cache, mock_warning, mock_info): programs = ProgramFactory.create_batch(3) all_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs } partial_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2] } def fake_get_many(keys): if len(keys) == 1: return {PROGRAM_CACHE_KEY_TPL.format(uuid=programs[-1]['uuid']): programs[-1]} else: return partial_programs mock_cache.get.return_value = [program['uuid'] for program in programs] mock_cache.get_many.side_effect = fake_get_many actual_programs = get_programs(site=self.site) # All 3 cached programs should be returned. An info message should be # logged about the one that was initially missing, but the code should # be able to stitch together all the details. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in all_programs.values()) ) self.assertFalse(mock_warning.called) mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.') for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, all_programs[key])
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']])
def test_completed_programs(self, mock_completed_course_runs, mock_get_programs): """Verify that completed programs are correctly identified.""" data = ProgramFactory.create_batch(3) mock_get_programs.return_value = data program_uuids = [] course_run_keys = [] for program in data: program_uuids.append(program['uuid']) for course in program['courses']: for course_run in course['course_runs']: course_run_keys.append(course_run['key']) # Verify that no programs are complete. meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, []) # Complete all programs. self._create_enrollments(*course_run_keys) mock_completed_course_runs.return_value = [{ 'course_run_id': course_run_key, 'type': MODES.verified } for course_run_key in course_run_keys] # Verify that all programs are complete. meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, program_uuids)
def test_empty_programs(self, mock_get_programs): """Verify that programs with no courses do not count as completed.""" program = ProgramFactory() program['courses'] = [] meter = ProgramProgressMeter(self.site, self.user) program_complete = meter._is_program_complete(program) self.assertFalse(program_complete)
def test_no_id_professional_in_progress(self, mock_get_programs): """ Verify that the progress meter treats no-id-professional enrollments as professional. """ course_run_key = generate_course_run_key() data = [ ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=course_run_key, type=CourseMode.PROFESSIONAL), ]), ]) ] mock_get_programs.return_value = data CourseEnrollmentFactory(user=self.user, course_id=course_run_key, mode=CourseMode.NO_ID_PROFESSIONAL_MODE) meter = ProgramProgressMeter(self.site, self.user) program = data[0] expected = [ ProgressFactory(uuid=program['uuid'], completed=[], in_progress=[program['courses'][0]], not_started=[]) ] self.assertEqual(meter.progress(count_only=False), 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 test_get_one_program(self, mock_get_edx_api_data): program = ProgramFactory() mock_get_edx_api_data.return_value = program data = get_programs(uuid=self.uuid) self.assert_contract(mock_get_edx_api_data.call_args, program_uuid=self.uuid) self.assertEqual(data, program)
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.data = ProgramFactory(uuid=cls.program_uuid, courses=[course])
def test_get_programs(self, mock_get_edx_api_data): programs = [ProgramFactory() for __ in range(3)] mock_get_edx_api_data.return_value = programs data = get_programs() self.assert_contract(mock_get_edx_api_data.call_args) self.assertEqual(data, programs)
def test_get_programs_by_types(self, mock_get_edx_api_data): programs = ProgramFactory.create_batch(2) mock_get_edx_api_data.return_value = programs data = get_programs(types=self.types) self.assert_contract(mock_get_edx_api_data.call_args, types=self.types) self.assertEqual(data, programs)
def setUp(self): super(GetUsersByExternalKeysTests, self).setUp() catalog_org = CatalogOrganizationFactory.create(key=self.organization_key) program = ProgramFactory.create( uuid=self.program_uuid, authoring_organizations=[catalog_org] ) cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=self.program_uuid), program, None)
def test_get_programs_by_types(self, mock_get_edx_api_data): programs = ProgramFactory.create_batch(2) mock_get_edx_api_data.return_value = programs data = get_programs(types=self.types) self.assert_contract(mock_get_edx_api_data.call_args, types=self.types) self.assertEqual(data, programs)
def setUp(self): super(TestProgramMarketingDataExtender, self).setUp() self.course_price = 100 self.number_of_courses = 2 self.program = ProgramFactory( courses=[self._create_course(self.course_price) for __ in range(self.number_of_courses)] )
def test_get_many(self, mock_warning): programs = ProgramFactory.create_batch(3) # Cache details for 2 of 3 programs. partial_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2] } cache.set_many(partial_programs, None) # When called before UUIDs are cached, the function should return an empty # list and log a warning. self.assertEqual(get_programs(), []) mock_warning.assert_called_once_with('Program UUIDs are not cached.') mock_warning.reset_mock() # Cache UUIDs for all 3 programs. cache.set(PROGRAM_UUIDS_CACHE_KEY, [program['uuid'] for program in programs], None) actual_programs = get_programs() # The 2 cached programs should be returned while a warning should be logged # for the missing one. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in partial_programs.values())) mock_warning.assert_called_with( 'Details for program {uuid} are not cached.'.format( uuid=programs[2]['uuid'])) mock_warning.reset_mock() # We can't use a set comparison here because these values are dictionaries # and aren't hashable. We've already verified that all programs came out # of the cache above, so all we need to do here is verify the accuracy of # the data itself. for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, partial_programs[key]) # Cache details for all 3 programs. all_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs } cache.set_many(all_programs, None) actual_programs = get_programs() # All 3 programs should be returned. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in all_programs.values())) self.assertFalse(mock_warning.called) for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, all_programs[key])
def setup_catalog_cache(self, program_uuid, organization_key): """ helper function to initialize a cached program with an single authoring_organization """ catalog_org = CatalogOrganizationFactory.create(key=organization_key) program = ProgramFactory.create( uuid=program_uuid, authoring_organizations=[catalog_org] ) cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program_uuid), program, None)
def setUp(self): super(TestProgramMarketingDataExtender, self).setUp() # Ensure the E-Commerce service user exists UserFactory(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME, is_staff=True) self.course_price = 100 self.number_of_courses = 2 self.program = ProgramFactory( courses=[self._create_course(self.course_price) for __ in range(self.number_of_courses)] )
def test_get_programs_with_status_filtering(self, mock_get_edx_api_data): """ The function should request active and retired programs when the Waffle switch is enabled. """ programs = ProgramFactory.create_batch(3) mock_get_edx_api_data.return_value = programs Switch.objects.get_or_create(name='display_retired_programs_on_learner_dashboard', defaults={'active': True}) data = get_programs() expected_querystring = { 'exclude_utm': 1, 'status': ('active', 'retired',) } self.assert_contract(mock_get_edx_api_data.call_args, expected_querystring=expected_querystring) self.assertEqual(data, programs)
def setUp(self): super(ProgramPageBase, self).setUp() self.set_programs_api_configuration(is_enabled=True) self.programs = ProgramFactory.create_batch(3) self.pathways = PathwayFactory.create_batch(3) for pathway in self.pathways: self.programs += pathway['programs'] # add some of the previously created programs to some pathways self.pathways[0]['programs'].extend([self.programs[0], self.programs[1]]) self.pathways[1]['programs'].append(self.programs[0]) self.username = None
def test_catalog_program_missing_org(self): """ Test OrganizationDoesNotExistException is thrown if the cached program does not have an authoring organization. """ program = ProgramFactory.create( uuid=self.program_uuid, authoring_organizations=[] ) cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=self.program_uuid), program, None) organization = OrganizationFactory.create(short_name=self.organization_key) provider = SAMLProviderConfigFactory.create(organization=organization) self.create_social_auth_entry(self.user, provider, self.external_user_id) with pytest.raises(OrganizationDoesNotExistException): get_user_by_program_id(self.external_user_id, self.program_uuid)
def setUp(self): super(TestCachePrograms, self).setUp() self.catalog_integration = self.create_catalog_integration() self.site_domain = 'testsite.com' self.set_up_site( self.site_domain, { 'COURSE_CATALOG_API_URL': self.catalog_integration.get_internal_api_url().rstrip('/') } ) self.list_url = self.catalog_integration.get_internal_api_url().rstrip('/') + '/programs/' self.detail_tpl = self.list_url.rstrip('/') + '/{uuid}/' self.programs = ProgramFactory.create_batch(3) self.uuids = [program['uuid'] for program in self.programs]
def test_get_programs_with_type(self, mock_get_program_types, mock_get_programs): """Verify get_programs_with_type returns the expected list of programs.""" programs_with_program_type = [] programs = ProgramFactory.create_batch(2) program_types = [] for program in programs: program_type = ProgramTypeFactory(name=program['type']) program_types.append(program_type) program_with_type = copy.deepcopy(program) program_with_type['type'] = program_type programs_with_program_type.append(program_with_type) mock_get_programs.return_value = programs mock_get_program_types.return_value = program_types actual = get_programs_with_type(self.site) self.assertEqual(actual, programs_with_program_type)
class TestProgramMarketingDataExtender(ModuleStoreTestCase): """Tests of the program data extender utility class.""" ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT = '{root}/api/v2/baskets/calculate/'.format(root=ECOMMERCE_URL_ROOT) instructors = { 'instructors': [ { 'name': 'test-instructor1', 'organization': 'TextX', }, { 'name': 'test-instructor2', 'organization': 'TextX', } ] } def setUp(self): super(TestProgramMarketingDataExtender, self).setUp() # Ensure the E-Commerce service user exists UserFactory(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME, is_staff=True) self.course_price = 100 self.number_of_courses = 2 self.program = ProgramFactory( courses=[_create_course(self, self.course_price) for __ in range(self.number_of_courses)], applicable_seat_types=['verified'] ) def _prepare_program_for_discounted_price_calculation_endpoint(self): """ Program's applicable seat types should match some or all seat types of the seats that are a part of the program. Otherwise, ecommerce API endpoint for calculating the discounted price won't be called. Returns: seat: seat for which the discount is applicable """ self.ecommerce_service = EcommerceService() seat = self.program['courses'][0]['course_runs'][0]['seats'][0] self.program['applicable_seat_types'] = [seat['type']] return seat def _update_discount_data(self, mock_discount_data): """ Helper method that updates mocked discount data with - a flag indicating whether the program price is discounted - the amount of the discount (0 in case there's no discount) """ program_discounted_price = mock_discount_data['total_incl_tax'] program_full_price = mock_discount_data['total_incl_tax_excl_discounts'] mock_discount_data.update({ 'is_discounted': program_discounted_price < program_full_price, 'discount_value': program_full_price - program_discounted_price }) def test_instructors(self): data = ProgramMarketingDataExtender(self.program, self.user).extend() self.program.update(self.instructors['instructors']) self.assertEqual(data, self.program) def test_course_pricing(self): data = ProgramMarketingDataExtender(self.program, self.user).extend() program_full_price = self.course_price * self.number_of_courses self.assertEqual(data['number_of_courses'], self.number_of_courses) self.assertEqual(data['full_program_price'], program_full_price) self.assertEqual(data['avg_price_per_course'], program_full_price / self.number_of_courses) 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) @ddt.data(True, False) @mock.patch(UTILS_MODULE + '.has_access') def test_can_enroll(self, can_enroll, mock_has_access): """ Verify that the student's can_enroll status is included. """ mock_has_access.return_value = can_enroll data = ProgramMarketingDataExtender(self.program, self.user).extend() self.assertEqual(data['courses'][0]['course_runs'][0]['can_enroll'], can_enroll) @httpretty.activate def test_fetching_program_discounted_price(self): """ Authenticated users eligible for one click purchase should see the purchase button - displaying program's discounted price if it exists. - leading to ecommerce basket page """ self._prepare_program_for_discounted_price_calculation_endpoint() mock_discount_data = { 'total_incl_tax_excl_discounts': 200.0, 'currency': 'USD', 'total_incl_tax': 50.0 } httpretty.register_uri( httpretty.GET, self.ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT, body=json.dumps(mock_discount_data), content_type='application/json' ) data = ProgramMarketingDataExtender(self.program, self.user).extend() self._update_discount_data(mock_discount_data) self.assertEqual( data['skus'], [course['course_runs'][0]['seats'][0]['sku'] for course in self.program['courses']] ) self.assertEqual(data['discount_data'], mock_discount_data) @httpretty.activate def test_fetching_program_discounted_price_as_anonymous_user(self): """ Anonymous users should see the purchase button same way the authenticated users do when the program is eligible for one click purchase. """ self._prepare_program_for_discounted_price_calculation_endpoint() mock_discount_data = { 'total_incl_tax_excl_discounts': 200.0, 'currency': 'USD', 'total_incl_tax': 50.0 } httpretty.register_uri( httpretty.GET, self.ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT, body=json.dumps(mock_discount_data), content_type='application/json' ) data = ProgramMarketingDataExtender(self.program, AnonymousUserFactory()).extend() self._update_discount_data(mock_discount_data) self.assertEqual( data['skus'], [course['course_runs'][0]['seats'][0]['sku'] for course in self.program['courses']] ) self.assertEqual(data['discount_data'], mock_discount_data) def test_fetching_program_discounted_price_no_applicable_seats(self): """ User shouldn't be able to do a one click purchase of a program if a program has no applicable seat types. """ self.program['applicable_seat_types'] = [] data = ProgramMarketingDataExtender(self.program, self.user).extend() self.assertEqual(len(data['skus']), 0) @httpretty.activate def test_fetching_program_discounted_price_api_exception_caught(self): """ User should be able to do a one click purchase of a program even if the ecommerce API throws an exception during the calculation of program discounted price. """ self._prepare_program_for_discounted_price_calculation_endpoint() httpretty.register_uri( httpretty.GET, self.ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT, status=400, content_type='application/json' ) data = ProgramMarketingDataExtender(self.program, self.user).extend() self.assertEqual( data['skus'], [course['course_runs'][0]['seats'][0]['sku'] for course in self.program['courses']] )
class TestProgramMarketingDataExtender(ModuleStoreTestCase): """Tests of the program data extender utility class.""" instructors = { 'instructors': [ { 'name': 'test-instructor1', 'organization': 'TextX', }, { 'name': 'test-instructor2', 'organization': 'TextX', } ] } def setUp(self): super(TestProgramMarketingDataExtender, self).setUp() self.course_price = 100 self.number_of_courses = 2 self.program = ProgramFactory( courses=[self._create_course(self.course_price) for __ in range(self.number_of_courses)] ) 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_instructors(self): data = ProgramMarketingDataExtender(self.program, self.user).extend() self.program.update(self.instructors['instructors']) self.assertEqual(data, self.program) def test_course_pricing(self): data = ProgramMarketingDataExtender(self.program, self.user).extend() program_full_price = self.course_price * self.number_of_courses self.assertEqual(data['number_of_courses'], self.number_of_courses) self.assertEqual(data['full_program_price'], program_full_price) self.assertEqual(data['avg_price_per_course'], program_full_price / self.number_of_courses) @ddt.data(True, False) @mock.patch(UTILS_MODULE + '.has_access') def test_can_enroll(self, can_enroll, mock_has_access): """ Verify that the student's can_enroll status is included. """ mock_has_access.return_value = can_enroll data = ProgramMarketingDataExtender(self.program, self.user).extend() self.assertEqual(data['courses'][0]['course_runs'][0]['can_enroll'], can_enroll)
def test_get_many(self, mock_warning, mock_info): programs = ProgramFactory.create_batch(3) # Cache details for 2 of 3 programs. partial_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2] } cache.set_many(partial_programs, None) # When called before UUIDs are cached, the function should return an # empty list and log a warning. self.assertEqual(get_programs(self.site), []) mock_warning.assert_called_once_with('Failed to get program UUIDs from the cache.') mock_warning.reset_mock() # Cache UUIDs for all 3 programs. cache.set( SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=self.site.domain), [program['uuid'] for program in programs], None ) actual_programs = get_programs(self.site) # The 2 cached programs should be returned while info and warning # messages should be logged for the missing one. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in partial_programs.values()) ) mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.') mock_warning.assert_called_with( 'Failed to get details for program {uuid} from the cache.'.format(uuid=programs[2]['uuid']) ) mock_warning.reset_mock() # We can't use a set comparison here because these values are dictionaries # and aren't hashable. We've already verified that all programs came out # of the cache above, so all we need to do here is verify the accuracy of # the data itself. for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, partial_programs[key]) # Cache details for all 3 programs. all_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs } cache.set_many(all_programs, None) actual_programs = get_programs(self.site) # All 3 programs should be returned. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in all_programs.values()) ) self.assertFalse(mock_warning.called) for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, all_programs[key])