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): ...@@ -45,7 +45,7 @@ def process_postpay_callback(params):
except CCProcessorException as e: except CCProcessorException as e:
return {'success': False, return {'success': False,
'order': None, #due to exception we may not have the order '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): def hash(value):
...@@ -57,7 +57,7 @@ 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 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. params needs to be an ordered dict, b/c cybersource documentation states that order is important.
Reverse engineered from PHP version provided by cybersource Reverse engineered from PHP version provided by cybersource
...@@ -74,8 +74,8 @@ def sign(params): ...@@ -74,8 +74,8 @@ def sign(params):
values = ",".join(["{0}={1}".format(i,params[i]) for i in params.keys()]) values = ",".join(["{0}={1}".format(i,params[i]) for i in params.keys()])
fields_sig = hash(fields) fields_sig = hash(fields)
values += ",signedFieldsPublicSignature=" + fields_sig values += ",signedFieldsPublicSignature=" + fields_sig
params['orderPage_signaturePublic'] = hash(values) params[full_sig_key] = hash(values)
params['orderPage_signedFields'] = fields params[signed_fields_key] = fields
return params return params
...@@ -97,7 +97,7 @@ def verify_signatures(params, signed_fields_key='signedFields', full_sig_key='si ...@@ -97,7 +97,7 @@ def verify_signatures(params, signed_fields_key='signedFields', full_sig_key='si
raise CCProcessorSignatureException() 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 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): ...@@ -111,14 +111,6 @@ def render_purchase_form_html(cart, user):
params['currency'] = cart.currency params['currency'] = cart.currency
params['orderPage_transactionType'] = 'sale' params['orderPage_transactionType'] = 'sale'
params['orderNumber'] = "{0:d}".format(cart.id) 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) signed_param_dict = sign(params)
return render_to_string('shoppingcart/cybersource_form.html', { return render_to_string('shoppingcart/cybersource_form.html', {
...@@ -179,14 +171,14 @@ def payment_accepted(params): ...@@ -179,14 +171,14 @@ def payment_accepted(params):
else: else:
raise CCProcessorWrongAmountException( raise CCProcessorWrongAmountException(
_("The amount charged by the processor {0} {1} is different than the total cost of the order {2} {3}."\ _("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)) order.total_cost, order.currency))
) )
else: else:
return {'accepted': False, return {'accepted': False,
'amt_charged': 0, 'amt_charged': 0,
'currency': 'usd', 'currency': 'usd',
'order': None} 'order': order}
def record_purchase(params, order): def record_purchase(params, order):
...@@ -236,7 +228,7 @@ def get_processor_decline_html(params): ...@@ -236,7 +228,7 @@ def get_processor_decline_html(params):
email=payment_support_email) email=payment_support_email)
def get_processor_exception_html(params, exception): def get_processor_exception_html(exception):
"""Return error HTML associated with exception""" """Return error HTML associated with exception"""
payment_support_email = settings.PAYMENT_SUPPORT_EMAIL payment_support_email = settings.PAYMENT_SUPPORT_EMAIL
...@@ -267,10 +259,11 @@ def get_processor_exception_html(params, exception): ...@@ -267,10 +259,11 @@ def get_processor_exception_html(params, exception):
<p class="error_msg"> <p class="error_msg">
Sorry! Our payment processor sent us back a corrupted message regarding your charge, so we are 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. 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. 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}. Your credit card may possibly have been charged. Contact us with payment-specific questions at {email}.
</p> </p>
""".format(email=payment_support_email))) """.format(msg=exception.message, email=payment_support_email)))
return msg return msg
# fallthrough case, which basically never happens # fallthrough case, which basically never happens
......
...@@ -5,8 +5,12 @@ from collections import OrderedDict ...@@ -5,8 +5,12 @@ from collections import OrderedDict
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import 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.CyberSource import *
from shoppingcart.processors.exceptions import CCProcessorSignatureException from shoppingcart.processors.exceptions import *
from mock import patch, Mock
TEST_CC_PROCESSOR = { TEST_CC_PROCESSOR = {
'CyberSource' : { 'CyberSource' : {
...@@ -25,8 +29,8 @@ class CyberSourceTests(TestCase): ...@@ -25,8 +29,8 @@ class CyberSourceTests(TestCase):
pass pass
def test_override_settings(self): def test_override_settings(self):
self.assertEquals(settings.CC_PROCESSOR['CyberSource']['MERCHANT_ID'], 'edx_test') self.assertEqual(settings.CC_PROCESSOR['CyberSource']['MERCHANT_ID'], 'edx_test')
self.assertEquals(settings.CC_PROCESSOR['CyberSource']['SHARED_SECRET'], 'secret') self.assertEqual(settings.CC_PROCESSOR['CyberSource']['SHARED_SECRET'], 'secret')
def test_hash(self): def test_hash(self):
""" """
...@@ -66,4 +70,218 @@ class CyberSourceTests(TestCase): ...@@ -66,4 +70,218 @@ class CyberSourceTests(TestCase):
with self.assertRaises(CCProcessorSignatureException): with self.assertRaises(CCProcessorSignatureException):
verify_signatures(params) 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): ...@@ -47,7 +47,7 @@ def show_cart(request):
total_cost = cart.total_cost total_cost = cart.total_cost
amount = "{0:0.2f}".format(total_cost) amount = "{0:0.2f}".format(total_cost)
cart_items = cart.orderitem_set.all() 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", return render_to_response("shoppingcart/list.html",
{'shoppingcart_items': cart_items, {'shoppingcart_items': cart_items,
'amount': amount, '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