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.

guest_checkout_tests.py 18KB

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