diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 490aac23a4..1ad71ff625 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -67,7 +67,10 @@ class Order(models.Model): @property def total_cost(self): - """ Return the total cost of the order """ + """ + Return the total cost of the cart. If the order has been purchased, returns total of + all purchased and not refunded items. + """ return sum(i.line_cost for i in self.orderitem_set.filter(status=self.status)) def clear(self): diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index a05096ab92..96bcef6fcd 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -1,12 +1,16 @@ """ Tests for Shopping Cart views """ +from urlparse import urlparse + from django.test import TestCase from django.test.utils import override_settings from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.exceptions import ItemNotFoundError from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from shoppingcart.views import add_course_to_cart from shoppingcart.models import Order, OrderItem, CertificateItem, InvalidCartItem, PaidCourseRegistration @@ -14,11 +18,28 @@ from student.tests.factories import UserFactory from student.models import CourseEnrollment from course_modes.models import CourseMode from ..exceptions import PurchasedCallbackException +from mitxmako.shortcuts import render_to_response +from shoppingcart.processors import render_purchase_form_html, process_postpay_callback +from mock import patch, Mock + +def mock_render_purchase_form_html(*args, **kwargs): + return render_purchase_form_html(*args, **kwargs) + +form_mock = Mock(side_effect=mock_render_purchase_form_html) + +def mock_render_to_response(*args, **kwargs): + return render_to_response(*args, **kwargs) + +render_mock = Mock(side_effect=mock_render_to_response) + +postpay_mock = Mock() @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, DEBUG=True) class ShoppingCartViewsTests(ModuleStoreTestCase): def setUp(self): self.user = UserFactory.create() + self.user.set_password('password') + self.user.save() self.course_id = "MITx/999/Robot_Super_Course" self.cost = 40 self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') @@ -29,6 +50,178 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self.course_mode.save() self.cart = Order.get_cart_for_user(self.user) + def login_user(self): + self.client.login(username=self.user.username, password="password") + + def test_add_course_to_cart_anon(self): resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) self.assertEqual(resp.status_code, 403) + + def test_add_course_to_cart_already_in_cart(self): + PaidCourseRegistration.add_to_order(self.cart, self.course_id) + self.login_user() + resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) + self.assertEqual(resp.status_code, 404) + self.assertIn(_('The course {0} is already in your cart.'.format(self.course_id)), resp.content) + + def test_add_course_to_cart_already_registered(self): + CourseEnrollment.enroll(self.user, self.course_id) + self.login_user() + resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) + self.assertEqual(resp.status_code, 404) + self.assertIn(_('You are already registered in course {0}.'.format(self.course_id)), resp.content) + + def test_add_nonexistent_course_to_cart(self): + self.login_user() + resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=['non/existent/course'])) + self.assertEqual(resp.status_code, 404) + self.assertIn(_("The course you requested does not exist."), resp.content) + + def test_add_course_to_cart_success(self): + self.login_user() + resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) + self.assertEqual(resp.status_code, 200) + self.assertTrue(PaidCourseRegistration.part_of_order(self.cart, self.course_id)) + + def test_register_for_verified_cert(self): + self.login_user() + resp = self.client.post(reverse('shoppingcart.views.register_for_verified_cert', args=[self.course_id])) + self.assertEqual(resp.status_code, 200) + self.assertIn(self.course_id, [ci.course_id for ci in + self.cart.orderitem_set.all().select_subclasses('certificateitem')]) + + @patch('shoppingcart.views.render_purchase_form_html', form_mock) + @patch('shoppingcart.views.render_to_response', render_mock) + def test_show_cart(self): + self.login_user() + reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) + cert_item = CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified') + resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[])) + self.assertEqual(resp.status_code, 200) + + ((purchase_form_arg_cart,), _) = form_mock.call_args + purchase_form_arg_cart_items = purchase_form_arg_cart.orderitem_set.all().select_subclasses() + self.assertIn(reg_item, purchase_form_arg_cart_items) + self.assertIn(cert_item, purchase_form_arg_cart_items) + self.assertEqual(len(purchase_form_arg_cart_items), 2) + + ((template, context), _) = render_mock.call_args + self.assertEqual(template, 'shoppingcart/list.html') + self.assertEqual(len(context['shoppingcart_items']), 2) + self.assertEqual(context['amount'], 80) + self.assertIn("80.00", context['form_html']) + + def test_clear_cart(self): + self.login_user() + PaidCourseRegistration.add_to_order(self.cart, self.course_id) + CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified') + self.assertEquals(self.cart.orderitem_set.count(), 2) + resp = self.client.post(reverse('shoppingcart.views.clear_cart', args=[])) + self.assertEqual(resp.status_code, 200) + self.assertEquals(self.cart.orderitem_set.count(), 0) + + @patch('shoppingcart.views.log.exception') + def test_remove_item(self, exception_log): + self.login_user() + reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) + cert_item = CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified') + self.assertEquals(self.cart.orderitem_set.count(), 2) + resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), + {'id': reg_item.id}) + self.assertEqual(resp.status_code, 200) + self.assertEquals(self.cart.orderitem_set.count(), 1) + self.assertNotIn(reg_item, self.cart.orderitem_set.all().select_subclasses()) + + self.cart.purchase() + resp2 = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), + {'id': cert_item.id}) + self.assertEqual(resp2.status_code, 200) + exception_log.assert_called_with( + 'Cannot remove cart OrderItem id={0}. DoesNotExist or item is already purchased'.format(cert_item.id)) + + resp3 = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), + {'id': -1}) + self.assertEqual(resp3.status_code, 200) + exception_log.assert_called_with( + 'Cannot remove cart OrderItem id={0}. DoesNotExist or item is already purchased'.format(-1)) + + @patch('shoppingcart.views.process_postpay_callback', postpay_mock) + def test_postpay_callback_success(self): + postpay_mock.return_value = {'success': True, 'order': self.cart} + self.login_user() + resp = self.client.post(reverse('shoppingcart.views.postpay_callback', args=[])) + self.assertEqual(resp.status_code, 302) + self.assertEqual(urlparse(resp.__getitem__('location')).path, + reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) + + @patch('shoppingcart.views.process_postpay_callback', postpay_mock) + @patch('shoppingcart.views.render_to_response', render_mock) + def test_postpay_callback_failure(self): + postpay_mock.return_value = {'success': False, 'order': self.cart, 'error_html':'ERROR_TEST!!!'} + self.login_user() + resp = self.client.post(reverse('shoppingcart.views.postpay_callback', args=[])) + self.assertEqual(resp.status_code, 200) + self.assertIn('ERROR_TEST!!!', resp.content) + + ((template, context), _) = render_mock.call_args + self.assertEqual(template, 'shoppingcart/error.html') + self.assertEqual(context['order'], self.cart) + self.assertEqual(context['error_html'], 'ERROR_TEST!!!') + + def test_show_receipt_404s(self): + PaidCourseRegistration.add_to_order(self.cart, self.course_id) + CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified') + self.cart.purchase() + + user2 = UserFactory.create() + cart2 = Order.get_cart_for_user(user2) + PaidCourseRegistration.add_to_order(cart2, self.course_id) + cart2.purchase() + + self.login_user() + resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[cart2.id])) + self.assertEqual(resp.status_code, 404) + + resp2 = self.client.get(reverse('shoppingcart.views.show_receipt', args=[1000])) + self.assertEqual(resp2.status_code, 404) + + @patch('shoppingcart.views.render_to_response', render_mock) + def test_show_receipt_success(self): + reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) + cert_item = CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified') + self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') + + self.login_user() + resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) + self.assertEqual(resp.status_code, 200) + self.assertIn('FirstNameTesting123', resp.content) + self.assertIn('StreetTesting123', resp.content) + self.assertIn('80.00', resp.content) + + ((template, context), _) = render_mock.call_args + self.assertEqual(template, 'shoppingcart/receipt.html') + self.assertEqual(context['order'], self.cart) + self.assertIn(reg_item.orderitem_ptr, context['order_items']) + self.assertIn(cert_item.orderitem_ptr, context['order_items']) + self.assertFalse(context['any_refunds']) + + @patch('shoppingcart.views.render_to_response', render_mock) + def test_show_receipt_success_refund(self): + reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) + cert_item = CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified') + self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') + cert_item.status = "refunded" + cert_item.save() + self.assertEqual(self.cart.total_cost, 40) + self.login_user() + resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) + self.assertEqual(resp.status_code, 200) + self.assertIn('40.00', resp.content) + + ((template, context), _) = render_mock.call_args + self.assertEqual(template, 'shoppingcart/receipt.html') + self.assertEqual(context['order'], self.cart) + self.assertIn(reg_item.orderitem_ptr, context['order_items']) + self.assertIn(cert_item.orderitem_ptr, context['order_items']) + self.assertTrue(context['any_refunds']) \ No newline at end of file diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index 39efab4771..a99568b133 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -26,7 +26,7 @@ def add_course_to_cart(request, course_id): PaidCourseRegistration.add_to_order(cart, course_id) except ItemNotFoundError: return HttpResponseNotFound(_('The course you requested does not exist.')) - if request.method == 'GET': + if request.method == 'GET': ### This is temporary for testing purposes and will go away before we pull return HttpResponseRedirect(reverse('shoppingcart.views.show_cart')) return HttpResponse(_("Course added to cart.")) diff --git a/lms/envs/test.py b/lms/envs/test.py index bf2df444f4..a9c51310f6 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -32,6 +32,8 @@ MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True MITX_FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True +MITX_FEATURES['ENABLE_SHOPPING_CART'] = True + # Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it. WIKI_ENABLED = True