From 0b10ad501896c730d90ee4c26b3a44139a86df4a Mon Sep 17 00:00:00 2001 From: ayub-khan Date: Mon, 2 Jul 2018 21:46:22 +0500 Subject: [PATCH] LEARNER-5603 Hot fix to prevent students from using expired coupons and submit payment for baskets with fake vouchers Moving this change under waffle switch --- ecommerce/extensions/payment/constants.py | 2 + .../payment/tests/views/test_cybersource.py | 72 ++++++++++++++++++- .../payment/tests/views/test_paypal.py | 4 +- .../extensions/payment/views/__init__.py | 25 ++++++- 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/ecommerce/extensions/payment/constants.py b/ecommerce/extensions/payment/constants.py index b820cfff4af..fd2b99622f5 100644 --- a/ecommerce/extensions/payment/constants.py +++ b/ecommerce/extensions/payment/constants.py @@ -55,3 +55,5 @@ STRIPE_CARD_TYPE_MAP = { value['stripe_brand']: key for key, value in six.iteritems(CARD_TYPES) if 'stripe_brand' in value } + +VOUCHER_VALIDATION_BEFORE_PAYMENT = 'voucher_validation_before_payment' diff --git a/ecommerce/extensions/payment/tests/views/test_cybersource.py b/ecommerce/extensions/payment/tests/views/test_cybersource.py index 03af47fd677..7b76784cc06 100644 --- a/ecommerce/extensions/payment/tests/views/test_cybersource.py +++ b/ecommerce/extensions/payment/tests/views/test_cybersource.py @@ -1,6 +1,7 @@ """ Tests of the Payment Views. """ from __future__ import unicode_literals +import datetime import json import ddt @@ -8,10 +9,12 @@ import responses from django.conf import settings from django.urls import reverse +from django.utils.timezone import now from freezegun import freeze_time from oscar.apps.payment.exceptions import TransactionDeclined from oscar.core.loading import get_class, get_model from oscar.test import factories +from waffle.testutils import override_switch from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME, ENROLLMENT_CODE_SWITCH from ecommerce.core.models import BusinessClient @@ -21,11 +24,12 @@ from ecommerce.extensions.api.serializers import OrderSerializer from ecommerce.extensions.basket.utils import basket_add_organization_attribute from ecommerce.extensions.order.constants import PaymentEventTypeName +from ecommerce.extensions.payment.constants import VOUCHER_VALIDATION_BEFORE_PAYMENT from ecommerce.extensions.payment.exceptions import InvalidBasketError, InvalidSignatureError from ecommerce.extensions.payment.processors.cybersource import Cybersource from ecommerce.extensions.payment.tests.mixins import CybersourceMixin, CybersourceNotificationTestsMixin from ecommerce.extensions.payment.views.cybersource import CybersourceInterstitialView -from ecommerce.extensions.test.factories import create_basket +from ecommerce.extensions.test.factories import create_basket, prepare_voucher from ecommerce.invoice.models import Invoice from ecommerce.tests.testcases import TestCase @@ -36,9 +40,10 @@ OrderNumberGenerator = get_class('order.utils', 'OrderNumberGenerator') PaymentEvent = get_model('order', 'PaymentEvent') PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse') +Product = get_model('catalogue', 'Product') Selector = get_class('partner.strategy', 'Selector') Source = get_model('payment', 'Source') -Product = get_model('catalogue', 'Product') +Voucher = get_model('voucher', 'Voucher') post_checkout = get_class('checkout.signals', 'post_checkout') @@ -79,6 +84,16 @@ def _create_valid_basket(self): basket.thaw() return basket + def _prepare_basket_for_voucher_validation_tests(self, voucher_start_date, voucher_end_date): + """ Prepares basket for voucher validation """ + basket = Basket.objects.create(site=self.site, owner=self.user) + voucher, product = prepare_voucher(start_datetime=voucher_start_date, end_datetime=voucher_end_date) + basket.strategy = Selector().strategy() + basket.add_product(product) + basket.vouchers.add(voucher) + basket.thaw() + return basket + def assert_basket_retrieval_error(self, basket_id): error_msg = 'There was a problem retrieving your basket. Refresh the page to try again.' return self._assert_basket_error(basket_id, error_msg) @@ -179,6 +194,59 @@ def test_field_error(self): errors = json.loads(response.content)['field_errors'] self.assertIn(field, errors) + @override_switch(VOUCHER_VALIDATION_BEFORE_PAYMENT, active=True) + @ddt.data( + (now() - datetime.timedelta(days=3), 400), + (now() + datetime.timedelta(days=3), 200)) + @ddt.unpack + def test_submit_view_fails_for_invalid_voucher(self, voucher_end_time, status_code): + """ Verify SubmitPaymentView fails if basket invalid voucher""" + # Create Basket and payment data + voucher_start_time = now() - datetime.timedelta(days=5) + basket = self._prepare_basket_for_voucher_validation_tests(voucher_start_time, voucher_end_time) + + data = self._generate_data(basket.id) + response = self.client.post(self.path, data) + + self.assertEqual(response.status_code, status_code) + self.assertEqual(response['content-type'], JSON) + + @override_switch(VOUCHER_VALIDATION_BEFORE_PAYMENT, active=True) + @mock.patch( + 'ecommerce.extensions.voucher.models.Voucher.is_available_to_user', + return_value=(False, None) + ) + def test_submit_view_fails_if_voucher_not_available(self, mock_is_available_to_user): + """ Verify SubmitPaymentView fails if basket voucher not available to student""" + # Create Basket and payment data + voucher_start_time = now() - datetime.timedelta(days=1) + voucher_end_time = now() + datetime.timedelta(days=3) + basket = self._prepare_basket_for_voucher_validation_tests(voucher_start_time, voucher_end_time) + + data = self._generate_data(basket.id) + response = self.client.post(self.path, data) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response['content-type'], JSON) + self.assertEqual(mock_is_available_to_user.call_count, 3) + + @override_switch(VOUCHER_VALIDATION_BEFORE_PAYMENT, active=False) + def test_successful_submit_view_with_voucher_switch_disabled(self): + """ + Temporary test to confirm the problem with SubmitPaymentView + Accepting an invalid voucher when the waffle switch is False. + This will be cleaned up in LEARNER-5719. + """ + voucher_start_time = now() - datetime.timedelta(days=5) + voucher_end_time = now() - datetime.timedelta(days=3) + basket = self._prepare_basket_for_voucher_validation_tests(voucher_start_time, voucher_end_time) + + data = self._generate_data(basket.id) + response = self.client.post(self.path, data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response['content-type'], JSON) + @ddt.ddt class CybersourceInterstitialViewTests(CybersourceNotificationTestsMixin, TestCase): diff --git a/ecommerce/extensions/payment/tests/views/test_paypal.py b/ecommerce/extensions/payment/tests/views/test_paypal.py index 26c5a4367bf..5f5210f3b45 100644 --- a/ecommerce/extensions/payment/tests/views/test_paypal.py +++ b/ecommerce/extensions/payment/tests/views/test_paypal.py @@ -32,9 +32,11 @@ PaymentEvent = get_model('order', 'PaymentEvent') PaymentEventType = get_model('order', 'PaymentEventType') PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse') +Product = get_model('catalogue', 'Product') Selector = get_class('partner.strategy', 'Selector') SourceType = get_model('payment', 'SourceType') -Product = get_model('catalogue', 'Product') +Voucher = get_model('voucher', 'Voucher') + post_checkout = get_class('checkout.signals', 'post_checkout') diff --git a/ecommerce/extensions/payment/views/__init__.py b/ecommerce/extensions/payment/views/__init__.py index 71c73ab5989..a82a26ff7ca 100644 --- a/ecommerce/extensions/payment/views/__init__.py +++ b/ecommerce/extensions/payment/views/__init__.py @@ -2,6 +2,7 @@ import logging import six +import waffle from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.utils.decorators import method_decorator @@ -10,6 +11,7 @@ from oscar.core.loading import get_class, get_model from ecommerce.core.url_utils import get_lms_url +from ecommerce.extensions.payment.constants import VOUCHER_VALIDATION_BEFORE_PAYMENT from ecommerce.extensions.payment.forms import PaymentForm logger = logging.getLogger(__name__) @@ -65,7 +67,7 @@ def post(self, request): # pylint: disable=unused-argument form_kwargs = self.get_form_kwargs() form = self.form_class(**form_kwargs) - if form.is_valid(): + if form.is_valid() and self.check_valid_voucher(): return self.form_valid(form) else: return self.form_invalid(form) @@ -77,6 +79,27 @@ def get_form_kwargs(self): 'request': self.request, } + def check_valid_voucher(self): + """ + LEARNER-5603 Hot fix + Learners are able to bypass the basket views and pay using the + expired vouchers. This will disable students from doing so. + (https://github.com/django-oscar/django-oscar/blob/master/src/oscar/apps/order/utils.py#L68-L70) + # TODO: LEARNER-5719: Clean-up validation code as needed after further investigation. + """ + if waffle.switch_is_active(VOUCHER_VALIDATION_BEFORE_PAYMENT): + basket = self.request.basket + for voucher in basket.vouchers.select_for_update(): + available_to_user, __ = voucher.is_available_to_user(user=self.request.user) + if not available_to_user or not voucher.is_active(): + logger.info( + '[%s] basket was checked out with invalid voucher [%s]', + basket.id, + voucher.code, + ) + return False + return True + @abc.abstractmethod def form_valid(self, form): """ Perform payment processing after validating the form submission. """