def test_non_200_propagates_to_response(self, ccxcon_api):
        """
        If ccx POST returns a non-200, the response will have this information.
        """
        error_message = "This is an error"
        ccxcon_api.return_value.create_ccx.return_value = [
            False, 500, error_message
        ]
        total_seats = 5

        # Add an extra module to assert we only POST to ccx endpoint once per course.
        module2 = ModuleFactory.create(course=self.course,
                                       price_without_tax=123)

        cart_item = {
            "uuids": [self.module.uuid, module2.uuid],
            "seats": total_seats,
            "course_uuid": self.course.uuid
        }
        cart = [cart_item]
        total = calculate_cart_subtotal(cart)
        # Note: autospec intentionally not used, we need an unbound method here
        with patch.object(Charge, 'create') as create_mock:

            resp = self.client.post(reverse('checkout'),
                                    content_type='application/json',
                                    data=json.dumps({
                                        "cart": cart,
                                        "token": "token",
                                        "total": float(total)
                                    }))

        assert resp.status_code == 500, resp.content.decode('utf-8')
        assert error_message in resp.content.decode('utf-8')
        assert create_mock.call_count == 1
    def test_failed_post_makes_orders(self, ccxcon_api):
        """
        If the ccx creation fails, orders are still created
        """
        start = Order.objects.count()
        start_ol = OrderLine.objects.count()
        ccxcon_api.return_value.create_ccx.side_effect = AttributeError()
        cart_item = {
            "uuids": [self.module.uuid],
            "seats": 5,
            "course_uuid": self.course.uuid
        }
        cart = [cart_item]

        with patch.object(Charge, 'create'):
            resp = self.client.post(reverse('checkout'),
                                    content_type='application/json',
                                    data=json.dumps({
                                        "cart":
                                        cart,
                                        "token":
                                        "token",
                                        "total":
                                        float(calculate_cart_subtotal(cart))
                                    }))

        assert resp.status_code == 500, resp.content.decode('utf-8')
        assert Order.objects.count() - start == 1
        assert OrderLine.objects.count() - start_ol == 1
    def test_errors_propagate_to_response(self, ccxcon_api):
        """
        If there are errors on checkout, they make it to the response.
        """
        course2 = CourseFactory.create(live=True)
        module2 = ModuleFactory.create(course=course2, price_without_tax=345)
        ccxcon_api.return_value.create_ccx.side_effect = [
            AttributeError("Example Error"),
            AttributeError("Another Error"),
        ]
        cart = [{
            "uuids": [self.module.uuid],
            "seats": 5,
            "course_uuid": self.course.uuid
        }, {
            "uuids": [module2.uuid],
            "seats": 9,
            "course_uuid": module2.course.uuid
        }]

        with patch.object(Charge, 'create'):
            resp = self.client.post(reverse('checkout'),
                                    content_type='application/json',
                                    data=json.dumps({
                                        "cart":
                                        cart,
                                        "token":
                                        "token",
                                        "total":
                                        float(calculate_cart_subtotal(cart))
                                    }))

        assert resp.status_code == 500, resp.content.decode('utf-8')
        assert "Example Error" in resp.content.decode('utf-8')
        assert "Another Error" in resp.content.decode('utf-8')
    def test_cart_fails_to_checkout(self, ccxcon_api):
        """
        Assert that we clean up everything if checkout failed.
        """
        cart_item = {
            "uuids": [self.module.uuid],
            "seats": 5,
            "course_uuid": self.course.uuid
        }
        cart = [cart_item]
        # Note: autospec intentionally not used, we need an unbound method here
        with patch.object(Charge, 'create') as create_mock:

            def _create_mock(**kwargs):  # pylint: disable=unused-argument
                """Side effect function to raise an exception"""
                raise Exception("test exception")

            create_mock.side_effect = _create_mock

            with self.assertRaises(Exception) as ex:
                self.client.post(reverse('checkout'),
                                 content_type='application/json',
                                 data=json.dumps({
                                     "cart":
                                     cart,
                                     "token":
                                     "token",
                                     "total":
                                     float(calculate_cart_subtotal(cart))
                                 }))
            assert ex.exception.args[0] == 'test exception'

            assert Order.objects.count() == 0
            assert OrderLine.objects.count() == 0
            assert not ccxcon_api.called
Exemple #5
0
    def validate_data(self):
        """
        Validates incoming request data.

        Returns:
            (string, dict): stripe token and cart information.
        """
        data = self.request.data
        try:
            token = str(data['token'])
            cart = data['cart']
            estimated_total = Decimal(float(data['total']))
        except KeyError as ex:
            raise ValidationError("Missing key {}".format(ex.args[0]))
        except (TypeError, ValueError):
            raise ValidationError("Invalid float")

        if not isinstance(cart, list):
            raise ValidationError("Cart must be a list of items")
        if len(cart) == 0:
            raise ValidationError("Cannot checkout an empty cart")
        validate_cart(cart, self.request.user)

        total = calculate_cart_subtotal(cart)
        if get_cents(total) != get_cents(estimated_total):
            log.error(
                "Cart total doesn't match expected value. "
                "Total from client: %f but actual total is: %f",
                estimated_total,
                total
            )
            raise ValidationError("Cart total doesn't match expected value")

        return token, cart
Exemple #6
0
    def test_cart_with_zero_price(self):
        """
        Assert that we support carts with zero priced modules
        """
        self.module.price_without_tax = 0
        self.module.save()

        assert calculate_cart_subtotal([{
            "uuids": [self.module.uuid],
            "seats": 10,
            "course_uuid": self.course.uuid
        }]) == 0
Exemple #7
0
 def test_cart_total(self):
     """
     Assert that the cart total is calculated correctly (seats * price)
     """
     module2 = ModuleFactory.create(course=self.course, )
     seats = 10
     assert calculate_cart_subtotal([{
         "uuids": [self.module.uuid, module2.uuid],
         "seats":
         seats,
         "course_uuid":
         self.course.uuid
     }]) == (self.module.price_without_tax * seats +
             module2.price_without_tax * seats)
    def test_stripe_charge(self, ccxcon_api):
        """
        Assert that we execute the stripe charge with the proper arguments, on successful checkout.
        """
        ccxcon_api.return_value.create_ccx.return_value = (True, 200, '')
        cart_item = {
            "uuids": [self.module.uuid],
            "seats": 5,
            "course_uuid": self.course.uuid
        }
        cart = [cart_item]
        total = calculate_cart_subtotal(cart)
        # Note: autospec intentionally not used, we need an unbound method here
        with patch.object(Charge, 'create') as create_mock:
            mocked_kwargs = {}

            def _create_mock(**kwargs):
                """Side effect function to capture kwargs for assert"""
                # Note: not just assigning to mocked_kwargs because of scope differences.
                for key, value in kwargs.items():
                    mocked_kwargs[key] = value

            create_mock.side_effect = _create_mock

            resp = self.client.post(reverse('checkout'),
                                    content_type='application/json',
                                    data=json.dumps({
                                        "cart": cart,
                                        "token": "token",
                                        "total": float(total)
                                    }))
        assert resp.status_code == 200, resp.content.decode('utf-8')

        assert mocked_kwargs['source'] == 'token'
        assert mocked_kwargs['amount'] == get_cents(total)
        assert mocked_kwargs['currency'] == 'usd'
        assert 'order_id' in mocked_kwargs['metadata']
        assert ccxcon_api.return_value.create_ccx.called

        order = Order.objects.get(id=mocked_kwargs['metadata']['order_id'])
        assert order.orderline_set.count() == 1
        order_line = order.orderline_set.first()
        assert calculate_orderline_total(
            cart_item['uuids'][0], cart_item['seats']) == order_line.line_total
    def test_no_userinfo(self, ccxcon_api):
        """Should show validation error if there isn't a user info"""
        user = User.objects.create_user(
            username='******',
            password='******',
            email='*****@*****.**',
        )
        self.client.login(username=user, password='******')

        ccxcon_api.return_value.create_ccx.return_value.status_code = [
            True, 200, ''
        ]
        cart_item = {
            "uuids": [self.module.uuid],
            "seats": 5,
            "course_uuid": self.course.uuid
        }
        cart = [cart_item]
        total = calculate_cart_subtotal(cart)
        # Note: autospec intentionally not used, we need an unbound method here
        with patch.object(Charge, 'create') as create_mock:
            mocked_kwargs = {}

            def _create_mock(**kwargs):
                """Side effect function to capture kwargs for assert"""
                # Note: not just assigning to mocked_kwargs because of scope differences.
                for key, value in kwargs.items():
                    mocked_kwargs[key] = value

            create_mock.side_effect = _create_mock

            resp = self.client.post(reverse('checkout'),
                                    content_type='application/json',
                                    data=json.dumps({
                                        "cart": cart,
                                        "token": "token",
                                        "total": float(total)
                                    }))

        assert resp.status_code == 400, resp.content.decode('utf-8')
        assert 'must have a user profile' in resp.content.decode('utf-8')
    def test_ccx_creation(self, ccxcon_api):
        """
        Assert that CCX is created, on successful checkout.
        """
        total_seats = 5

        # Add an extra module to assert we only POST to ccx endpoint once per course.
        module2 = ModuleFactory.create(course=self.course,
                                       price_without_tax=123)

        ccxcon_api.return_value.create_ccx.return_value = (True, 200, '')

        cart_item = {
            "uuids": [self.module.uuid, module2.uuid],
            "seats": total_seats,
            "course_uuid": self.course.uuid
        }
        cart = [cart_item]
        total = calculate_cart_subtotal(cart)
        # Note: autospec intentionally not used, we need an unbound method here
        with patch.object(Charge, 'create') as create_mock:

            resp = self.client.post(reverse('checkout'),
                                    content_type='application/json',
                                    data=json.dumps({
                                        "cart": cart,
                                        "token": "token",
                                        "total": float(total)
                                    }))

        assert resp.status_code == 200, resp.content.decode('utf-8')
        assert create_mock.call_count == 1
        assert ccxcon_api.return_value.create_ccx.call_count == 1
        ccxcon_api.return_value.create_ccx.assert_called_with(
            self.course.uuid,
            self.user.email,
            total_seats,
            self.course.title,
            course_modules=[self.module.uuid, module2.uuid],
        )
    def test_total_is_a_number_string(self):  # pylint: disable=unused-argument
        """
        Assert that the total can be a string with a number representation
        """
        total_seats = 5

        # Add an extra module to assert we only POST to ccx endpoint once per course.
        module2 = ModuleFactory.create(course=self.course,
                                       price_without_tax=123)

        cart_item = {
            "uuids": [self.module.uuid, module2.uuid],
            "seats": total_seats,
            "course_uuid": self.course.uuid
        }
        cart = [cart_item]
        total = calculate_cart_subtotal(cart)
        # Note: autospec intentionally not used, we need an unbound method here
        with patch.object(Charge, 'create') as create_mock:
            with patch(
                    'portal.views.checkout_api.CheckoutView.notify_external_services'
            ) as notify_mock:
                resp = self.client.post(reverse('checkout'),
                                        content_type='application/json',
                                        data=json.dumps({
                                            "cart":
                                            cart,
                                            "token":
                                            "token",
                                            "total":
                                            str(float(total))
                                        }))

        assert resp.status_code == 200, resp.content.decode('utf-8')
        assert create_mock.call_count == 1
        assert notify_mock.call_count == 1
Exemple #12
0
 def test_empty_cart_total(self):  # pylint: disable=no-self-use
     """
     Assert that an empty cart has a total of $0
     """
     assert calculate_cart_subtotal([]) == 0