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
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
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
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
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