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 ...@@ -17,13 +17,6 @@ from mitxmako.shortcuts import render_to_string
from shoppingcart.models import Order from shoppingcart.models import Order
from .exceptions import * 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): def process_postpay_callback(params):
""" """
The top level call to this module, basically The top level call to this module, basically
...@@ -59,6 +52,7 @@ def hash(value): ...@@ -59,6 +52,7 @@ def hash(value):
""" """
Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page 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) 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 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): ...@@ -68,6 +62,10 @@ def sign(params):
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
""" """
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['merchantID'] = merchant_id
params['orderPage_timestamp'] = int(time.time()*1000) params['orderPage_timestamp'] = int(time.time()*1000)
params['orderPage_version'] = orderPage_version params['orderPage_version'] = orderPage_version
...@@ -82,7 +80,7 @@ def sign(params): ...@@ -82,7 +80,7 @@ def sign(params):
return 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 Verify the signatures accompanying the POST back from Cybersource Hosted Order Page
...@@ -90,11 +88,11 @@ def verify_signatures(params): ...@@ -90,11 +88,11 @@ def verify_signatures(params):
raises CCProcessorSignatureException if not verified 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]) 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 data += ",signedFieldsPublicSignature=" + signed_fields_sig
returned_sig = params.get('signedDataPublicSignature','') returned_sig = params.get(full_sig_key, '')
if hash(data) != returned_sig: if hash(data) != returned_sig:
raise CCProcessorSignatureException() raise CCProcessorSignatureException()
...@@ -103,11 +101,12 @@ def render_purchase_form_html(cart, user): ...@@ -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 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 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()
params = OrderedDict() params = OrderedDict()
params['comment'] = 'Stanford OpenEdX Purchase'
params['amount'] = amount params['amount'] = amount
params['currency'] = cart.currency params['currency'] = cart.currency
params['orderPage_transactionType'] = 'sale' params['orderPage_transactionType'] = 'sale'
...@@ -217,6 +216,8 @@ def record_purchase(params, order): ...@@ -217,6 +216,8 @@ def record_purchase(params, order):
def get_processor_decline_html(params): def get_processor_decline_html(params):
"""Have to parse through the error codes to return a helpful message""" """Have to parse through the error codes to return a helpful message"""
payment_support_email = settings.PAYMENT_SUPPORT_EMAIL
msg = _(dedent( msg = _(dedent(
""" """
<p class="error_msg"> <p class="error_msg">
...@@ -238,6 +239,7 @@ def get_processor_decline_html(params): ...@@ -238,6 +239,7 @@ def get_processor_decline_html(params):
def get_processor_exception_html(params, exception): def get_processor_exception_html(params, exception):
"""Return error HTML associated with exception""" """Return error HTML associated with exception"""
payment_support_email = settings.PAYMENT_SUPPORT_EMAIL
if isinstance(exception, CCProcessorDataException): if isinstance(exception, CCProcessorDataException):
msg = _(dedent( msg = _(dedent(
""" """
...@@ -359,7 +361,7 @@ REASONCODE_MAP.update( ...@@ -359,7 +361,7 @@ REASONCODE_MAP.update(
'234' : _(dedent( '234' : _(dedent(
""" """
There is a problem with our CyberSource merchant configuration. Please let us know at {0} 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 # 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.'), '235' : _('The requested amount exceeds the originally authorized amount.'),
'236' : _('Processor Failure. Possible fix: retry the payment'), '236' : _('Processor Failure. Possible fix: retry the payment'),
......
...@@ -3,11 +3,7 @@ from django.conf import settings ...@@ -3,11 +3,7 @@ from django.conf import settings
### Now code that determines, using settings, which actual processor implementation we're using. ### Now code that determines, using settings, which actual processor implementation we're using.
processor_name = settings.CC_PROCESSOR.keys()[0] processor_name = settings.CC_PROCESSOR.keys()[0]
module = __import__('shoppingcart.processors.' + processor_name, module = __import__('shoppingcart.processors.' + processor_name,
fromlist=['sign', fromlist=['render_purchase_form_html'
'verify',
'render_purchase_form_html'
'payment_accepted',
'record_purchase',
'process_postpay_callback', 'process_postpay_callback',
]) ])
...@@ -34,37 +30,3 @@ def process_postpay_callback(*args, **kwargs): ...@@ -34,37 +30,3 @@ def process_postpay_callback(*args, **kwargs):
""" """
return module.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