From d140ffd868c2b0cf4401c191f5c5f05e1635ef77 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Tue, 20 Aug 2013 21:36:32 -0700 Subject: [PATCH] Start of tests for CyberSource processor --- .../shoppingcart/processors/CyberSource.py | 28 ++++---- .../shoppingcart/processors/__init__.py | 40 +---------- .../shoppingcart/processors/tests/__init__.py | 0 .../processors/tests/test_CyberSource.py | 69 +++++++++++++++++++ 4 files changed, 85 insertions(+), 52 deletions(-) create mode 100644 lms/djangoapps/shoppingcart/processors/tests/__init__.py create mode 100644 lms/djangoapps/shoppingcart/processors/tests/test_CyberSource.py diff --git a/lms/djangoapps/shoppingcart/processors/CyberSource.py b/lms/djangoapps/shoppingcart/processors/CyberSource.py index e7f593db4a..20b2b1bda8 100644 --- a/lms/djangoapps/shoppingcart/processors/CyberSource.py +++ b/lms/djangoapps/shoppingcart/processors/CyberSource.py @@ -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( """

@@ -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'), diff --git a/lms/djangoapps/shoppingcart/processors/__init__.py b/lms/djangoapps/shoppingcart/processors/__init__.py index 45a6e3114d..bbbbe41cde 100644 --- a/lms/djangoapps/shoppingcart/processors/__init__.py +++ b/lms/djangoapps/shoppingcart/processors/__init__.py @@ -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) - diff --git a/lms/djangoapps/shoppingcart/processors/tests/__init__.py b/lms/djangoapps/shoppingcart/processors/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource.py b/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource.py new file mode 100644 index 0000000000..0dc3887437 --- /dev/null +++ b/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource.py @@ -0,0 +1,69 @@ +""" +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) + +