| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- from decimal import Decimal as D
- from http import client as http_client
- from unittest import mock
-
- from django.urls import reverse
-
- from oscar.apps.shipping import methods
- from oscar.core.loading import get_class, get_classes, get_model
- from oscar.test import factories
-
- Basket = get_model('basket', 'Basket')
- ConditionalOffer = get_model('offer', 'ConditionalOffer')
- Order = get_model('order', 'Order')
-
- FailedPreCondition = get_class('checkout.exceptions', 'FailedPreCondition')
- GatewayForm = get_class('checkout.forms', 'GatewayForm')
- UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder')
- RedirectRequired, UnableToTakePayment, PaymentError = get_classes(
- 'payment.exceptions', ['RedirectRequired', 'UnableToTakePayment', 'PaymentError'])
- NoShippingRequired = get_class('shipping.methods', 'NoShippingRequired')
-
-
- class CheckoutMixin(object):
-
- def create_digital_product(self):
- product_class = factories.ProductClassFactory(
- requires_shipping=False, track_stock=False)
- product = factories.ProductFactory(product_class=product_class)
- factories.StockRecordFactory(
- num_in_stock=None, price=D('12.00'), product=product)
- return product
-
- def add_product_to_basket(self, product=None, **kwargs):
- if product is None:
- product = factories.ProductFactory()
- factories.StockRecordFactory(
- num_in_stock=10, price=D('12.00'), product=product)
- detail_page = self.get(product.get_absolute_url(), user=kwargs.get('logged_in_user', self.user))
- form = detail_page.forms['add_to_basket_form']
- form.submit()
-
- def add_voucher_to_basket(self, voucher=None):
- if voucher is None:
- voucher = factories.create_voucher()
- basket_page = self.get(reverse('basket:summary'))
- form = basket_page.forms['voucher_form']
- form['code'] = voucher.code
- form.submit()
-
- def enter_guest_details(self, email='guest@example.com'):
- index_page = self.get(reverse('checkout:index'))
- if index_page.status_code == 200:
- index_page.form['username'] = email
- index_page.form.select('options', GatewayForm.GUEST)
- index_page.form.submit()
-
- def create_shipping_country(self):
- return factories.CountryFactory(
- iso_3166_1_a2='GB', is_shipping_country=True)
-
- def enter_shipping_address(self):
- self.create_shipping_country()
- address_page = self.get(reverse('checkout:shipping-address'))
- if address_page.status_code == 200:
- form = address_page.forms['new_shipping_address']
- form['first_name'] = 'John'
- form['last_name'] = 'Doe'
- form['line1'] = '1 Egg Road'
- form['line4'] = 'Shell City'
- form['postcode'] = 'N12 9RT'
- form.submit()
-
- def enter_shipping_method(self):
- self.get(reverse('checkout:shipping-method'))
-
- def place_order(self):
- payment_details = self.get(
- reverse('checkout:shipping-method')).follow().follow()
- preview = payment_details.click(linkid="view_preview")
- return preview.forms['place_order_form'].submit().follow()
-
- def reach_payment_details_page(self):
- self.add_product_to_basket()
- if self.is_anonymous:
- self.enter_guest_details('hello@egg.com')
- self.enter_shipping_address()
- return self.get(
- reverse('checkout:shipping-method')).follow().follow()
-
- def ready_to_place_an_order(self):
- payment_details = self.reach_payment_details_page()
- return payment_details.click(linkid="view_preview")
-
-
- class IndexViewPreConditionsMixin:
-
- view_name = None
-
- # Disable skip conditions, so that we do not first get redirected forwards
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_payment_is_required')
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_basket_requires_shipping')
- def test_check_basket_is_not_empty(
- self,
- mock_skip_unless_basket_requires_shipping,
- mock_skip_unless_payment_is_required,
- ):
- response = self.get(reverse(self.view_name))
- self.assertRedirectsTo(response, 'basket:summary')
-
- # Disable skip conditions, so that we do not first get redirected forwards
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_payment_is_required')
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_basket_requires_shipping')
- def test_check_basket_is_valid(
- self,
- mock_skip_unless_basket_requires_shipping,
- mock_skip_unless_payment_is_required,
- ):
- # Add product to basket but then remove its stock so it is not
- # purchasable.
- product = factories.ProductFactory()
- self.add_product_to_basket(product)
- product.stockrecords.all().update(num_in_stock=0)
- if self.is_anonymous:
- self.enter_guest_details()
-
- response = self.get(reverse(self.view_name))
- self.assertRedirectsTo(response, 'basket:summary')
-
-
- class ShippingAddressViewSkipConditionsMixin:
-
- view_name = None
- next_view_name = None
-
- def test_skip_unless_basket_requires_shipping(self):
- product = self.create_digital_product()
- self.add_product_to_basket(product)
- if self.is_anonymous:
- self.enter_guest_details()
-
- response = self.get(reverse(self.view_name))
- self.assertRedirectsTo(response, self.next_view_name)
-
-
- class ShippingAddressViewPreConditionsMixin(IndexViewPreConditionsMixin):
-
- view_name = None
-
- # Disable skip conditions, so that we do not first get redirected forwards
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_payment_is_required')
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_basket_requires_shipping')
- def test_check_user_email_is_captured(
- self,
- mock_skip_unless_basket_requires_shipping,
- mock_skip_unless_payment_is_required,
- ):
- if self.is_anonymous:
- self.add_product_to_basket()
- response = self.get(reverse(self.view_name))
- self.assertRedirectsTo(response, 'checkout:index')
-
-
- class ShippingAddressViewMixin(ShippingAddressViewSkipConditionsMixin, ShippingAddressViewPreConditionsMixin):
-
- def test_submitting_valid_form_adds_data_to_session(self):
- self.add_product_to_basket()
- if self.is_anonymous:
- self.enter_guest_details()
- self.create_shipping_country()
-
- page = self.get(reverse('checkout:shipping-address'))
- form = page.forms['new_shipping_address']
- form['first_name'] = 'Barry'
- form['last_name'] = 'Chuckle'
- form['line1'] = '1 King Street'
- form['line4'] = 'Gotham City'
- form['postcode'] = 'N1 7RR'
- response = form.submit()
- self.assertRedirectsTo(response, 'checkout:shipping-method')
-
- session_data = self.app.session['checkout_data']
- session_fields = session_data['shipping']['new_address_fields']
- self.assertEqual('Barry', session_fields['first_name'])
- self.assertEqual('Chuckle', session_fields['last_name'])
- self.assertEqual('1 King Street', session_fields['line1'])
- self.assertEqual('Gotham City', session_fields['line4'])
- self.assertEqual('N1 7RR', session_fields['postcode'])
-
- def test_shows_initial_data_if_the_form_has_already_been_submitted(self):
- self.add_product_to_basket()
- if self.is_anonymous:
- self.enter_guest_details()
- self.enter_shipping_address()
- page = self.get(reverse('checkout:shipping-address'), user=self.user)
- form = page.forms['new_shipping_address']
- self.assertEqual('John', form['first_name'].value)
- self.assertEqual('Doe', form['last_name'].value)
- self.assertEqual('1 Egg Road', form['line1'].value)
- self.assertEqual('Shell City', form['line4'].value)
- self.assertEqual('N12 9RT', form['postcode'].value)
-
-
- class ShippingMethodViewSkipConditionsMixin:
-
- view_name = None
- next_view_name = None
-
- def test_skip_unless_basket_requires_shipping(self):
- # This skip condition is not a "normal" one, but is implemented in the
- # view's "get" method
- product = self.create_digital_product()
- self.add_product_to_basket(product)
- if self.is_anonymous:
- self.enter_guest_details()
-
- response = self.get(reverse(self.view_name))
- self.assertRedirectsTo(response, self.next_view_name)
- self.assertEqual(self.app.session['checkout_data']['shipping']['method_code'], NoShippingRequired.code)
-
- @mock.patch('oscar.apps.checkout.views.Repository')
- def test_skip_if_single_shipping_method_is_available(self, mock_repo):
- # This skip condition is not a "normal" one, but is implemented in the
- # view's "get" method
- self.add_product_to_basket()
- if self.is_anonymous:
- self.enter_guest_details()
- self.enter_shipping_address()
-
- # Ensure one shipping method available
- instance = mock_repo.return_value
- instance.get_shipping_methods.return_value = [methods.Free()]
-
- response = self.get(reverse('checkout:shipping-method'))
- self.assertRedirectsTo(response, 'checkout:payment-method')
-
-
- class ShippingMethodViewPreConditionsMixin(ShippingAddressViewPreConditionsMixin):
-
- view_name = None
-
- # Disable skip conditions, so that we do not first get redirected forwards
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_payment_is_required')
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_basket_requires_shipping')
- @mock.patch('oscar.apps.checkout.views.Repository')
- def test_check_shipping_methods_are_available(
- self,
- mock_repo,
- mock_skip_unless_basket_requires_shipping,
- mock_skip_unless_payment_is_required,
- ):
- # This pre condition is not a "normal" one, but is implemented in the
- # view's "get" method
- self.add_product_to_basket()
- if self.is_anonymous:
- self.enter_guest_details()
- self.enter_shipping_address()
-
- # Ensure no shipping methods available
- instance = mock_repo.return_value
- instance.get_shipping_methods.return_value = []
-
- response = self.get(reverse('checkout:shipping-method'))
- self.assertRedirectsTo(response, 'checkout:shipping-address')
-
- # Disable skip conditions, so that we do not first get redirected forwards
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_payment_is_required')
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_basket_requires_shipping')
- def test_check_shipping_data_is_captured(
- self,
- mock_skip_unless_basket_requires_shipping,
- mock_skip_unless_payment_is_required,
- ):
- # This pre condition is not a "normal" one, but is implemented in the
- # view's "get" method
- self.add_product_to_basket()
- if self.is_anonymous:
- self.enter_guest_details()
-
- response = self.get(reverse(self.view_name))
- self.assertRedirectsTo(response, 'checkout:shipping-address')
-
-
- class ShippingMethodViewMixin(ShippingMethodViewSkipConditionsMixin, ShippingMethodViewPreConditionsMixin):
-
- @mock.patch('oscar.apps.checkout.views.Repository')
- def test_shows_form_when_multiple_shipping_methods_available(self, mock_repo):
- self.add_product_to_basket()
- if self.is_anonymous:
- self.enter_guest_details()
- self.enter_shipping_address()
-
- # Ensure multiple shipping methods available
- method = mock.MagicMock()
- method.code = 'm'
- instance = mock_repo.return_value
- instance.get_shipping_methods.return_value = [methods.Free(), method]
- form_page = self.get(reverse('checkout:shipping-method'))
- self.assertIsOk(form_page)
-
- response = form_page.forms[0].submit()
- self.assertRedirectsTo(response, 'checkout:payment-method')
-
- # Disable skip conditions, so that we do not first get redirected forwards
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_payment_is_required')
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_basket_requires_shipping')
- @mock.patch('oscar.apps.checkout.views.Repository')
- def test_check_user_can_submit_only_valid_shipping_method(
- self,
- mock_repo,
- mock_skip_unless_basket_requires_shipping,
- mock_skip_unless_payment_is_required,
- ):
- self.add_product_to_basket()
- if self.is_anonymous:
- self.enter_guest_details()
- self.enter_shipping_address()
- method = mock.MagicMock()
- method.code = 'm'
- instance = mock_repo.return_value
- instance.get_shipping_methods.return_value = [methods.Free(), method]
- form_page = self.get(reverse('checkout:shipping-method'))
- # a malicious attempt?
- form_page.forms[0]['method_code'].value = 'super-free-shipping'
- response = form_page.forms[0].submit()
- self.assertIsNotRedirect(response)
- response.mustcontain('Your submitted shipping method is not permitted')
-
-
- class PaymentMethodViewSkipConditionsMixin:
-
- @mock.patch('oscar.apps.checkout.session.SurchargeApplicator.get_surcharges')
- def test_skip_unless_payment_is_required(self, mock_get_surcharges):
- mock_get_surcharges.return_value = []
-
- product = factories.create_product(price=D('0.00'), num_in_stock=100)
- self.add_product_to_basket(product)
- if self.is_anonymous:
- self.enter_guest_details()
- self.enter_shipping_address()
- # The shipping method is set automatically, as there is only one (free)
- # available
-
- response = self.get(reverse('checkout:payment-method'))
- self.assertRedirectsTo(response, 'checkout:preview')
-
-
- class PaymentMethodViewPreConditionsMixin(ShippingMethodViewPreConditionsMixin):
-
- view_name = None
-
- # Disable skip conditions, so that we do not first get redirected forwards
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_payment_is_required')
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_basket_requires_shipping')
- def test_check_shipping_data_is_captured(
- self,
- mock_skip_unless_basket_requires_shipping,
- mock_skip_unless_payment_is_required,
- ):
- super().test_check_shipping_data_is_captured()
-
- self.enter_shipping_address()
-
- response = self.get(reverse(self.view_name))
- self.assertRedirectsTo(response, 'checkout:shipping-method')
-
-
- class PaymentMethodViewMixin(PaymentMethodViewSkipConditionsMixin, PaymentMethodViewPreConditionsMixin):
-
- pass
-
-
- class PaymentDetailsViewSkipConditionsMixin:
-
- @mock.patch('oscar.apps.checkout.session.SurchargeApplicator.get_surcharges')
- def test_skip_unless_payment_is_required(self, mock_get_surcharges):
- mock_get_surcharges.return_value = []
-
- product = factories.create_product(price=D('0.00'), num_in_stock=100)
- self.add_product_to_basket(product)
- if self.is_anonymous:
- self.enter_guest_details()
- self.enter_shipping_address()
- # The shipping method is set automatically, as there is only one (free)
- # available
-
- response = self.get(reverse('checkout:payment-details'))
- self.assertRedirectsTo(response, 'checkout:preview')
-
-
- class PaymentDetailsViewPreConditionsMixin(PaymentMethodViewPreConditionsMixin):
- """
- Does not add any new pre conditions.
- """
-
-
- class PaymentDetailsViewMixin(PaymentDetailsViewSkipConditionsMixin, PaymentDetailsViewPreConditionsMixin):
-
- @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_payment')
- def test_redirects_customers_when_using_bank_gateway(self, mock_method):
-
- bank_url = 'https://bank-website.com'
- e = RedirectRequired(url=bank_url)
- mock_method.side_effect = e
- preview = self.ready_to_place_an_order()
- bank_redirect = preview.forms['place_order_form'].submit()
-
- assert bank_redirect.status_code == 302
- assert bank_redirect.url == bank_url
-
- @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_payment')
- def test_handles_anticipated_payments_errors_gracefully(self, mock_method):
- msg = 'Submitted expiration date is wrong'
- e = UnableToTakePayment(msg)
- mock_method.side_effect = e
- preview = self.ready_to_place_an_order()
- response = preview.forms['place_order_form'].submit()
- self.assertIsOk(response)
- # check user is warned
- response.mustcontain(msg)
- # check basket is restored
- basket = Basket.objects.get()
- self.assertEqual(basket.status, Basket.OPEN)
-
- @mock.patch('oscar.apps.checkout.views.logger')
- @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_payment')
- def test_handles_unexpected_payment_errors_gracefully(
- self, mock_method, mock_logger):
- msg = 'This gateway is down for maintenance'
- e = PaymentError(msg)
- mock_method.side_effect = e
- preview = self.ready_to_place_an_order()
- response = preview.forms['place_order_form'].submit()
- self.assertIsOk(response)
- # check user is warned with a generic error
- response.mustcontain(
- 'A problem occurred while processing payment for this order',
- no=[msg])
- # admin should be warned
- self.assertTrue(mock_logger.error.called)
- # check basket is restored
- basket = Basket.objects.get()
- self.assertEqual(basket.status, Basket.OPEN)
-
- @mock.patch('oscar.apps.checkout.views.logger')
- @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_payment')
- def test_handles_bad_errors_during_payments(
- self, mock_method, mock_logger):
- e = Exception()
- mock_method.side_effect = e
- preview = self.ready_to_place_an_order()
- response = preview.forms['place_order_form'].submit()
- self.assertIsOk(response)
- self.assertTrue(mock_logger.exception.called)
- basket = Basket.objects.get()
- self.assertEqual(basket.status, Basket.OPEN)
-
- @mock.patch('oscar.apps.checkout.views.logger')
- @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_order_placement')
- def test_handles_unexpected_order_placement_errors_gracefully(
- self, mock_method, mock_logger):
- e = UnableToPlaceOrder()
- mock_method.side_effect = e
- preview = self.ready_to_place_an_order()
- response = preview.forms['place_order_form'].submit()
- self.assertIsOk(response)
- self.assertTrue(mock_logger.error.called)
- basket = Basket.objects.get()
- self.assertEqual(basket.status, Basket.OPEN)
-
- @mock.patch('oscar.apps.checkout.views.logger')
- @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_order_placement')
- def test_handles_all_other_exceptions_gracefully(self, mock_method, mock_logger):
- mock_method.side_effect = Exception()
- preview = self.ready_to_place_an_order()
- response = preview.forms['place_order_form'].submit()
- self.assertIsOk(response)
- self.assertTrue(mock_logger.exception.called)
- basket = Basket.objects.get()
- self.assertEqual(basket.status, Basket.OPEN)
-
-
- class PaymentDetailsPreviewViewPreConditionsMixin(PaymentDetailsViewPreConditionsMixin):
-
- # Disable skip conditions, so that we do not first get redirected forwards
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_payment_is_required')
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.skip_unless_basket_requires_shipping')
- @mock.patch('oscar.apps.checkout.session.CheckoutSessionMixin.check_payment_data_is_captured')
- def test_check_payment_data_is_captured(
- self,
- mock_check_payment_data_is_captured,
- mock_skip_unless_basket_requires_shipping,
- mock_skip_unless_payment_is_required,
- ):
- mock_check_payment_data_is_captured.side_effect = FailedPreCondition(url=reverse('checkout:payment-details'))
- response = self.ready_to_place_an_order()
- self.assertRedirectsTo(response, 'checkout:payment-details')
-
-
- class PaymentDetailsPreviewViewMixin(PaymentDetailsPreviewViewPreConditionsMixin):
-
- def test_allows_order_to_be_placed(self):
- self.add_product_to_basket()
- if self.is_anonymous:
- self.enter_guest_details()
- self.enter_shipping_address()
-
- payment_details = self.get(
- reverse('checkout:shipping-method')).follow().follow()
- preview = payment_details.click(linkid="view_preview")
- preview.forms['place_order_form'].submit().follow()
-
- self.assertEqual(1, Order.objects.all().count())
-
- def test_payment_form_being_submitted_from_payment_details_view(self):
- payment_details = self.reach_payment_details_page()
- preview = payment_details.forms['sensible_data'].submit()
- self.assertEqual(0, Order.objects.all().count())
- preview.form.submit().follow()
- self.assertEqual(1, Order.objects.all().count())
-
- def test_handles_invalid_payment_forms(self):
- payment_details = self.reach_payment_details_page()
- form = payment_details.forms['sensible_data']
- # payment forms should use the preview URL not the payment details URL
- form.action = reverse('checkout:payment-details')
- self.assertEqual(form.submit(status="*").status_code, http_client.BAD_REQUEST)
-
- def test_placing_an_order_using_a_voucher_records_use(self):
- self.add_product_to_basket()
- self.add_voucher_to_basket()
- if self.is_anonymous:
- self.enter_guest_details()
- self.enter_shipping_address()
- thankyou = self.place_order()
-
- order = thankyou.context['order']
- self.assertEqual(1, order.discounts.all().count())
-
- discount = order.discounts.all()[0]
- voucher = discount.voucher
- self.assertEqual(1, voucher.num_orders)
-
- def test_placing_an_order_using_an_offer_records_use(self):
- offer = factories.create_offer()
- self.add_product_to_basket()
- if self.is_anonymous:
- self.enter_guest_details()
- self.enter_shipping_address()
- self.place_order()
-
- # Reload offer
- offer = ConditionalOffer.objects.get(id=offer.id)
-
- self.assertEqual(1, offer.num_orders)
- self.assertEqual(1, offer.num_applications)
|