27
Oct

Mock testing Paypal's IPN with Rails

posted by gchatz 27 comments rails ruby testing

In 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:

ipn flow
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)

Pin Jatinder Singh said on Dec 18, 03:55 PM:

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.

Pin Adam said on Aug 22, 02:50 PM:

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.

Pin Humberto said on Dec 09, 06:39 PM:

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

Pin facial said on Nov 02, 07:04 AM:

Thanks for putting this up.
As a frequent user of Pay Pal this information will be valuable to my business.

Regards

Pin insurance said on Nov 05, 06:20 AM:

Thanks for sharing your approach. I have followed the above approach for mocking calls to facebook for one of my facebook applications.

Pin bingo said on Nov 09, 03:23 AM:

If you can't get enough gadgets into your home then add this to your list

Pin ebay coupons said on Nov 26, 06:53 AM:

I really like these post. Appreciate it. Thank you for sharing.

Pin Fire Safety for Kids said on Nov 26, 03:32 PM:

Reading this post makes it more interesting.
Even though with a few disadvantages,
this makes a good post. Thank you.

Pin travelocity coupon said on Dec 05, 12:57 PM:

appreciate the post...

Pin sydney airport parking said on Dec 09, 12:26 AM:

Thanks for the help, I've been having trouble working this out.

Pin tire rack coupon said on Dec 14, 11:42 AM:

great site!

Pin اخبار مصر said on Dec 21, 03:27 PM:

authentic information is described in simple words ...... Thanks

Pin Bill said on Dec 30, 07:44 AM:

Yes, the sandbox is problematic but the IPN simulator is very useful.

Pin Rafael said on Jan 19, 06:19 PM:

For me paypal is the best

Pin العاب said on Jan 27, 04:24 PM:

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 ..

Pin hp coupons said on Feb 01, 04:57 PM:

well nice..

Pin medifast coupons said on Feb 02, 12:25 PM:

Great test

Pin mania virtual said on Feb 05, 05:18 PM:

Nice point of view, keep updating good content.

Pin sabrina said on Feb 06, 07:48 PM:

Thank you to inform
şarkılar
duygusal
duygusal şarkılar
amatör şarkılar

Pin panic attak symptoms said on Feb 08, 06:14 PM:

thanks for the share..

Pin water delivery services said on Feb 10, 12:25 PM:

Nice approach. Awesome idea. THanks

Pin bad credit mobile phones said on Feb 14, 03:13 PM:

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.

Pin Technology said on Feb 19, 04:55 PM:

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.

Pin farmville secrets said on Feb 22, 05:02 PM:

If you really want to learn how to grow your farm fast, then check out this farmville secrets guide today.

Pin منتديات said on Feb 23, 11:47 PM:

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.

Pin reverse email search said on Feb 27, 06:46 PM:

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.

Pin compre direto da china said on Mar 09, 08:36 PM:

Thank you for your topic
very good article

Drop a comment: