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, is_enrolled=False): """ 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( is_enrolled=is_enrolled, 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_learner_eligibility_for_one_click_purchase(self): """ Learner should be eligible for one click purchase if: - program is eligible for one click purchase - learner is not enrolled in any of the course runs associated with the program """ data = ProgramMarketingDataExtender(self.program, self.user).extend() self.assertTrue(data['is_learner_eligible_for_one_click_purchase']) courses = [self._create_course(self.course_price)] program = ProgramFactory( courses=courses, is_program_eligible_for_one_click_purchase=False ) data = ProgramMarketingDataExtender(program, self.user).extend() self.assertFalse(data['is_learner_eligible_for_one_click_purchase']) courses.append(self._create_course(self.course_price, is_enrolled=True)) program2 = ProgramFactory( courses=courses, is_program_eligible_for_one_click_purchase=True ) data = ProgramMarketingDataExtender(program2, self.user).extend() self.assertFalse(data['is_learner_eligible_for_one_click_purchase'])
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)
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=[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 _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 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_learner_eligibility_for_one_click_purchase(self): """ Learner should be eligible for one click purchase if: - program is eligible for one click purchase - learner is not enrolled in any of the course runs associated with the program """ data = ProgramMarketingDataExtender(self.program, self.user).extend() self.assertTrue(data['is_learner_eligible_for_one_click_purchase']) courses = [self._create_course(self.course_price)] program = ProgramFactory( courses=courses, is_program_eligible_for_one_click_purchase=False ) data = ProgramMarketingDataExtender(program, self.user).extend() self.assertFalse(data['is_learner_eligible_for_one_click_purchase']) course = self._create_course(self.course_price) CourseEnrollmentFactory(user=self.user, course_id=course['course_runs'][0]['key']) program2 = ProgramFactory( courses=[course], is_program_eligible_for_one_click_purchase=True ) data = ProgramMarketingDataExtender(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 ) 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']) @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.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.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. """ 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 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.instructor_info = self.instructors 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) def test_instructors_retrieval(self): data = ProgramDataExtender(self.program, self.user).extend(include_instructors=True) self.program.update(self.instructors['instructors']) self.assertEqual(data, self.program)