예제 #1
0
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']]
        )
예제 #2
0
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'])
예제 #3
0
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']]
        )
예제 #4
0
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)
예제 #5
0
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']]
        )
예제 #6
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.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)