100% coverage on CyberSource.py
This commit is contained in:
@@ -45,7 +45,7 @@ def process_postpay_callback(params):
|
||||
except CCProcessorException as e:
|
||||
return {'success': False,
|
||||
'order': None, #due to exception we may not have the order
|
||||
'error_html': get_processor_exception_html(params, e)}
|
||||
'error_html': get_processor_exception_html(e)}
|
||||
|
||||
|
||||
def hash(value):
|
||||
@@ -57,7 +57,7 @@ def hash(value):
|
||||
return binascii.b2a_base64(hash_obj.digest())[:-1] # last character is a '\n', which we don't want
|
||||
|
||||
|
||||
def sign(params):
|
||||
def sign(params, signed_fields_key='orderPage_signedFields', full_sig_key='orderPage_signaturePublic'):
|
||||
"""
|
||||
params needs to be an ordered dict, b/c cybersource documentation states that order is important.
|
||||
Reverse engineered from PHP version provided by cybersource
|
||||
@@ -74,8 +74,8 @@ def sign(params):
|
||||
values = ",".join(["{0}={1}".format(i,params[i]) for i in params.keys()])
|
||||
fields_sig = hash(fields)
|
||||
values += ",signedFieldsPublicSignature=" + fields_sig
|
||||
params['orderPage_signaturePublic'] = hash(values)
|
||||
params['orderPage_signedFields'] = fields
|
||||
params[full_sig_key] = hash(values)
|
||||
params[signed_fields_key] = fields
|
||||
|
||||
return params
|
||||
|
||||
@@ -97,7 +97,7 @@ def verify_signatures(params, signed_fields_key='signedFields', full_sig_key='si
|
||||
raise CCProcessorSignatureException()
|
||||
|
||||
|
||||
def render_purchase_form_html(cart, user):
|
||||
def render_purchase_form_html(cart):
|
||||
"""
|
||||
Renders the HTML of the hidden POST form that must be used to initiate a purchase with CyberSource
|
||||
"""
|
||||
@@ -111,14 +111,6 @@ def render_purchase_form_html(cart, user):
|
||||
params['currency'] = cart.currency
|
||||
params['orderPage_transactionType'] = 'sale'
|
||||
params['orderNumber'] = "{0:d}".format(cart.id)
|
||||
idx=1
|
||||
for item in cart_items:
|
||||
prefix = "item_{0:d}_".format(idx)
|
||||
params[prefix+'productSKU'] = "{0:d}".format(item.id)
|
||||
params[prefix+'quantity'] = item.qty
|
||||
params[prefix+'productName'] = item.line_desc
|
||||
params[prefix+'unitPrice'] = item.unit_cost
|
||||
params[prefix+'taxAmount'] = "0.00"
|
||||
signed_param_dict = sign(params)
|
||||
|
||||
return render_to_string('shoppingcart/cybersource_form.html', {
|
||||
@@ -179,14 +171,14 @@ def payment_accepted(params):
|
||||
else:
|
||||
raise CCProcessorWrongAmountException(
|
||||
_("The amount charged by the processor {0} {1} is different than the total cost of the order {2} {3}."\
|
||||
.format(valid_params['ccAuthReply_amount'], valid_params['orderCurrency'],
|
||||
.format(charged_amt, valid_params['orderCurrency'],
|
||||
order.total_cost, order.currency))
|
||||
)
|
||||
else:
|
||||
return {'accepted': False,
|
||||
'amt_charged': 0,
|
||||
'currency': 'usd',
|
||||
'order': None}
|
||||
'order': order}
|
||||
|
||||
|
||||
def record_purchase(params, order):
|
||||
@@ -236,7 +228,7 @@ def get_processor_decline_html(params):
|
||||
email=payment_support_email)
|
||||
|
||||
|
||||
def get_processor_exception_html(params, exception):
|
||||
def get_processor_exception_html(exception):
|
||||
"""Return error HTML associated with exception"""
|
||||
|
||||
payment_support_email = settings.PAYMENT_SUPPORT_EMAIL
|
||||
@@ -267,10 +259,11 @@ def get_processor_exception_html(params, exception):
|
||||
<p class="error_msg">
|
||||
Sorry! Our payment processor sent us back a corrupted message regarding your charge, so we are
|
||||
unable to validate that the message actually came from the payment processor.
|
||||
The specific error message is: <span class="exception_msg">{msg}</span>.
|
||||
We apologize that we cannot verify whether the charge went through and take further action on your order.
|
||||
Your credit card may possibly have been charged. Contact us with payment-specific questions at {email}.
|
||||
</p>
|
||||
""".format(email=payment_support_email)))
|
||||
""".format(msg=exception.message, email=payment_support_email)))
|
||||
return msg
|
||||
|
||||
# fallthrough case, which basically never happens
|
||||
|
||||
@@ -5,8 +5,12 @@ from collections import OrderedDict
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from student.tests.factories import UserFactory
|
||||
from shoppingcart.models import Order, OrderItem
|
||||
from shoppingcart.processors.CyberSource import *
|
||||
from shoppingcart.processors.exceptions import CCProcessorSignatureException
|
||||
from shoppingcart.processors.exceptions import *
|
||||
from mock import patch, Mock
|
||||
|
||||
|
||||
TEST_CC_PROCESSOR = {
|
||||
'CyberSource' : {
|
||||
@@ -25,8 +29,8 @@ class CyberSourceTests(TestCase):
|
||||
pass
|
||||
|
||||
def test_override_settings(self):
|
||||
self.assertEquals(settings.CC_PROCESSOR['CyberSource']['MERCHANT_ID'], 'edx_test')
|
||||
self.assertEquals(settings.CC_PROCESSOR['CyberSource']['SHARED_SECRET'], 'secret')
|
||||
self.assertEqual(settings.CC_PROCESSOR['CyberSource']['MERCHANT_ID'], 'edx_test')
|
||||
self.assertEqual(settings.CC_PROCESSOR['CyberSource']['SHARED_SECRET'], 'secret')
|
||||
|
||||
def test_hash(self):
|
||||
"""
|
||||
@@ -66,4 +70,218 @@ class CyberSourceTests(TestCase):
|
||||
with self.assertRaises(CCProcessorSignatureException):
|
||||
verify_signatures(params)
|
||||
|
||||
def test_get_processor_decline_html(self):
|
||||
"""
|
||||
Tests the processor decline html message
|
||||
"""
|
||||
DECISION = 'REJECT'
|
||||
for code, reason in REASONCODE_MAP.iteritems():
|
||||
params={
|
||||
'decision': DECISION,
|
||||
'reasonCode': code,
|
||||
}
|
||||
html = get_processor_decline_html(params)
|
||||
self.assertIn(DECISION, html)
|
||||
self.assertIn(reason, html)
|
||||
self.assertIn(code, html)
|
||||
self.assertIn(settings.PAYMENT_SUPPORT_EMAIL, html)
|
||||
|
||||
def test_get_processor_exception_html(self):
|
||||
"""
|
||||
Tests the processor exception html message
|
||||
"""
|
||||
for type in [CCProcessorSignatureException, CCProcessorWrongAmountException, CCProcessorDataException]:
|
||||
error_msg = "An exception message of with exception type {0}".format(str(type))
|
||||
exception = type(error_msg)
|
||||
html = get_processor_exception_html(exception)
|
||||
self.assertIn(settings.PAYMENT_SUPPORT_EMAIL, html)
|
||||
self.assertIn('Sorry!', html)
|
||||
self.assertIn(error_msg, html)
|
||||
|
||||
# test base case
|
||||
self.assertIn("EXCEPTION!", get_processor_exception_html(CCProcessorException()))
|
||||
|
||||
def test_record_purchase(self):
|
||||
"""
|
||||
Tests record_purchase with good and without returned CCNum
|
||||
"""
|
||||
student1 = UserFactory()
|
||||
student1.save()
|
||||
student2 = UserFactory()
|
||||
student2.save()
|
||||
params_cc = {'card_accountNumber':'1234', 'card_cardType':'001', 'billTo_firstName':student1.first_name}
|
||||
params_nocc = {'card_accountNumber':'', 'card_cardType':'002', 'billTo_firstName':student2.first_name}
|
||||
order1 = Order.get_cart_for_user(student1)
|
||||
order2 = Order.get_cart_for_user(student2)
|
||||
record_purchase(params_cc, order1)
|
||||
record_purchase(params_nocc, order2)
|
||||
self.assertEqual(order1.bill_to_ccnum, '1234')
|
||||
self.assertEqual(order1.bill_to_cardtype, 'Visa')
|
||||
self.assertEqual(order1.bill_to_first, student1.first_name)
|
||||
self.assertEqual(order1.status, 'purchased')
|
||||
|
||||
order2 = Order.objects.get(user=student2)
|
||||
self.assertEqual(order2.bill_to_ccnum, '####')
|
||||
self.assertEqual(order2.bill_to_cardtype, 'MasterCard')
|
||||
self.assertEqual(order2.bill_to_first, student2.first_name)
|
||||
self.assertEqual(order2.status, 'purchased')
|
||||
|
||||
def test_payment_accepted_invalid_dict(self):
|
||||
"""
|
||||
Tests exception is thrown when params to payment_accepted don't have required key
|
||||
or have an bad value
|
||||
"""
|
||||
baseline = {
|
||||
'orderNumber': '1',
|
||||
'orderCurrency': 'usd',
|
||||
'decision': 'ACCEPT',
|
||||
}
|
||||
wrong = {
|
||||
'orderNumber': 'k',
|
||||
}
|
||||
# tests for missing key
|
||||
for key in baseline:
|
||||
params = baseline.copy()
|
||||
del params[key]
|
||||
with self.assertRaises(CCProcessorDataException):
|
||||
payment_accepted(params)
|
||||
|
||||
# tests for keys with value that can't be converted to proper type
|
||||
for key in wrong:
|
||||
params = baseline.copy()
|
||||
params[key] = wrong[key]
|
||||
with self.assertRaises(CCProcessorDataException):
|
||||
payment_accepted(params)
|
||||
|
||||
def test_payment_accepted_order(self):
|
||||
"""
|
||||
Tests payment_accepted cases with an order
|
||||
"""
|
||||
student1 = UserFactory()
|
||||
student1.save()
|
||||
|
||||
order1 = Order.get_cart_for_user(student1)
|
||||
params = {
|
||||
'card_accountNumber': '1234',
|
||||
'card_cardType': '001',
|
||||
'billTo_firstName': student1.first_name,
|
||||
'orderNumber': str(order1.id),
|
||||
'orderCurrency': 'usd',
|
||||
'decision': 'ACCEPT',
|
||||
'ccAuthReply_amount': '0.00'
|
||||
}
|
||||
|
||||
# tests for an order number that doesn't match up
|
||||
params_bad_ordernum = params.copy()
|
||||
params_bad_ordernum['orderNumber'] = str(order1.id+10)
|
||||
with self.assertRaises(CCProcessorDataException):
|
||||
payment_accepted(params_bad_ordernum)
|
||||
|
||||
# tests for a reply amount of the wrong type
|
||||
params_wrong_type_amt = params.copy()
|
||||
params_wrong_type_amt['ccAuthReply_amount'] = 'ab'
|
||||
with self.assertRaises(CCProcessorDataException):
|
||||
payment_accepted(params_wrong_type_amt)
|
||||
|
||||
# tests for a reply amount of the wrong type
|
||||
params_wrong_amt = params.copy()
|
||||
params_wrong_amt['ccAuthReply_amount'] = '1.00'
|
||||
with self.assertRaises(CCProcessorWrongAmountException):
|
||||
payment_accepted(params_wrong_amt)
|
||||
|
||||
# tests for a not accepted order
|
||||
params_not_accepted = params.copy()
|
||||
params_not_accepted['decision'] = "REJECT"
|
||||
self.assertFalse(payment_accepted(params_not_accepted)['accepted'])
|
||||
|
||||
# finally, tests an accepted order
|
||||
self.assertTrue(payment_accepted(params)['accepted'])
|
||||
|
||||
@patch('shoppingcart.processors.CyberSource.render_to_string', autospec=True)
|
||||
def test_render_purchase_form_html(self, render):
|
||||
"""
|
||||
Tests the rendering of the purchase form
|
||||
"""
|
||||
student1 = UserFactory()
|
||||
student1.save()
|
||||
|
||||
order1 = Order.get_cart_for_user(student1)
|
||||
item1 = OrderItem(order=order1, user=student1, unit_cost=1.0, line_cost=1.0)
|
||||
item1.save()
|
||||
html = render_purchase_form_html(order1)
|
||||
((template, context), render_kwargs) = render.call_args
|
||||
|
||||
self.assertEqual(template, 'shoppingcart/cybersource_form.html')
|
||||
self.assertDictContainsSubset({'amount': '1.00',
|
||||
'currency': 'usd',
|
||||
'orderPage_transactionType': 'sale',
|
||||
'orderNumber':str(order1.id)},
|
||||
context['params'])
|
||||
|
||||
def test_process_postpay_exception(self):
|
||||
"""
|
||||
Tests the exception path of process_postpay_callback
|
||||
"""
|
||||
baseline = {
|
||||
'orderNumber': '1',
|
||||
'orderCurrency': 'usd',
|
||||
'decision': 'ACCEPT',
|
||||
}
|
||||
# tests for missing key
|
||||
for key in baseline:
|
||||
params = baseline.copy()
|
||||
del params[key]
|
||||
result = process_postpay_callback(params)
|
||||
self.assertFalse(result['success'])
|
||||
self.assertIsNone(result['order'])
|
||||
self.assertIn('error_msg', result['error_html'])
|
||||
|
||||
@patch('shoppingcart.processors.CyberSource.verify_signatures', Mock(return_value=True))
|
||||
def test_process_postpay_accepted(self):
|
||||
"""
|
||||
Tests the ACCEPTED path of process_postpay
|
||||
"""
|
||||
student1 = UserFactory()
|
||||
student1.save()
|
||||
|
||||
order1 = Order.get_cart_for_user(student1)
|
||||
params = {
|
||||
'card_accountNumber': '1234',
|
||||
'card_cardType': '001',
|
||||
'billTo_firstName': student1.first_name,
|
||||
'orderNumber': str(order1.id),
|
||||
'orderCurrency': 'usd',
|
||||
'decision': 'ACCEPT',
|
||||
'ccAuthReply_amount': '0.00'
|
||||
}
|
||||
result = process_postpay_callback(params)
|
||||
self.assertTrue(result['success'])
|
||||
self.assertEqual(result['order'], order1)
|
||||
order1 = Order.objects.get(id=order1.id) # reload from DB to capture side-effect of process_postpay_callback
|
||||
self.assertEqual(order1.status, 'purchased')
|
||||
self.assertFalse(result['error_html'])
|
||||
|
||||
@patch('shoppingcart.processors.CyberSource.verify_signatures', Mock(return_value=True))
|
||||
def test_process_postpay_not_accepted(self):
|
||||
"""
|
||||
Tests the non-ACCEPTED path of process_postpay
|
||||
"""
|
||||
student1 = UserFactory()
|
||||
student1.save()
|
||||
|
||||
order1 = Order.get_cart_for_user(student1)
|
||||
params = {
|
||||
'card_accountNumber': '1234',
|
||||
'card_cardType': '001',
|
||||
'billTo_firstName': student1.first_name,
|
||||
'orderNumber': str(order1.id),
|
||||
'orderCurrency': 'usd',
|
||||
'decision': 'REJECT',
|
||||
'ccAuthReply_amount': '0.00',
|
||||
'reasonCode': '207'
|
||||
}
|
||||
result = process_postpay_callback(params)
|
||||
self.assertFalse(result['success'])
|
||||
self.assertEqual(result['order'], order1)
|
||||
self.assertEqual(order1.status, 'cart')
|
||||
self.assertIn(REASONCODE_MAP['207'], result['error_html'])
|
||||
@@ -47,7 +47,7 @@ def show_cart(request):
|
||||
total_cost = cart.total_cost
|
||||
amount = "{0:0.2f}".format(total_cost)
|
||||
cart_items = cart.orderitem_set.all()
|
||||
form_html = render_purchase_form_html(cart, request.user)
|
||||
form_html = render_purchase_form_html(cart)
|
||||
return render_to_response("shoppingcart/list.html",
|
||||
{'shoppingcart_items': cart_items,
|
||||
'amount': amount,
|
||||
|
||||
Reference in New Issue
Block a user