import sys from http import client as http_client from imp import reload from importlib import import_module from unittest import mock from urllib.parse import quote from django.conf import settings from django.test.utils import override_settings from django.urls import clear_url_caches, reverse from oscar.apps.shipping import methods from oscar.core.compat import get_user_model from oscar.core.loading import get_class, get_classes, get_model from oscar.test import factories from oscar.test.testcases import WebTestCase from . import CheckoutMixin GatewayForm = get_class('checkout.forms', 'GatewayForm') CheckoutSessionData = get_class('checkout.utils', 'CheckoutSessionData') RedirectRequired, UnableToTakePayment, PaymentError = get_classes( 'payment.exceptions', [ 'RedirectRequired', 'UnableToTakePayment', 'PaymentError']) UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder') Basket = get_model('basket', 'Basket') Order = get_model('order', 'Order') User = get_user_model() def reload_url_conf(): # Reload URLs to pick up the overridden settings if settings.ROOT_URLCONF in sys.modules: reload(sys.modules[settings.ROOT_URLCONF]) import_module(settings.ROOT_URLCONF) clear_url_caches() @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True) class TestIndexView(CheckoutMixin, WebTestCase): is_anonymous = True def setUp(self): reload_url_conf() super().setUp() def test_redirects_customers_with_empty_basket(self): response = self.get(reverse('checkout:index')) self.assertRedirectsTo(response, 'basket:summary') def test_redirects_customers_with_invalid_basket(self): # 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) response = self.get(reverse('checkout:index')) self.assertRedirectsTo(response, 'basket:summary') def test_redirects_new_customers_to_registration_page(self): self.add_product_to_basket() page = self.get(reverse('checkout:index')) form = page.form form['options'].select(GatewayForm.NEW) new_user_email = 'newcustomer@test.com' form['username'].value = new_user_email response = form.submit() expected_url = '{register_url}?next={forward}&email={email}'.format( register_url=reverse('customer:register'), forward='/checkout/shipping-address/', email=quote(new_user_email)) self.assertRedirects(response, expected_url) def test_redirects_existing_customers_to_shipping_address_page(self): existing_user = User.objects.create_user( username=self.username, email=self.email, password=self.password) self.add_product_to_basket() page = self.get(reverse('checkout:index')) form = page.form form.select('options', GatewayForm.EXISTING) form['username'].value = existing_user.email form['password'].value = self.password response = form.submit() self.assertRedirectsTo(response, 'checkout:shipping-address') def test_redirects_guest_customers_to_shipping_address_page(self): self.add_product_to_basket() response = self.enter_guest_details() self.assertRedirectsTo(response, 'checkout:shipping-address') def test_prefill_form_with_email_for_returning_guest(self): self.add_product_to_basket() email = 'forgetfulguest@test.com' self.enter_guest_details(email) page = self.get(reverse('checkout:index')) self.assertEqual(email, page.form['username'].value) @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True) class TestShippingAddressView(CheckoutMixin, WebTestCase): is_anonymous = True def setUp(self): reload_url_conf() super().setUp() def test_redirects_customers_with_empty_basket(self): response = self.get(reverse('checkout:shipping-address')) self.assertRedirectsTo(response, 'basket:summary') def test_redirects_customers_who_have_skipped_guest_form(self): self.add_product_to_basket() response = self.get(reverse('checkout:shipping-address')) self.assertRedirectsTo(response, 'checkout:index') def test_redirects_customers_whose_basket_doesnt_require_shipping(self): product = self.create_digital_product() self.add_product_to_basket(product) self.enter_guest_details() response = self.get(reverse('checkout:shipping-address')) self.assertRedirectsTo(response, 'checkout:shipping-method') def test_redirects_customers_with_invalid_basket(self): # Add product to basket but then remove its stock so it is not # purchasable. product = factories.create_product(num_in_stock=1) self.add_product_to_basket(product) self.enter_guest_details() product.stockrecords.all().update(num_in_stock=0) response = self.get(reverse('checkout:shipping-address')) self.assertRedirectsTo(response, 'basket:summary') def test_shows_initial_data_if_the_form_has_already_been_submitted(self): self.add_product_to_basket() self.enter_guest_details('hello@egg.com') self.enter_shipping_address() page = self.get(reverse('checkout:shipping-address'), user=self.user) self.assertEqual('John', page.form['first_name'].value) self.assertEqual('Doe', page.form['last_name'].value) self.assertEqual('1 Egg Road', page.form['line1'].value) self.assertEqual('Shell City', page.form['line4'].value) self.assertEqual('N12 9RT', page.form['postcode'].value) @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True) class TestShippingMethodView(CheckoutMixin, WebTestCase): is_anonymous = True def setUp(self): reload_url_conf() super().setUp() def test_redirects_customers_with_empty_basket(self): response = self.get(reverse('checkout:shipping-method')) self.assertRedirectsTo(response, 'basket:summary') def test_redirects_customers_with_invalid_basket(self): product = factories.create_product(num_in_stock=1) self.add_product_to_basket(product) self.enter_guest_details() self.enter_shipping_address() product.stockrecords.all().update(num_in_stock=0) response = self.get(reverse('checkout:shipping-method')) self.assertRedirectsTo(response, 'basket:summary') def test_redirects_customers_who_have_skipped_guest_form(self): self.add_product_to_basket() response = self.get(reverse('checkout:shipping-method')) self.assertRedirectsTo(response, 'checkout:index') def test_redirects_customers_whose_basket_doesnt_require_shipping(self): product = self.create_digital_product() self.add_product_to_basket(product) self.enter_guest_details() response = self.get(reverse('checkout:shipping-method')) self.assertRedirectsTo(response, 'checkout:payment-method') def test_redirects_customers_who_have_skipped_shipping_address_form(self): self.add_product_to_basket() self.enter_guest_details() response = self.get(reverse('checkout:shipping-method')) self.assertRedirectsTo(response, 'checkout:shipping-address') @mock.patch('oscar.apps.checkout.views.Repository') def test_redirects_customers_when_no_shipping_methods_available( self, mock_repo): self.add_product_to_basket() 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') @mock.patch('oscar.apps.checkout.views.Repository') def test_redirects_customers_when_only_one_shipping_method_is_available( self, mock_repo): self.add_product_to_basket() 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') @mock.patch('oscar.apps.checkout.views.Repository') def test_shows_form_when_multiple_shipping_methods_available( self, mock_repo): self.add_product_to_basket() 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') @mock.patch('oscar.apps.checkout.views.Repository') def test_check_user_can_submit_only_valid_shipping_method(self, mock_repo): self.add_product_to_basket() 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') @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True) class TestPaymentMethodView(CheckoutMixin, WebTestCase): is_anonymous = True def setUp(self): reload_url_conf() super().setUp() def test_redirects_customers_with_empty_basket(self): response = self.get(reverse('checkout:payment-method')) self.assertRedirectsTo(response, 'basket:summary') def test_redirects_customers_with_invalid_basket(self): product = factories.create_product(num_in_stock=1) self.add_product_to_basket(product) self.enter_guest_details() self.enter_shipping_address() product.stockrecords.all().update(num_in_stock=0) response = self.get(reverse('checkout:payment-method')) self.assertRedirectsTo(response, 'basket:summary') def test_redirects_customers_who_have_skipped_guest_form(self): self.add_product_to_basket() response = self.get(reverse('checkout:payment-method')) self.assertRedirectsTo(response, 'checkout:index') def test_redirects_customers_who_have_skipped_shipping_address_form(self): self.add_product_to_basket() self.enter_guest_details() response = self.get(reverse('checkout:payment-method')) self.assertRedirectsTo(response, 'checkout:shipping-address') def test_redirects_customers_who_have_skipped_shipping_method_step(self): self.add_product_to_basket() self.enter_guest_details() self.enter_shipping_address() response = self.get(reverse('checkout:payment-method')) self.assertRedirectsTo(response, 'checkout:shipping-method') @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True) class TestPaymentDetailsView(CheckoutMixin, WebTestCase): is_anonymous = True def setUp(self): reload_url_conf() super().setUp() def test_redirects_customers_with_empty_basket(self): response = self.get(reverse('checkout:payment-details')) self.assertRedirectsTo(response, 'basket:summary') def test_redirects_customers_with_invalid_basket(self): product = factories.create_product(num_in_stock=1) self.add_product_to_basket(product) self.enter_guest_details() self.enter_shipping_address() product.stockrecords.all().update(num_in_stock=0) response = self.get(reverse('checkout:payment-details')) self.assertRedirectsTo(response, 'basket:summary') def test_redirects_customers_who_have_skipped_guest_form(self): self.add_product_to_basket() response = self.get(reverse('checkout:payment-details')) self.assertRedirectsTo(response, 'checkout:index') def test_redirects_customers_who_have_skipped_shipping_address_form(self): self.add_product_to_basket() self.enter_guest_details() response = self.get(reverse('checkout:payment-details')) self.assertRedirectsTo(response, 'checkout:shipping-address') def test_redirects_customers_who_have_skipped_shipping_method_step(self): self.add_product_to_basket() self.enter_guest_details() self.enter_shipping_address() response = self.get(reverse('checkout:payment-details')) self.assertRedirectsTo(response, 'checkout:shipping-method') @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(is_guest=True) 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(is_guest=True) 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(is_guest=True) 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(is_guest=True) 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_unexpected_order_placement_errors_gracefully( self, mock_method, mock_logger): e = UnableToPlaceOrder() mock_method.side_effect = e preview = self.ready_to_place_an_order(is_guest=True) 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) @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True) class TestPaymentDetailsWithPreview(CheckoutMixin, WebTestCase): is_anonymous = True csrf_checks = False def setUp(self): reload_url_conf() super().setUp() def test_payment_form_being_submitted_from_payment_details_view(self): payment_details = self.reach_payment_details_page(is_guest=True) 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(is_guest=True) 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) @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True) class TestPlacingOrder(CheckoutMixin, WebTestCase): is_anonymous = True def setUp(self): reload_url_conf() super().setUp() def test_saves_guest_email_with_order(self): preview = self.ready_to_place_an_order(is_guest=True) thank_you = preview.forms['place_order_form'].submit().follow() order = thank_you.context['order'] self.assertEqual('hello@egg.com', order.guest_email)