Commit 0bf68e5f by Jim Abramson

Merge pull request #128 from edx/jsa/xcom-399

Fix errors when cybersource callbacks don't have "state"
parents 3a9ffe5e ab4ec244
...@@ -6,6 +6,7 @@ from django.conf import settings ...@@ -6,6 +6,7 @@ from django.conf import settings
import httpretty import httpretty
import mock import mock
from oscar.core.loading import get_model from oscar.core.loading import get_model
from oscar.test import newfactories
from suds.sudsobject import Factory from suds.sudsobject import Factory
from ecommerce.extensions.payment.constants import CARD_TYPES from ecommerce.extensions.payment.constants import CARD_TYPES
...@@ -77,6 +78,23 @@ class CybersourceMixin(object): ...@@ -77,6 +78,23 @@ class CybersourceMixin(object):
message = u','.join([u'{key}={value}'.format(key=key, value=data[key]) for key in keys]) message = u','.join([u'{key}={value}'.format(key=key, value=data[key]) for key in keys])
return sign(message, secret_key) return sign(message, secret_key)
def make_billing_address(self, overrides=None):
"""
Create a billing address for Cybersource tests with minimal required
fields defined.
"""
kwargs = {
'first_name': 'TestForename',
'last_name': 'TestSurname',
'line1': 'TestLine1',
'line2': '', # this is not required by Cybersource, so make it empty unless the caller overrides it.
'line4': 'TestLine4',
'postcode': 'TestPostCode',
'country': newfactories.CountryFactory(),
}
kwargs.update(overrides or {})
return newfactories.BillingAddressFactory(**kwargs)
def generate_notification(self, secret_key, basket, decision=u'ACCEPT', billing_address=None, auth_amount=None, def generate_notification(self, secret_key, basket, decision=u'ACCEPT', billing_address=None, auth_amount=None,
**kwargs): **kwargs):
""" Generates a dict containing the API reply fields expected to be received from CyberSource. """ """ Generates a dict containing the API reply fields expected to be received from CyberSource. """
...@@ -103,13 +121,14 @@ class CybersourceMixin(object): ...@@ -103,13 +121,14 @@ class CybersourceMixin(object):
u'req_bill_to_address_line1': billing_address.line1, u'req_bill_to_address_line1': billing_address.line1,
u'req_bill_to_address_city': billing_address.line4, u'req_bill_to_address_city': billing_address.line4,
u'req_bill_to_address_postal_code': billing_address.postcode, u'req_bill_to_address_postal_code': billing_address.postcode,
u'req_bill_to_address_state': billing_address.state,
u'req_bill_to_address_country': billing_address.country.iso_3166_1_a2 u'req_bill_to_address_country': billing_address.country.iso_3166_1_a2
}) })
# Address Line 2 is an optional response field # handle optional address fields
if billing_address.line2: if billing_address.line2:
notification[u'req_bill_to_address_line2'] = billing_address.line2 notification[u'req_bill_to_address_line2'] = billing_address.line2
if billing_address.state:
notification[u'req_bill_to_address_state'] = billing_address.state
notification[u'signed_field_names'] = u','.join(notification.keys()) notification[u'signed_field_names'] = u','.join(notification.keys())
notification[u'signature'] = self.generate_signature(secret_key, notification) notification[u'signature'] = self.generate_signature(secret_key, notification)
......
...@@ -37,7 +37,7 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase) ...@@ -37,7 +37,7 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase)
super(CybersourceNotifyViewTests, self).setUp() super(CybersourceNotifyViewTests, self).setUp()
self.user = factories.UserFactory() self.user = factories.UserFactory()
factories.UserAddressFactory(user=self.user) self.billing_address = self.make_billing_address()
self.basket = factories.create_basket() self.basket = factories.create_basket()
self.basket.owner = self.user self.basket.owner = self.user
...@@ -76,8 +76,11 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase) ...@@ -76,8 +76,11 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase)
# The basket should not have an associated order if no payment was made. # The basket should not have an associated order if no payment was made.
self.assertFalse(Order.objects.filter(basket=self.basket).exists()) self.assertFalse(Order.objects.filter(basket=self.basket).exists())
address = self.user.addresses.first() notification = self.generate_notification(
notification = self.generate_notification(self.processor.secret_key, self.basket, billing_address=address) self.processor.secret_key,
self.basket,
billing_address=self.billing_address,
)
with mock_signal_receiver(post_checkout) as receiver: with mock_signal_receiver(post_checkout) as receiver:
response = self.client.post(reverse('cybersource_notify'), notification) response = self.client.post(reverse('cybersource_notify'), notification)
...@@ -130,8 +133,11 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase) ...@@ -130,8 +133,11 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase)
def test_unable_to_place_order(self): def test_unable_to_place_order(self):
""" When payment is accepted, but an order cannot be placed, log an error and return HTTP 200. """ """ When payment is accepted, but an order cannot be placed, log an error and return HTTP 200. """
address = self.user.addresses.first() notification = self.generate_notification(
notification = self.generate_notification(self.processor.secret_key, self.basket, billing_address=address) self.processor.secret_key,
self.basket,
billing_address=self.billing_address,
)
with mock.patch.object(CybersourceNotifyView, 'handle_order_placement', side_effect=UnableToPlaceOrder): with mock.patch.object(CybersourceNotifyView, 'handle_order_placement', side_effect=UnableToPlaceOrder):
response = self.client.post(reverse('cybersource_notify'), notification) response = self.client.post(reverse('cybersource_notify'), notification)
...@@ -146,14 +152,56 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase) ...@@ -146,14 +152,56 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase)
def test_invalid_basket(self, basket_id): def test_invalid_basket(self, basket_id):
""" When payment is accepted for a non-existent basket, log an error and record the response. """ """ When payment is accepted for a non-existent basket, log an error and record the response. """
address = self.user.addresses.first() notification = self.generate_notification(
notification = self.generate_notification(self.processor.secret_key, self.basket, billing_address=address, self.processor.secret_key,
req_reference_number=basket_id) self.basket,
billing_address=self.billing_address,
req_reference_number=basket_id,
)
response = self.client.post(reverse('cybersource_notify'), notification) response = self.client.post(reverse('cybersource_notify'), notification)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assert_processor_response_recorded(self.processor_name, notification[u'transaction_id'], notification) self.assert_processor_response_recorded(self.processor_name, notification[u'transaction_id'], notification)
@ddt.data(('line2', 'foo'), ('state', 'bar'))
@ddt.unpack
@mock.patch('ecommerce.extensions.payment.views.CybersourceNotifyView.handle_order_placement')
def test_optional_fields(self, field_name, field_value, mock_placement_handler):
""" Ensure notifications are handled properly with or without keys/values present for optional fields. """
def check_notification_address(notification, expected_address):
response = self.client.post(reverse('cybersource_notify'), notification)
self.assertEqual(response.status_code, 200)
self.assertTrue(mock_placement_handler.called)
actual_address = mock_placement_handler.call_args[0][6]
self.assertEqual(actual_address.summary, expected_address.summary)
cybersource_key = 'req_bill_to_address_{}'.format(field_name)
# Generate a notification without the optional field set.
# Ensure that the Cybersource key does not exist in the notification,
# and that the address our endpoint parses from the notification is
# equivalent to the original.
notification = self.generate_notification(
self.processor.secret_key,
self.basket,
billing_address=self.billing_address,
)
self.assertNotIn(cybersource_key, notification)
check_notification_address(notification, self.billing_address)
# Add the optional field to the billing address in the notification.
# Ensure that the Cybersource key now does exist, and that our endpoint
# recognizes and parses it correctly.
billing_address = self.make_billing_address({field_name: field_value})
notification = self.generate_notification(
self.processor.secret_key,
self.basket,
billing_address=billing_address,
)
self.assertIn(cybersource_key, notification)
check_notification_address(notification, billing_address)
def test_invalid_signature(self): def test_invalid_signature(self):
""" """
If the response signature is invalid, the view should return a 400. The response should be recorded, but an If the response signature is invalid, the view should return a 400. The response should be recorded, but an
......
...@@ -47,7 +47,8 @@ class CybersourceNotifyView(EdxOrderPlacementMixin, View): ...@@ -47,7 +47,8 @@ class CybersourceNotifyView(EdxOrderPlacementMixin, View):
# Oscar uses line4 for city # Oscar uses line4 for city
line4=cybersource_response['req_bill_to_address_city'], line4=cybersource_response['req_bill_to_address_city'],
postcode=cybersource_response['req_bill_to_address_postal_code'], postcode=cybersource_response['req_bill_to_address_postal_code'],
state=cybersource_response['req_bill_to_address_state'], # State is optional
state=cybersource_response.get('req_bill_to_address_state', ''),
country=Country.objects.get( country=Country.objects.get(
iso_3166_1_a2=cybersource_response['req_bill_to_address_country'])) iso_3166_1_a2=cybersource_response['req_bill_to_address_country']))
......
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