Commit d140ffd8 by Jason Bau Committed by Diana Huang

Start of tests for CyberSource processor

parent 9fdf60ff
......@@ -17,13 +17,6 @@ from mitxmako.shortcuts import render_to_string
from shoppingcart.models import Order
from .exceptions import *
shared_secret = settings.CC_PROCESSOR['CyberSource'].get('SHARED_SECRET','')
merchant_id = settings.CC_PROCESSOR['CyberSource'].get('MERCHANT_ID','')
serial_number = settings.CC_PROCESSOR['CyberSource'].get('SERIAL_NUMBER','')
orderPage_version = settings.CC_PROCESSOR['CyberSource'].get('ORDERPAGE_VERSION','7')
purchase_endpoint = settings.CC_PROCESSOR['CyberSource'].get('PURCHASE_ENDPOINT','')
payment_support_email = settings.PAYMENT_SUPPORT_EMAIL
def process_postpay_callback(params):
"""
The top level call to this module, basically
......@@ -59,6 +52,7 @@ def hash(value):
"""
Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page
"""
shared_secret = settings.CC_PROCESSOR['CyberSource'].get('SHARED_SECRET','')
hash_obj = hmac.new(shared_secret, value, sha1)
return binascii.b2a_base64(hash_obj.digest())[:-1] # last character is a '\n', which we don't want
......@@ -68,6 +62,10 @@ def sign(params):
params needs to be an ordered dict, b/c cybersource documentation states that order is important.
Reverse engineered from PHP version provided by cybersource
"""
merchant_id = settings.CC_PROCESSOR['CyberSource'].get('MERCHANT_ID','')
orderPage_version = settings.CC_PROCESSOR['CyberSource'].get('ORDERPAGE_VERSION','7')
serial_number = settings.CC_PROCESSOR['CyberSource'].get('SERIAL_NUMBER','')
params['merchantID'] = merchant_id
params['orderPage_timestamp'] = int(time.time()*1000)
params['orderPage_version'] = orderPage_version
......@@ -82,7 +80,7 @@ def sign(params):
return params
def verify_signatures(params):
def verify_signatures(params, signed_fields_key='signedFields', full_sig_key='signedDataPublicSignature'):
"""
Verify the signatures accompanying the POST back from Cybersource Hosted Order Page
......@@ -90,11 +88,11 @@ def verify_signatures(params):
raises CCProcessorSignatureException if not verified
"""
signed_fields = params.get('signedFields', '').split(',')
signed_fields = params.get(signed_fields_key, '').split(',')
data = ",".join(["{0}={1}".format(k, params.get(k, '')) for k in signed_fields])
signed_fields_sig = hash(params.get('signedFields', ''))
signed_fields_sig = hash(params.get(signed_fields_key, ''))
data += ",signedFieldsPublicSignature=" + signed_fields_sig
returned_sig = params.get('signedDataPublicSignature','')
returned_sig = params.get(full_sig_key, '')
if hash(data) != returned_sig:
raise CCProcessorSignatureException()
......@@ -103,11 +101,12 @@ def render_purchase_form_html(cart, user):
"""
Renders the HTML of the hidden POST form that must be used to initiate a purchase with CyberSource
"""
purchase_endpoint = settings.CC_PROCESSOR['CyberSource'].get('PURCHASE_ENDPOINT','')
total_cost = cart.total_cost
amount = "{0:0.2f}".format(total_cost)
cart_items = cart.orderitem_set.all()
params = OrderedDict()
params['comment'] = 'Stanford OpenEdX Purchase'
params['amount'] = amount
params['currency'] = cart.currency
params['orderPage_transactionType'] = 'sale'
......@@ -217,6 +216,8 @@ def record_purchase(params, order):
def get_processor_decline_html(params):
"""Have to parse through the error codes to return a helpful message"""
payment_support_email = settings.PAYMENT_SUPPORT_EMAIL
msg = _(dedent(
"""
<p class="error_msg">
......@@ -238,6 +239,7 @@ def get_processor_decline_html(params):
def get_processor_exception_html(params, exception):
"""Return error HTML associated with exception"""
payment_support_email = settings.PAYMENT_SUPPORT_EMAIL
if isinstance(exception, CCProcessorDataException):
msg = _(dedent(
"""
......@@ -359,7 +361,7 @@ REASONCODE_MAP.update(
'234' : _(dedent(
"""
There is a problem with our CyberSource merchant configuration. Please let us know at {0}
""".format(payment_support_email))),
""".format(settings.PAYMENT_SUPPORT_EMAIL))),
# reason code 235 only applies if we are processing a capture through the API. so we should never see it
'235' : _('The requested amount exceeds the originally authorized amount.'),
'236' : _('Processor Failure. Possible fix: retry the payment'),
......
......@@ -3,11 +3,7 @@ from django.conf import settings
### Now code that determines, using settings, which actual processor implementation we're using.
processor_name = settings.CC_PROCESSOR.keys()[0]
module = __import__('shoppingcart.processors.' + processor_name,
fromlist=['sign',
'verify',
'render_purchase_form_html'
'payment_accepted',
'record_purchase',
fromlist=['render_purchase_form_html'
'process_postpay_callback',
])
......@@ -34,37 +30,3 @@ def process_postpay_callback(*args, **kwargs):
"""
return module.process_postpay_callback(*args, **kwargs)
def sign(*args, **kwargs):
"""
Given a dict (or OrderedDict) of parameters to send to the
credit card processor, signs them in the manner expected by
the processor
Returns a dict containing the signature
"""
return module.sign(*args, **kwargs)
def verify(*args, **kwargs):
"""
Given a dict (or OrderedDict) of parameters to returned by the
credit card processor, verifies them in the manner specified by
the processor
Returns a boolean
"""
return module.sign(*args, **kwargs)
def payment_accepted(*args, **kwargs):
"""
Given params returned by the CC processor, check that processor has accepted the payment
Returns a dict of {accepted:bool, amt_charged:float, currency:str, order:Order}
"""
return module.payment_accepted(*args, **kwargs)
def record_purchase(*args, **kwargs):
"""
Given params returned by the CC processor, record that the purchase has occurred in
the database and also run callbacks
"""
return module.record_purchase(*args, **kwargs)
"""
Tests for the CyberSource processor handler
"""
from collections import OrderedDict
from django.test import TestCase
from django.test.utils import override_settings
from django.conf import settings
from shoppingcart.processors.CyberSource import *
from shoppingcart.processors.exceptions import CCProcessorSignatureException
TEST_CC_PROCESSOR = {
'CyberSource' : {
'SHARED_SECRET': 'secret',
'MERCHANT_ID' : 'edx_test',
'SERIAL_NUMBER' : '12345',
'ORDERPAGE_VERSION': '7',
'PURCHASE_ENDPOINT': '',
}
}
@override_settings(CC_PROCESSOR=TEST_CC_PROCESSOR)
class CyberSourceTests(TestCase):
def setUp(self):
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')
def test_hash(self):
"""
Tests the hash function. Basically just hardcodes the answer.
"""
self.assertEqual(hash('test'), 'GqNJWF7X7L07nEhqMAZ+OVyks1Y=')
self.assertEqual(hash('edx '), '/KowheysqM2PFYuxVKg0P8Flfk4=')
def test_sign_then_verify(self):
"""
"loopback" test:
Tests the that the verify function verifies parameters signed by the sign function
"""
params = OrderedDict()
params['amount'] = "12.34"
params['currency'] = 'usd'
params['orderPage_transactionType'] = 'sale'
params['orderNumber'] = "567"
verify_signatures(sign(params), signed_fields_key='orderPage_signedFields',
full_sig_key='orderPage_signaturePublic')
# if the above verify_signature fails it will throw an exception, so basically we're just
# testing for the absence of that exception. the trivial assert below does that
self.assertEqual(1, 1)
def test_verify_exception(self):
"""
Tests that failure to verify raises the proper CCProcessorSignatureException
"""
params = OrderedDict()
params['a'] = 'A'
params['b'] = 'B'
params['signedFields'] = 'A,B'
params['signedDataPublicSignature'] = 'WONTVERIFY'
with self.assertRaises(CCProcessorSignatureException):
verify_signatures(params)
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