Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

test_guest_checkout.py 18KB

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