Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
e0372b00
Commit
e0372b00
authored
Aug 28, 2013
by
Will Daly
Committed by
Jay Zoldak
Sep 03, 2013
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implemented fake payment page.
parent
a52ca363
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
407 additions
and
0 deletions
+407
-0
lms/djangoapps/shoppingcart/tests/payment_fake.py
+229
-0
lms/djangoapps/shoppingcart/tests/test_payment_fake.py
+112
-0
lms/djangoapps/shoppingcart/urls.py
+7
-0
lms/envs/acceptance.py
+18
-0
lms/envs/test.py
+20
-0
lms/templates/shoppingcart/test/fake_payment_error.html
+9
-0
lms/templates/shoppingcart/test/fake_payment_page.html
+12
-0
No files found.
lms/djangoapps/shoppingcart/tests/payment_fake.py
0 → 100644
View file @
e0372b00
"""
Fake payment page for use in acceptance tests.
This view is enabled in the URLs by the feature flag `ENABLE_PAYMENT_FAKE`.
Note that you will still need to configure this view as the payment
processor endpoint in order for the shopping cart to use it:
settings.CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = "/shoppingcart/payment_fake"
You can configure the payment to indicate success or failure by sending a PUT
request to the view with param "success"
set to "success" or "failure". The view defaults to payment success.
"""
from
django.views.generic.base
import
View
from
django.views.decorators.csrf
import
csrf_exempt
from
django.http
import
HttpResponse
,
HttpResponseBadRequest
from
mitxmako.shortcuts
import
render_to_response
# We use the same hashing function as the software under test,
# because it mainly uses standard libraries, and I want
# to avoid duplicating that code.
from
shoppingcart.processors.CyberSource
import
processor_hash
class
PaymentFakeView
(
View
):
"""
Fake payment page for use in acceptance tests.
"""
# We store the payment status to respond with in a class
# variable. In a multi-process Django app, this wouldn't work,
# since processes don't share memory. Since Lettuce
# runs one Django server process, this works for acceptance testing.
PAYMENT_STATUS_RESPONSE
=
"success"
@csrf_exempt
def
dispatch
(
self
,
*
args
,
**
kwargs
):
"""
Disable CSRF for these methods.
"""
return
super
(
PaymentFakeView
,
self
)
.
dispatch
(
*
args
,
**
kwargs
)
def
post
(
self
,
request
):
"""
Render a fake payment page.
This is an HTML form that:
* Triggers a POST to `postpay_callback()` on submit.
* Has hidden fields for all the data CyberSource sends to the callback.
- Most of this data is duplicated from the request POST params (e.g. `amount` and `course_id`)
- Other params contain fake data (always the same user name and address.
- Still other params are calculated (signatures)
* Serves an error page (HTML) with a 200 status code
if the signatures are invalid. This is what CyberSource does.
Since all the POST requests are triggered by HTML forms, this is
equivalent to the CyberSource payment page, even though it's
served by the shopping cart app.
"""
if
self
.
_is_signature_valid
(
request
.
POST
):
return
self
.
_payment_page_response
(
request
.
POST
,
'/postpay_callback'
)
else
:
return
render_to_response
(
'shoppingcart/test/fake_payment_error.html'
)
def
put
(
self
,
request
):
"""
Set the status of payment requests to success or failure.
Accepts one POST param "status" that can be either "success"
or "failure".
"""
new_status
=
request
.
body
if
not
new_status
in
[
"success"
,
"failure"
]:
return
HttpResponseBadRequest
()
else
:
# Configure all views to respond with the new status
PaymentFakeView
.
PAYMENT_STATUS_RESPONSE
=
new_status
return
HttpResponse
()
@staticmethod
def
_is_signature_valid
(
post_params
):
"""
Return a bool indicating whether the client sent
us a valid signature in the payment page request.
"""
# Calculate the fields signature
fields_sig
=
processor_hash
(
post_params
.
get
(
'orderPage_signedFields'
))
# Retrieve the list of signed fields
signed_fields
=
post_params
.
get
(
'orderPage_signedFields'
)
.
split
(
','
)
# Calculate the public signature
hash_val
=
","
.
join
([
"{0}={1}"
.
format
(
key
,
post_params
[
key
])
for
key
in
signed_fields
])
+
",signedFieldsPublicSignature={0}"
.
format
(
fields_sig
)
public_sig
=
processor_hash
(
hash_val
)
return
public_sig
==
post_params
.
get
(
'orderPage_signaturePublic'
)
@classmethod
def
response_post_params
(
cls
,
post_params
):
"""
Calculate the POST params we want to send back to the client.
"""
resp_params
=
{
# Indicate whether the payment was successful
"decision"
:
"ACCEPT"
if
cls
.
PAYMENT_STATUS_RESPONSE
==
"success"
else
"REJECT"
,
# Reflect back whatever the client sent us,
# defaulting to `None` if a paramter wasn't received
"course_id"
:
post_params
.
get
(
'course_id'
),
"orderAmount"
:
post_params
.
get
(
'amount'
),
"ccAuthReply_amount"
:
post_params
.
get
(
'amount'
),
"orderPage_transactionType"
:
post_params
.
get
(
'orderPage_transactionType'
),
"orderPage_serialNumber"
:
post_params
.
get
(
'orderPage_serialNumber'
),
"orderNumber"
:
post_params
.
get
(
'orderNumber'
),
"orderCurrency"
:
post_params
.
get
(
'currency'
),
"match"
:
post_params
.
get
(
'match'
),
"merchantID"
:
post_params
.
get
(
'merchantID'
),
# Send fake user data
"billTo_firstName"
:
"John"
,
"billTo_lastName"
:
"Doe"
,
"billTo_street1"
:
"123 Fake Street"
,
"billTo_state"
:
"MA"
,
"billTo_city"
:
"Boston"
,
"billTo_postalCode"
:
"02134"
,
"billTo_country"
:
"us"
,
# Send fake data for other fields
"card_cardType"
:
"001"
,
"card_accountNumber"
:
"############1111"
,
"card_expirationMonth"
:
"08"
,
"card_expirationYear"
:
"2019"
,
"paymentOption"
:
"card"
,
"orderPage_environment"
:
"TEST"
,
"orderPage_requestToken"
:
"unused"
,
"reconciliationID"
:
"39093601YKVO1I5D"
,
"ccAuthReply_authorizationCode"
:
"888888"
,
"ccAuthReply_avsCodeRaw"
:
"I1"
,
"reasonCode"
:
"100"
,
"requestID"
:
"3777139938170178147615"
,
"ccAuthReply_reasonCode"
:
"100"
,
"ccAuthReply_authorizedDateTime"
:
"2013-08-28T181954Z"
,
"ccAuthReply_processorResponse"
:
"100"
,
"ccAuthReply_avsCode"
:
"X"
,
# We don't use these signatures
"transactionSignature"
:
"unused="
,
"decision_publicSignature"
:
"unused="
,
"orderAmount_publicSignature"
:
"unused="
,
"orderNumber_publicSignature"
:
"unused="
,
"orderCurrency_publicSignature"
:
"unused="
,
}
# Indicate which fields we are including in the signature
# Order is important
signed_fields
=
[
'billTo_lastName'
,
'orderAmount'
,
'course_id'
,
'billTo_street1'
,
'card_accountNumber'
,
'orderAmount_publicSignature'
,
'orderPage_serialNumber'
,
'orderCurrency'
,
'reconciliationID'
,
'decision'
,
'ccAuthReply_processorResponse'
,
'billTo_state'
,
'billTo_firstName'
,
'card_expirationYear'
,
'billTo_city'
,
'billTo_postalCode'
,
'orderPage_requestToken'
,
'ccAuthReply_amount'
,
'orderCurrency_publicSignature'
,
'orderPage_transactionType'
,
'ccAuthReply_authorizationCode'
,
'decision_publicSignature'
,
'match'
,
'ccAuthReply_avsCodeRaw'
,
'paymentOption'
,
'billTo_country'
,
'reasonCode'
,
'ccAuthReply_reasonCode'
,
'orderPage_environment'
,
'card_expirationMonth'
,
'merchantID'
,
'orderNumber_publicSignature'
,
'requestID'
,
'orderNumber'
,
'ccAuthReply_authorizedDateTime'
,
'card_cardType'
,
'ccAuthReply_avsCode'
]
# Add the list of signed fields
resp_params
[
'signedFields'
]
=
","
.
join
(
signed_fields
)
# Calculate the fields signature
signed_fields_sig
=
processor_hash
(
resp_params
[
'signedFields'
])
# Calculate the public signature
hash_val
=
","
.
join
([
"{0}={1}"
.
format
(
key
,
resp_params
[
key
])
for
key
in
signed_fields
])
+
",signedFieldsPublicSignature={0}"
.
format
(
signed_fields_sig
)
resp_params
[
'signedDataPublicSignature'
]
=
processor_hash
(
hash_val
)
return
resp_params
def
_payment_page_response
(
self
,
post_params
,
callback_url
):
"""
Render the payment page to a response. This is an HTML form
that triggers a POST request to `callback_url`.
The POST params are described in the CyberSource documentation:
http://apps.cybersource.com/library/documentation/dev_guides/HOP_UG/html/wwhelp/wwhimpl/js/html/wwhelp.htm
To figure out the POST params to send to the callback,
we either:
1) Use fake static data (e.g. always send user name "John Doe")
2) Use the same info we received (e.g. send the same `course_id` and `amount`)
3) Dynamically calculate signatures using a shared secret
"""
# Build the context dict used to render the HTML form,
# filling in values for the hidden input fields.
# These will be sent in the POST request to the callback URL.
context_dict
=
{
# URL to send the POST request to
"callback_url"
:
callback_url
,
# POST params embedded in the HTML form
'post_params'
:
self
.
response_post_params
(
post_params
)
}
return
render_to_response
(
'shoppingcart/test/fake_payment_page.html'
,
context_dict
)
lms/djangoapps/shoppingcart/tests/test_payment_fake.py
0 → 100644
View file @
e0372b00
"""
Tests for the fake payment page used in acceptance tests.
"""
from
django.test
import
TestCase
from
shoppingcart.processors.CyberSource
import
sign
,
verify_signatures
,
\
CCProcessorSignatureException
from
shoppingcart.tests.payment_fake
import
PaymentFakeView
from
collections
import
OrderedDict
class
PaymentFakeViewTest
(
TestCase
):
"""
Test that the fake payment view interacts
correctly with the shopping cart.
"""
CLIENT_POST_PARAMS
=
OrderedDict
([
(
'match'
,
'on'
),
(
'course_id'
,
'edx/999/2013_Spring'
),
(
'amount'
,
'25.00'
),
(
'currency'
,
'usd'
),
(
'orderPage_transactionType'
,
'sale'
),
(
'orderNumber'
,
'33'
),
(
'merchantID'
,
'edx'
),
(
'djch'
,
'012345678912'
),
(
'orderPage_version'
,
2
),
(
'orderPage_serialNumber'
,
'1234567890'
),
])
def
setUp
(
self
):
super
(
PaymentFakeViewTest
,
self
)
.
setUp
()
# Reset the view state
PaymentFakeView
.
PAYMENT_STATUS_RESPONSE
=
"success"
def
test_accepts_client_signatures
(
self
):
# Generate shoppingcart signatures
post_params
=
sign
(
self
.
CLIENT_POST_PARAMS
)
# Simulate a POST request from the payment workflow
# page to the fake payment page.
resp
=
self
.
client
.
post
(
'/shoppingcart/payment_fake'
,
dict
(
post_params
)
)
# Expect that the response was successful
self
.
assertEqual
(
resp
.
status_code
,
200
)
# Expect that we were served the payment page
# (not the error page)
self
.
assertIn
(
"Payment Form"
,
resp
.
content
)
def
test_rejects_invalid_signature
(
self
):
# Generate shoppingcart signatures
post_params
=
sign
(
self
.
CLIENT_POST_PARAMS
)
# Tamper with the signature
post_params
[
'orderPage_signaturePublic'
]
=
"invalid"
# Simulate a POST request from the payment workflow
# page to the fake payment page.
resp
=
self
.
client
.
post
(
'/shoppingcart/payment_fake'
,
dict
(
post_params
)
)
# Expect that we got an error
self
.
assertIn
(
"Error"
,
resp
.
content
)
def
test_sends_valid_signature
(
self
):
# Generate shoppingcart signatures
post_params
=
sign
(
self
.
CLIENT_POST_PARAMS
)
# Get the POST params that the view would send back to us
resp_params
=
PaymentFakeView
.
response_post_params
(
post_params
)
# Check that the client accepts these
try
:
verify_signatures
(
resp_params
)
except
CCProcessorSignatureException
:
self
.
fail
(
"Client rejected signatures."
)
def
test_set_payment_status
(
self
):
# Generate shoppingcart signatures
post_params
=
sign
(
self
.
CLIENT_POST_PARAMS
)
# Configure the view to fail payments
resp
=
self
.
client
.
put
(
'/shoppingcart/payment_fake'
,
data
=
"failure"
,
content_type
=
'text/plain'
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
# Check that the decision is "REJECT"
resp_params
=
PaymentFakeView
.
response_post_params
(
post_params
)
self
.
assertEqual
(
resp_params
.
get
(
'decision'
),
'REJECT'
)
# Configure the view to accept payments
resp
=
self
.
client
.
put
(
'/shoppingcart/payment_fake'
,
data
=
"success"
,
content_type
=
'text/plain'
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
# Check that the decision is "ACCEPT"
resp_params
=
PaymentFakeView
.
response_post_params
(
post_params
)
self
.
assertEqual
(
resp_params
.
get
(
'decision'
),
'ACCEPT'
)
lms/djangoapps/shoppingcart/urls.py
View file @
e0372b00
...
...
@@ -13,3 +13,10 @@ if settings.MITX_FEATURES['ENABLE_SHOPPING_CART']:
url
(
r'^remove_item/$'
,
'remove_item'
),
url
(
r'^add/course/(?P<course_id>[^/]+/[^/]+/[^/]+)/$'
,
'add_course_to_cart'
,
name
=
'add_course_to_cart'
),
)
if
settings
.
MITX_FEATURES
.
get
(
'ENABLE_PAYMENT_FAKE'
):
from
shoppingcart.tests.payment_fake
import
PaymentFakeView
urlpatterns
+=
patterns
(
'shoppingcart.tests.payment_fake'
,
url
(
r'^payment_fake'
,
PaymentFakeView
.
as_view
())
)
lms/envs/acceptance.py
View file @
e0372b00
...
...
@@ -19,6 +19,7 @@ import logging
logging
.
disable
(
logging
.
ERROR
)
import
os
from
random
import
choice
,
randint
import
string
def
seed
():
...
...
@@ -83,6 +84,23 @@ MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
# Use the auto_auth workflow for creating users and logging them in
MITX_FEATURES
[
'AUTOMATIC_AUTH_FOR_TESTING'
]
=
True
# Enable fake payment processing page
MITX_FEATURES
[
'ENABLE_PAYMENT_FAKE'
]
=
True
# Configure the payment processor to use the fake processing page
# Since both the fake payment page and the shoppingcart app are using
# the same settings, we can generate this randomly and guarantee
# that they are using the same secret.
RANDOM_SHARED_SECRET
=
''
.
join
(
choice
(
string
.
letters
+
string
.
digits
+
string
.
punctuation
)
for
x
in
range
(
250
)
)
CC_PROCESSOR
[
'CyberSource'
][
'SHARED_SECRET'
]
=
RANDOM_SHARED_SECRET
CC_PROCESSOR
[
'CyberSource'
][
'MERCHANT_ID'
]
=
"edx"
CC_PROCESSOR
[
'CyberSource'
][
'SERIAL_NUMBER'
]
=
"0123456789012345678901"
CC_PROCESSOR
[
'CyberSource'
][
'PURCHASE_ENDPOINT'
]
=
"/shoppingcart/payment_fake"
# HACK
# Setting this flag to false causes imports to not load correctly in the lettuce python files
# We do not yet understand why this occurs. Setting this to true is a stopgap measure
...
...
lms/envs/test.py
View file @
e0372b00
...
...
@@ -155,6 +155,26 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True
OPENID_USE_AS_ADMIN_LOGIN
=
False
OPENID_PROVIDER_TRUSTED_ROOTS
=
[
'*'
]
###################### Payment ##############################3
# Enable fake payment processing page
MITX_FEATURES
[
'ENABLE_PAYMENT_FAKE'
]
=
True
# Configure the payment processor to use the fake processing page
# Since both the fake payment page and the shoppingcart app are using
# the same settings, we can generate this randomly and guarantee
# that they are using the same secret.
from
random
import
choice
import
string
RANDOM_SHARED_SECRET
=
''
.
join
(
choice
(
string
.
letters
+
string
.
digits
+
string
.
punctuation
)
for
x
in
range
(
250
)
)
CC_PROCESSOR
[
'CyberSource'
][
'SHARED_SECRET'
]
=
RANDOM_SHARED_SECRET
CC_PROCESSOR
[
'CyberSource'
][
'MERCHANT_ID'
]
=
"edx"
CC_PROCESSOR
[
'CyberSource'
][
'SERIAL_NUMBER'
]
=
"0123456789012345678901"
CC_PROCESSOR
[
'CyberSource'
][
'PURCHASE_ENDPOINT'
]
=
"/shoppingcart/payment_fake"
################################# CELERY ######################################
CELERY_ALWAYS_EAGER
=
True
...
...
lms/templates/shoppingcart/test/fake_payment_error.html
0 → 100644
View file @
e0372b00
<html>
<head>
<title>
Payment Error
</title>
</head>
<body>
<p>
An error occurred while you submitted your order.
If you are trying to make a purchase, please contact the merchant.
</p>
</body>
</html>
lms/templates/shoppingcart/test/fake_payment_page.html
0 → 100644
View file @
e0372b00
<html>
<head><title>
Payment Form
</title></head>
<body>
<p>
Payment page
</p>
<form
name=
"input"
action=
"${callback_url}"
method=
"post"
>
% for name, value in post_params.items():
<input
type=
"hidden"
name=
"${name}"
value=
"${value}"
>
% endfor
<input
type=
"submit"
value=
"Submit"
>
</form>
</body>
</html>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment