Mock testing Paypal's IPN with Rails
posted by gchatz 27 commentsIn the previous article Using Paypal with Rails we showed how to implement a Paypal form using some of the Rails magic.
What’s equally important to the actual form, is, well …testing it.
Transactions are about customer’s money so you can’t rely on point and click testing.
Up until the redirection of the user to the Paypal gateway, testing can be done like usual, using the build-in mechanisms Rails provides.
What you can’t test in an automated way is Paypal’s IPN call back.
And you can’t test it because Paypal’s sandbox is unreliable. It can fire the call back after 2 seconds or 2 hours or 2 years.
So instead of relying on IPN to ping back to us, we’ll consider it as a black box and mock its input-output.
If we predict all the possible outputs of IPN we are safe in all cases.
What we need is a mock Paypal lib.
1. Creating the mock lib
In our case (using the ActiveMerchant plugin) the controller method handling the IPN is the following:
def ipn
# Create a notify object we must
notify = Paypal::Notification.new(request.raw_post)
#we must make sure this transaction id is not allready completed
if !Trans.count("*", :conditions => ["paypal_transaction_id = ?", notify.transaction_id]).zero?
# do some logging here...
end
if notify.acknowledge
begin
if notify.complete?
#transaction complete.. add your business logic here
else
#Reason to be suspicious
end
rescue => e
#Houston we have a bug
ensure
#make sure we logged everything we must
end
else #transaction was not acknowledged
# another reason to be suspicious
end
render :nothing => true
end
And in flow terms:

Flowchart created with Gliffy
What we need to mock are the decisions that come from IPN:
- Acknowledgment. (notify.acknowledge)
- Completion. (notifiy.complete?)
The Paypal notification object that comes with ActiveMerchant is actually simple:
require 'net/http'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
module Integrations #:nodoc:
module Paypal
class Notification < ActiveMerchant::Billing::Integrations::Notification
include PostsData
# Was the transaction complete?
def complete?
status == "Completed"
end
def received_at
Time.parse params['payment_date']
end
def status
params['payment_status']
end
# Id of this transaction (paypal number)
def transaction_id
params['txn_id']
end
# What type of transaction are we dealing with?
# "cart" "send_money" "web_accept" are possible here.
def type
params['txn_type']
end
# the money amount we received in X.2 decimal.
def gross
params['mc_gross']
end
# the markup paypal charges for the transaction
def fee
params['mc_fee']
end
# What currency have we been dealing with
def currency
params['mc_currency']
end
def item_id
params['item_number'] || params['custom']
end
# This is the invoice which you passed to paypal
def invoice
params['invoice']
end
# Was this a test transaction?
def test?
params['test_ipn'] == '1'
end
def account
params['business'] || params['receiver_email']
end
def acknowledge
payload = raw
response = ssl_post(Paypal.service_url + '?cmd=_notify-validate', payload,
'Content-Length' => "#{payload.size}",
'User-Agent' => "Active Merchant -- http://activemerchant.org"
)
raise StandardError.new("Faulty paypal result: #{response}") unless ["VERIFIED", "INVALID"].include?(response)
response == "VERIFIED"
####################
# mock this!
####################
end
end
end
end
end
end
We’ll create the same object in test/mocks/test/paypal_ipn_mock.rb.
The object code is exactly the same with one small change.
def acknowledge
params["acknowledge"] == "true"
end
2. Setting up the test environment
Next we require our object in *test/test_helper.rb"
ENV["RAILS_ENV"] = "test" require File.expand_path(File.dirname(__FILE__) + "/../config/environment") require 'test_help' class Test::Unit::TestCase # ... #... #..... require 'assertion_helpers' include Test::Unit::Assertions::ActiveRecordAssertions require 'paypal_ipn_mock' end
3. Writing the tests
The trick is simple. To toggle “acknowledge” on/off we’ll pass an extra custom parameter “acknowledged” and the mock lib will return true/false depending on that parameter.
post :ipn, @ipn_params.merge("acknowledge" => "false")
Our functional test includes a default IPN parameter list that we will manipulate in each test according to our needs.
require File.dirname(__FILE__) + '/../test_helper'
require 'users_controller'
# Re-raise errors caught by the controller.
class UsersController; def rescue_action(e) raise e end; end
class UsersControllerTest < Test::Unit::TestCase
include ApplicationHelper
def setup
@controller = UsersController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@emails = ActionMailer::Base.deliveries
@emails.clear
@trans_id = "16F08736TA389152H"
@ipn_params = {"payment_date" => "04:33:33 Oct 13.2007+PDT" ,
"txn_type" => "web_accept",
"last_name" => "User",
"residence_country" => "US",
"item_name" => "FWJ - 3 Credits",
"payment_gross" => "180.00",
"mc_currency" => "USD",
"business" => replace_this_with_your_account,
"payment_type" => "instant",
"verify_sign" => "AZQLcOZ7B.YM2m-QDAXOrQQcLFYuA0N0XoC3zadaGhkGNF2nlRWmpzlI",
"payer_status" => "verified",
"test_ipn" => "1",
"tax" => "0.00",
"payer_email" => replace_this_with_the_payers_email ,
"txn_id" => @trans_id,
"quantity" => "1",
"receiver_email" => replace_this_with_the_recievers_email,
"first_name" => "Test",
"invoice" => nil,
"payer_id" => replace_with_payers_id,
"receiver_id" => replace_with_recievers_id,
"item_number" => "3",
"payment_status" => "Completed",
"payment_fee" => "5.52",
"mc_fee" => "5.52",
"shipping" => "0.00",
"mc_gross" => "180.00",
"custom" => "3",
"charset" => "windows-1252",
"notify_version" => "2.4"
}
end
The best way to create the @ipn_params hash is to go through the payment procedure once and grab it from the development log.
And finally the tests:
##################
# I P N
##################
def test_should_log_error_and_log_transaction_if_ipn_not_acknowledged
#force ipn to return acknowledge as false
post :ipn, @ipn_params.merge("acknowledge" => "false")
assert_response 403
assert !@emails.empty?
email = @emails.first
#application specific tests
assert_equal email.to.sort, ["some@email.com"].sort
assert !Trans.find_by_paypal_transaction_id(@trans_id)
end
def test_should_log_error_and_log_transaction_if_ipn_not_completed
post :ipn, @ipn_params.merge("acknowledge" => "true", "payment_status" => "Cancelled")
assert_response 403
#some more application specific tests
end
# add tests for duplicate transaction identification, etc
As a TODO , all other statuses of IPN (“Refunded”, “Cancelled”) should be logged and handled somehow.
And remember, testing makes you sleep at night :)

Comments (27)
Thanks for sharing your approach. I have followed the above approach for mocking calls to facebook for one of my facebook applications.
The other way that I follow for mocking some of other external services is to use http mock. that I way I don't really modify the client library of the service I am trying to use.
Thanks so much for sharing this.
I've got an idea that's not going to make any money, but I just want to get the experience of doing it and getting it out there, and since the restrictions on Authorize.net and other processors are so high, Paypal seems the only viable option.
Legends.
Is there a downloadable project with all this stuff ready to go? A kit, if you will.
Hello. Im using active merchant with rails and I'm writting functional tests for paypal direct and paypal express purchases.
My problem is that when I test a purchase through paypal express gateway, the application redirects to paypal site expecting the user to accept the purchase, as user does never accepts, the flow never comes back to the application. How could I simulate the the paypal express redirection and response?
Regards
Thanks for putting this up.
As a frequent user of Pay Pal this information will be valuable to my business.
Regards
Thanks for sharing your approach. I have followed the above approach for mocking calls to facebook for one of my facebook applications.
If you can't get enough gadgets into your home then add this to your list
I really like these post. Appreciate it. Thank you for sharing.
Reading this post makes it more interesting.
Even though with a few disadvantages,
this makes a good post. Thank you.
appreciate the post...
Thanks for the help, I've been having trouble working this out.
great site!
authentic information is described in simple words ...... Thanks
Yes, the sandbox is problematic but the IPN simulator is very useful.
For me paypal is the best
Thank you for your topic
The subject of bitter, sweet, beautiful, moon
Accept traffic
Gisele thanks from me to you
Mra thanks
To the meeting ..
well nice..
Great test
Nice point of view, keep updating good content.
Thank you to inform
şarkılar
duygusal
duygusal şarkılar
amatör şarkılar
thanks for the share..
Nice approach. Awesome idea. THanks
I have a question about how I could go about unit testing a paypal instant payment notification (IPN). I googled and found a Rails example, but it didn't answer the main question I have, which is...
How can I mock the IPN verification procedure? As you probably know, when a transaction hits, PayPal posts IPN data to your server. You then post back to PayPal to verify the transaction.
Mocking the initial IPN hit is a no-brainer, but how can I mock the verification? My idea is to just mock the various possible verification outputs, but how would I make that work in my existing code? It doesn't seem like proper form to code a caveat into your function that checks to see if you're in the middle of a unit test and then skips the actual verification post if you are... but I'm a testing newb so I'm not sure.
currently used paypal a lot if people. I want to try to use it to make use paypal. I will try and practice this tutorial, this is very useful. thank you for sharing.
If you really want to learn how to grow your farm fast, then check out this farmville secrets guide today.
currently used paypal a lot if people. I want to try to use it to make use paypal. I will try and practice this tutorial, this is very useful. thank you for sharing.
thanks.
If you're looking to do an email search, this article discusses your options and reviews a popular service that gets results. visit this site.
Thank you for your topic
very good article
Drop a comment: