Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
LEARNER-5603
Browse files Browse the repository at this point in the history
Hot fix to prevent students from using
expired coupons and submit payment for
baskets with fake vouchers

Moving this change under  waffle switch
  • Loading branch information
Ayub-Khan authored and Albert (AJ) St. Aubin committed Jul 5, 2018
1 parent f436e69 commit 0b10ad5
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 4 deletions.
2 changes: 2 additions & 0 deletions ecommerce/extensions/payment/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
72 changes: 70 additions & 2 deletions ecommerce/extensions/payment/tests/views/test_cybersource.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
""" Tests of the Payment Views. """
from __future__ import unicode_literals

import datetime
import json

import ddt
import mock
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
Expand All @@ -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

Expand All @@ -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')

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 3 additions & 1 deletion ecommerce/extensions/payment/tests/views/test_paypal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
25 changes: 24 additions & 1 deletion ecommerce/extensions/payment/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__)
Expand Down Expand Up @@ -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)
Expand All @@ -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. """
Expand Down

0 comments on commit 0b10ad5

Please sign in to comment.