Commit 055ad535 by Jason Bau Committed by Diana Huang

100% coverage on CyberSource.py

parent d140ffd8
......@@ -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'])
\ No newline at end of file
......@@ -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,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment