You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_guest_checkout.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. import sys
  2. from http import client as http_client
  3. from imp import reload
  4. from importlib import import_module
  5. from unittest import mock
  6. from urllib.parse import quote
  7. from django.conf import settings
  8. from django.test.utils import override_settings
  9. from django.urls import clear_url_caches, reverse
  10. from oscar.apps.shipping import methods
  11. from oscar.core.compat import get_user_model
  12. from oscar.core.loading import get_class, get_classes, get_model
  13. from oscar.test import factories
  14. from oscar.test.testcases import WebTestCase
  15. from . import CheckoutMixin
  16. GatewayForm = get_class('checkout.forms', 'GatewayForm')
  17. CheckoutSessionData = get_class('checkout.utils', 'CheckoutSessionData')
  18. RedirectRequired, UnableToTakePayment, PaymentError = get_classes(
  19. 'payment.exceptions', [
  20. 'RedirectRequired', 'UnableToTakePayment', 'PaymentError'])
  21. UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder')
  22. Basket = get_model('basket', 'Basket')
  23. Order = get_model('order', 'Order')
  24. User = get_user_model()
  25. def reload_url_conf():
  26. # Reload URLs to pick up the overridden settings
  27. if settings.ROOT_URLCONF in sys.modules:
  28. reload(sys.modules[settings.ROOT_URLCONF])
  29. import_module(settings.ROOT_URLCONF)
  30. clear_url_caches()
  31. @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True)
  32. class TestIndexView(CheckoutMixin, WebTestCase):
  33. is_anonymous = True
  34. def setUp(self):
  35. reload_url_conf()
  36. super().setUp()
  37. def test_redirects_customers_with_empty_basket(self):
  38. response = self.get(reverse('checkout:index'))
  39. self.assertRedirectsTo(response, 'basket:summary')
  40. def test_redirects_customers_with_invalid_basket(self):
  41. # Add product to basket but then remove its stock so it is not
  42. # purchasable.
  43. product = factories.ProductFactory()
  44. self.add_product_to_basket(product)
  45. product.stockrecords.all().update(num_in_stock=0)
  46. response = self.get(reverse('checkout:index'))
  47. self.assertRedirectsTo(response, 'basket:summary')
  48. def test_redirects_new_customers_to_registration_page(self):
  49. self.add_product_to_basket()
  50. page = self.get(reverse('checkout:index'))
  51. form = page.form
  52. form['options'].select(GatewayForm.NEW)
  53. new_user_email = 'newcustomer@test.com'
  54. form['username'].value = new_user_email
  55. response = form.submit()
  56. expected_url = '{register_url}?next={forward}&email={email}'.format(
  57. register_url=reverse('customer:register'),
  58. forward='/checkout/shipping-address/',
  59. email=quote(new_user_email))
  60. self.assertRedirects(response, expected_url)
  61. def test_redirects_existing_customers_to_shipping_address_page(self):
  62. existing_user = User.objects.create_user(
  63. username=self.username, email=self.email, password=self.password)
  64. self.add_product_to_basket()
  65. page = self.get(reverse('checkout:index'))
  66. form = page.form
  67. form.select('options', GatewayForm.EXISTING)
  68. form['username'].value = existing_user.email
  69. form['password'].value = self.password
  70. response = form.submit()
  71. self.assertRedirectsTo(response, 'checkout:shipping-address')
  72. def test_redirects_guest_customers_to_shipping_address_page(self):
  73. self.add_product_to_basket()
  74. response = self.enter_guest_details()
  75. self.assertRedirectsTo(response, 'checkout:shipping-address')
  76. def test_prefill_form_with_email_for_returning_guest(self):
  77. self.add_product_to_basket()
  78. email = 'forgetfulguest@test.com'
  79. self.enter_guest_details(email)
  80. page = self.get(reverse('checkout:index'))
  81. self.assertEqual(email, page.form['username'].value)
  82. @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True)
  83. class TestShippingAddressView(CheckoutMixin, WebTestCase):
  84. is_anonymous = True
  85. def setUp(self):
  86. reload_url_conf()
  87. super().setUp()
  88. def test_redirects_customers_with_empty_basket(self):
  89. response = self.get(reverse('checkout:shipping-address'))
  90. self.assertRedirectsTo(response, 'basket:summary')
  91. def test_redirects_customers_who_have_skipped_guest_form(self):
  92. self.add_product_to_basket()
  93. response = self.get(reverse('checkout:shipping-address'))
  94. self.assertRedirectsTo(response, 'checkout:index')
  95. def test_redirects_customers_whose_basket_doesnt_require_shipping(self):
  96. product = self.create_digital_product()
  97. self.add_product_to_basket(product)
  98. self.enter_guest_details()
  99. response = self.get(reverse('checkout:shipping-address'))
  100. self.assertRedirectsTo(response, 'checkout:shipping-method')
  101. def test_redirects_customers_with_invalid_basket(self):
  102. # Add product to basket but then remove its stock so it is not
  103. # purchasable.
  104. product = factories.create_product(num_in_stock=1)
  105. self.add_product_to_basket(product)
  106. self.enter_guest_details()
  107. product.stockrecords.all().update(num_in_stock=0)
  108. response = self.get(reverse('checkout:shipping-address'))
  109. self.assertRedirectsTo(response, 'basket:summary')
  110. def test_shows_initial_data_if_the_form_has_already_been_submitted(self):
  111. self.add_product_to_basket()
  112. self.enter_guest_details('hello@egg.com')
  113. self.enter_shipping_address()
  114. page = self.get(reverse('checkout:shipping-address'), user=self.user)
  115. self.assertEqual('John', page.form['first_name'].value)
  116. self.assertEqual('Doe', page.form['last_name'].value)
  117. self.assertEqual('1 Egg Road', page.form['line1'].value)
  118. self.assertEqual('Shell City', page.form['line4'].value)
  119. self.assertEqual('N12 9RT', page.form['postcode'].value)
  120. @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True)
  121. class TestShippingMethodView(CheckoutMixin, WebTestCase):
  122. is_anonymous = True
  123. def setUp(self):
  124. reload_url_conf()
  125. super().setUp()
  126. def test_redirects_customers_with_empty_basket(self):
  127. response = self.get(reverse('checkout:shipping-method'))
  128. self.assertRedirectsTo(response, 'basket:summary')
  129. def test_redirects_customers_with_invalid_basket(self):
  130. product = factories.create_product(num_in_stock=1)
  131. self.add_product_to_basket(product)
  132. self.enter_guest_details()
  133. self.enter_shipping_address()
  134. product.stockrecords.all().update(num_in_stock=0)
  135. response = self.get(reverse('checkout:shipping-method'))
  136. self.assertRedirectsTo(response, 'basket:summary')
  137. def test_redirects_customers_who_have_skipped_guest_form(self):
  138. self.add_product_to_basket()
  139. response = self.get(reverse('checkout:shipping-method'))
  140. self.assertRedirectsTo(response, 'checkout:index')
  141. def test_redirects_customers_whose_basket_doesnt_require_shipping(self):
  142. product = self.create_digital_product()
  143. self.add_product_to_basket(product)
  144. self.enter_guest_details()
  145. response = self.get(reverse('checkout:shipping-method'))
  146. self.assertRedirectsTo(response, 'checkout:payment-method')
  147. def test_redirects_customers_who_have_skipped_shipping_address_form(self):
  148. self.add_product_to_basket()
  149. self.enter_guest_details()
  150. response = self.get(reverse('checkout:shipping-method'))
  151. self.assertRedirectsTo(response, 'checkout:shipping-address')
  152. @mock.patch('oscar.apps.checkout.views.Repository')
  153. def test_redirects_customers_when_no_shipping_methods_available(
  154. self, mock_repo):
  155. self.add_product_to_basket()
  156. self.enter_guest_details()
  157. self.enter_shipping_address()
  158. # Ensure no shipping methods available
  159. instance = mock_repo.return_value
  160. instance.get_shipping_methods.return_value = []
  161. response = self.get(reverse('checkout:shipping-method'))
  162. self.assertRedirectsTo(response, 'checkout:shipping-address')
  163. @mock.patch('oscar.apps.checkout.views.Repository')
  164. def test_redirects_customers_when_only_one_shipping_method_is_available(
  165. self, mock_repo):
  166. self.add_product_to_basket()
  167. self.enter_guest_details()
  168. self.enter_shipping_address()
  169. # Ensure one shipping method available
  170. instance = mock_repo.return_value
  171. instance.get_shipping_methods.return_value = [methods.Free()]
  172. response = self.get(reverse('checkout:shipping-method'))
  173. self.assertRedirectsTo(response, 'checkout:payment-method')
  174. @mock.patch('oscar.apps.checkout.views.Repository')
  175. def test_shows_form_when_multiple_shipping_methods_available(
  176. self, mock_repo):
  177. self.add_product_to_basket()
  178. self.enter_guest_details()
  179. self.enter_shipping_address()
  180. # Ensure multiple shipping methods available
  181. method = mock.MagicMock()
  182. method.code = 'm'
  183. instance = mock_repo.return_value
  184. instance.get_shipping_methods.return_value = [methods.Free(), method]
  185. form_page = self.get(reverse('checkout:shipping-method'))
  186. self.assertIsOk(form_page)
  187. response = form_page.forms[0].submit()
  188. self.assertRedirectsTo(response, 'checkout:payment-method')
  189. @mock.patch('oscar.apps.checkout.views.Repository')
  190. def test_check_user_can_submit_only_valid_shipping_method(self, mock_repo):
  191. self.add_product_to_basket()
  192. self.enter_guest_details()
  193. self.enter_shipping_address()
  194. method = mock.MagicMock()
  195. method.code = 'm'
  196. instance = mock_repo.return_value
  197. instance.get_shipping_methods.return_value = [methods.Free(), method]
  198. form_page = self.get(reverse('checkout:shipping-method'))
  199. # a malicious attempt?
  200. form_page.forms[0]['method_code'].value = 'super-free-shipping'
  201. response = form_page.forms[0].submit()
  202. self.assertIsNotRedirect(response)
  203. response.mustcontain('Your submitted shipping method is not permitted')
  204. @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True)
  205. class TestPaymentMethodView(CheckoutMixin, WebTestCase):
  206. is_anonymous = True
  207. def setUp(self):
  208. reload_url_conf()
  209. super().setUp()
  210. def test_redirects_customers_with_empty_basket(self):
  211. response = self.get(reverse('checkout:payment-method'))
  212. self.assertRedirectsTo(response, 'basket:summary')
  213. def test_redirects_customers_with_invalid_basket(self):
  214. product = factories.create_product(num_in_stock=1)
  215. self.add_product_to_basket(product)
  216. self.enter_guest_details()
  217. self.enter_shipping_address()
  218. product.stockrecords.all().update(num_in_stock=0)
  219. response = self.get(reverse('checkout:payment-method'))
  220. self.assertRedirectsTo(response, 'basket:summary')
  221. def test_redirects_customers_who_have_skipped_guest_form(self):
  222. self.add_product_to_basket()
  223. response = self.get(reverse('checkout:payment-method'))
  224. self.assertRedirectsTo(response, 'checkout:index')
  225. def test_redirects_customers_who_have_skipped_shipping_address_form(self):
  226. self.add_product_to_basket()
  227. self.enter_guest_details()
  228. response = self.get(reverse('checkout:payment-method'))
  229. self.assertRedirectsTo(response, 'checkout:shipping-address')
  230. def test_redirects_customers_who_have_skipped_shipping_method_step(self):
  231. self.add_product_to_basket()
  232. self.enter_guest_details()
  233. self.enter_shipping_address()
  234. response = self.get(reverse('checkout:payment-method'))
  235. self.assertRedirectsTo(response, 'checkout:shipping-method')
  236. @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True)
  237. class TestPaymentDetailsView(CheckoutMixin, WebTestCase):
  238. is_anonymous = True
  239. def setUp(self):
  240. reload_url_conf()
  241. super().setUp()
  242. def test_redirects_customers_with_empty_basket(self):
  243. response = self.get(reverse('checkout:payment-details'))
  244. self.assertRedirectsTo(response, 'basket:summary')
  245. def test_redirects_customers_with_invalid_basket(self):
  246. product = factories.create_product(num_in_stock=1)
  247. self.add_product_to_basket(product)
  248. self.enter_guest_details()
  249. self.enter_shipping_address()
  250. product.stockrecords.all().update(num_in_stock=0)
  251. response = self.get(reverse('checkout:payment-details'))
  252. self.assertRedirectsTo(response, 'basket:summary')
  253. def test_redirects_customers_who_have_skipped_guest_form(self):
  254. self.add_product_to_basket()
  255. response = self.get(reverse('checkout:payment-details'))
  256. self.assertRedirectsTo(response, 'checkout:index')
  257. def test_redirects_customers_who_have_skipped_shipping_address_form(self):
  258. self.add_product_to_basket()
  259. self.enter_guest_details()
  260. response = self.get(reverse('checkout:payment-details'))
  261. self.assertRedirectsTo(response, 'checkout:shipping-address')
  262. def test_redirects_customers_who_have_skipped_shipping_method_step(self):
  263. self.add_product_to_basket()
  264. self.enter_guest_details()
  265. self.enter_shipping_address()
  266. response = self.get(reverse('checkout:payment-details'))
  267. self.assertRedirectsTo(response, 'checkout:shipping-method')
  268. @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_payment')
  269. def test_redirects_customers_when_using_bank_gateway(self, mock_method):
  270. bank_url = 'https://bank-website.com'
  271. e = RedirectRequired(url=bank_url)
  272. mock_method.side_effect = e
  273. preview = self.ready_to_place_an_order(is_guest=True)
  274. bank_redirect = preview.forms['place_order_form'].submit()
  275. assert bank_redirect.status_code == 302
  276. assert bank_redirect.url == bank_url
  277. @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_payment')
  278. def test_handles_anticipated_payments_errors_gracefully(self, mock_method):
  279. msg = 'Submitted expiration date is wrong'
  280. e = UnableToTakePayment(msg)
  281. mock_method.side_effect = e
  282. preview = self.ready_to_place_an_order(is_guest=True)
  283. response = preview.forms['place_order_form'].submit()
  284. self.assertIsOk(response)
  285. # check user is warned
  286. response.mustcontain(msg)
  287. # check basket is restored
  288. basket = Basket.objects.get()
  289. self.assertEqual(basket.status, Basket.OPEN)
  290. @mock.patch('oscar.apps.checkout.views.logger')
  291. @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_payment')
  292. def test_handles_unexpected_payment_errors_gracefully(
  293. self, mock_method, mock_logger):
  294. msg = 'This gateway is down for maintenance'
  295. e = PaymentError(msg)
  296. mock_method.side_effect = e
  297. preview = self.ready_to_place_an_order(is_guest=True)
  298. response = preview.forms['place_order_form'].submit()
  299. self.assertIsOk(response)
  300. # check user is warned with a generic error
  301. response.mustcontain(
  302. 'A problem occurred while processing payment for this order',
  303. no=[msg])
  304. # admin should be warned
  305. self.assertTrue(mock_logger.error.called)
  306. # check basket is restored
  307. basket = Basket.objects.get()
  308. self.assertEqual(basket.status, Basket.OPEN)
  309. @mock.patch('oscar.apps.checkout.views.logger')
  310. @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_payment')
  311. def test_handles_bad_errors_during_payments(
  312. self, mock_method, mock_logger):
  313. e = Exception()
  314. mock_method.side_effect = e
  315. preview = self.ready_to_place_an_order(is_guest=True)
  316. response = preview.forms['place_order_form'].submit()
  317. self.assertIsOk(response)
  318. self.assertTrue(mock_logger.error.called)
  319. basket = Basket.objects.get()
  320. self.assertEqual(basket.status, Basket.OPEN)
  321. @mock.patch('oscar.apps.checkout.views.logger')
  322. @mock.patch('oscar.apps.checkout.views.PaymentDetailsView.handle_order_placement')
  323. def test_handles_unexpected_order_placement_errors_gracefully(
  324. self, mock_method, mock_logger):
  325. e = UnableToPlaceOrder()
  326. mock_method.side_effect = e
  327. preview = self.ready_to_place_an_order(is_guest=True)
  328. response = preview.forms['place_order_form'].submit()
  329. self.assertIsOk(response)
  330. self.assertTrue(mock_logger.error.called)
  331. basket = Basket.objects.get()
  332. self.assertEqual(basket.status, Basket.OPEN)
  333. @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True)
  334. class TestPaymentDetailsWithPreview(CheckoutMixin, WebTestCase):
  335. is_anonymous = True
  336. csrf_checks = False
  337. def setUp(self):
  338. reload_url_conf()
  339. super().setUp()
  340. def test_payment_form_being_submitted_from_payment_details_view(self):
  341. payment_details = self.reach_payment_details_page(is_guest=True)
  342. preview = payment_details.forms['sensible_data'].submit()
  343. self.assertEqual(0, Order.objects.all().count())
  344. preview.form.submit().follow()
  345. self.assertEqual(1, Order.objects.all().count())
  346. def test_handles_invalid_payment_forms(self):
  347. payment_details = self.reach_payment_details_page(is_guest=True)
  348. form = payment_details.forms['sensible_data']
  349. # payment forms should use the preview URL not the payment details URL
  350. form.action = reverse('checkout:payment-details')
  351. self.assertEqual(form.submit(status="*").status_code, http_client.BAD_REQUEST)
  352. @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True)
  353. class TestPlacingOrder(CheckoutMixin, WebTestCase):
  354. is_anonymous = True
  355. def setUp(self):
  356. reload_url_conf()
  357. super().setUp()
  358. def test_saves_guest_email_with_order(self):
  359. preview = self.ready_to_place_an_order(is_guest=True)
  360. thank_you = preview.forms['place_order_form'].submit().follow()
  361. order = thank_you.context['order']
  362. self.assertEqual('hello@egg.com', order.guest_email)