sinatra-paypal 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +3 -4
- data/lib/sinatra/paypal.rb +27 -17
- data/lib/sinatra/paypal/_payment.erb +7 -7
- data/lib/sinatra/paypal/paypal-helper.rb +8 -0
- data/lib/sinatra/paypal/paypal-request.rb +109 -9
- data/lib/sinatra/paypal/version.rb +1 -1
- data/sinatra-paypal.gemspec +4 -1
- data/test/app.rb +57 -0
- data/test/form_test.rb +93 -0
- data/test/paypal_test.rb +135 -0
- metadata +26 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbbe8b8b2351d5cf7d643aabff1146ea783b4729
|
4
|
+
data.tar.gz: 6095033aa3049ec0307a63960c936b025bc91b11
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6bba20dbb1049f2ccd157a7331bae400bcb495ef316eb18cadac06876d6dc1ca717982bc3a6e25aa1501448a5f05777fb1a766b21959efb7977fb9063cca87a
|
7
|
+
data.tar.gz: cdc5ac4baf40951a567cdeccdaec36abdab8ef7a16ab03d38c51088054b0ab573b88603381e57f59a3a94a53b4bdbd9c5487b22d0fb6f4dda6eb7d7dd7128d35
|
data/Rakefile
CHANGED
data/lib/sinatra/paypal.rb
CHANGED
@@ -9,7 +9,7 @@ PAYPAL_BLOCKS = {}
|
|
9
9
|
module PayPal
|
10
10
|
|
11
11
|
module Helpers
|
12
|
-
def
|
12
|
+
def _paypal_block(name)
|
13
13
|
return Proc.new{} if !PAYPAL_BLOCKS.key? name
|
14
14
|
PAYPAL_BLOCKS[name]
|
15
15
|
end
|
@@ -18,16 +18,21 @@ module PayPal
|
|
18
18
|
PaypalHelper.form_url(settings.paypal.sandbox?)
|
19
19
|
end
|
20
20
|
|
21
|
-
def html_payment_form(
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
def html_payment_form(item, data = {})
|
22
|
+
# it is valid to send a nil item through - we might be going to set the fields in
|
23
|
+
# javascript - but if it isn't null then there are certain fields that need to be
|
24
|
+
# set
|
25
|
+
if !item.nil?
|
26
|
+
raise 'item.code required' if !item.respond_to?(:code) || item.code.nil?
|
27
|
+
raise 'item.price required' if !item.respond_to?(:price) || item.price.nil?
|
28
|
+
raise 'item.price must be a number above zero' if item.price.to_f <= 0
|
29
|
+
end
|
25
30
|
|
26
31
|
raise 'cannot generate a payment form without settings.paypal.email' if settings.paypal.email.nil?
|
27
32
|
|
28
33
|
erb :_payment, :views => File.join(File.dirname(__FILE__), '/paypal'), :locals => {
|
29
34
|
:custom_data => data,
|
30
|
-
:
|
35
|
+
:item => item
|
31
36
|
}
|
32
37
|
end
|
33
38
|
end
|
@@ -54,30 +59,35 @@ module PayPal
|
|
54
59
|
halt 400, 'no username provided' if paypal_request.username.nil?
|
55
60
|
|
56
61
|
# check transaction log to make sure this not a replay attack
|
57
|
-
if instance_exec(paypal_request, &
|
58
|
-
# we want to
|
59
|
-
#
|
60
|
-
# back
|
61
|
-
log_error 'already processed' if respond_to? :log_error
|
62
|
+
if instance_exec(paypal_request, &_paypal_block(:repeated?))
|
63
|
+
# we also want to return 200, because if it is paypal sending this, it will send
|
64
|
+
# it again and again until it gets a 200 back
|
62
65
|
halt 200, 'already processed'
|
63
|
-
else
|
64
|
-
instance_exec(paypal_request, &paypal_block(:save))
|
65
66
|
end
|
66
67
|
|
67
|
-
instance_exec(paypal_request, &
|
68
|
+
instance_exec(paypal_request, &_paypal_block(:validate!))
|
68
69
|
|
69
70
|
# check that the payment is complete. we still return 200 if not, but
|
70
71
|
# we don't need to do anymore processing (except for marking it as accountable, if it is)
|
71
72
|
if paypal_request.complete?
|
72
|
-
instance_exec(paypal_request, &
|
73
|
+
instance_exec(paypal_request, &_paypal_block(:complete))
|
73
74
|
end
|
74
75
|
|
75
|
-
instance_exec(paypal_request, &
|
76
|
+
instance_exec(paypal_request, &_paypal_block(:finish))
|
76
77
|
|
77
78
|
return 200
|
78
79
|
end
|
79
80
|
end
|
80
81
|
|
82
|
+
# Register a payment callback. All callbacks are called
|
83
|
+
# with a single argument of the type +PaypalRequest+ containing all the
|
84
|
+
# data for the notification.
|
85
|
+
#
|
86
|
+
# payment :complete do |p|
|
87
|
+
# # process the payment here
|
88
|
+
# # don't forget to check that the price is correct!
|
89
|
+
# end
|
90
|
+
#
|
81
91
|
def payment(name, &block)
|
82
92
|
PAYPAL_BLOCKS[name] = block
|
83
93
|
end
|
@@ -86,4 +96,4 @@ end
|
|
86
96
|
|
87
97
|
module Sinatra
|
88
98
|
register PayPal
|
89
|
-
end
|
99
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
|
2
|
-
<form
|
2
|
+
<form class='sinatra-paypal-form' action="<%= paypal_form_url %>" method="post">
|
3
3
|
<input type="hidden" name="cmd" value="_xclick">
|
4
4
|
|
5
5
|
<input type="hidden" name="business" value="<%= settings.paypal.email %>">
|
@@ -7,14 +7,14 @@
|
|
7
7
|
<input type="hidden" name="return" value="<%= url(settings.paypal.return_url) %>">
|
8
8
|
<input type="hidden" name="notify_url" value="<%= url(settings.paypal.notify_url) %>">
|
9
9
|
|
10
|
-
<% if
|
11
|
-
<input type="hidden" name="item_number" id='paypal-item-number' value="
|
12
|
-
<input type="hidden" name="item_name" id='paypal-item-name' value="
|
10
|
+
<% if item.nil? %>
|
11
|
+
<input type="hidden" name="item_number" id='paypal-item-number' value="NO_ITEM">
|
12
|
+
<input type="hidden" name="item_name" id='paypal-item-name' value="NO_ITEM">
|
13
13
|
<input type="hidden" name="amount" id='paypal-amount' value="1.00">
|
14
14
|
<% else %>
|
15
|
-
<input type="hidden" name="item_number" id='paypal-item-number' value="<%=
|
16
|
-
<input type="hidden" name="item_name" id='paypal-item-name' value="<%=
|
17
|
-
<input type="hidden" name="amount" id='paypal-amount' value="<%=
|
15
|
+
<input type="hidden" name="item_number" id='paypal-item-number' value="<%= item.code %>">
|
16
|
+
<input type="hidden" name="item_name" id='paypal-item-name' value="<%= item.name || item.code %>">
|
17
|
+
<input type="hidden" name="amount" id='paypal-amount' value="<%= item.price %>">
|
18
18
|
<% end %>
|
19
19
|
|
20
20
|
<input type="hidden" name="no_shipping" value="1">
|
@@ -5,6 +5,12 @@ class PaypalHelper
|
|
5
5
|
@use_sandbox = use_sandbox
|
6
6
|
end
|
7
7
|
|
8
|
+
# returns the url that the payment forms must be submitted to so they can be
|
9
|
+
# processed by paypal. If the sandbox attribute is set, then it will return the
|
10
|
+
# url for the sandbox
|
11
|
+
#
|
12
|
+
# form_url # => https://www.paypal.com/cgi-bin/webscr
|
13
|
+
#
|
8
14
|
def form_url
|
9
15
|
@use_sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr'
|
10
16
|
end
|
@@ -13,6 +19,8 @@ class PaypalHelper
|
|
13
19
|
new(use_sandbox).form_url
|
14
20
|
end
|
15
21
|
|
22
|
+
# validates the ipn request with paypal to make sure it is genuine. +params+ should contain
|
23
|
+
# the exact params object that was sent as part of the IPN POST
|
16
24
|
def ipn_valid?(params)
|
17
25
|
return false if params.nil?
|
18
26
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'json'
|
1
2
|
|
2
3
|
class PaypalRequest
|
3
4
|
def initialize(params)
|
@@ -9,17 +10,26 @@ class PaypalRequest
|
|
9
10
|
@custom_data = nil
|
10
11
|
end
|
11
12
|
|
12
|
-
|
13
|
+
# a unique id for the transaction (event if the paypal transaction_id is the
|
14
|
+
# same)
|
15
|
+
#
|
16
|
+
# payment.id # => 661295c9cbf9d6b2f6428414504a8deed3020641
|
17
|
+
#
|
13
18
|
def id
|
14
19
|
self.transaction_hash
|
15
20
|
end
|
16
21
|
|
17
|
-
#
|
18
|
-
# so we need to check both of them in order to see if the txn is unquie
|
22
|
+
# alias for +id+
|
19
23
|
def transaction_hash
|
24
|
+
# the same transaction id can come in mulitple times with different statuses
|
25
|
+
# so we need to check both of them in order to see if the txn is unquie
|
20
26
|
"#{self.transaction_id}-#{@fields[:payment_status]}".sha1
|
21
27
|
end
|
22
28
|
|
29
|
+
# the paypal transaction_id
|
30
|
+
#
|
31
|
+
# payment.transaction_id # => 6FH51066BB6306017
|
32
|
+
#
|
23
33
|
def transaction_id
|
24
34
|
@fields[:txn_id]
|
25
35
|
end
|
@@ -44,7 +54,7 @@ class PaypalRequest
|
|
44
54
|
end
|
45
55
|
|
46
56
|
def item_number
|
47
|
-
@fields[:item_number]
|
57
|
+
@fields[:item_number]
|
48
58
|
end
|
49
59
|
|
50
60
|
def amount
|
@@ -55,17 +65,39 @@ class PaypalRequest
|
|
55
65
|
Float(@fields[:mc_fee] || 0)
|
56
66
|
end
|
57
67
|
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
68
|
+
# The payment amount minus any fees, giving the net profit
|
69
|
+
#
|
70
|
+
# payment.profit # => 1.63
|
71
|
+
#
|
61
72
|
def profit
|
73
|
+
# defined as the gross amount, minus transaction fees
|
74
|
+
# could also subtract shipping and tax here as well, but we don't have to deal with
|
75
|
+
# any of that yet
|
62
76
|
self.amount - self.payment_fee
|
63
77
|
end
|
64
78
|
|
79
|
+
# One of the most common peices of data to send through with the payment is the
|
80
|
+
# username that the payment applies to. This method will return the username from
|
81
|
+
# the custom_data field, if it contains one
|
82
|
+
#
|
83
|
+
# payment.username # => reednj
|
84
|
+
#
|
65
85
|
def username
|
66
86
|
self.custom_data[:username]
|
67
87
|
end
|
68
88
|
|
89
|
+
# Whatever is put in the custom_data field on the payment form will be sent back
|
90
|
+
# in the payment notification. This is useful for tracking the username and other
|
91
|
+
# information that is required to process the payment.
|
92
|
+
#
|
93
|
+
# The sinatra-paypal module expects this to be either a plain string containing
|
94
|
+
# the username or a json string that can be parse into a ruby object.
|
95
|
+
#
|
96
|
+
# Note that this field can be modified by the user before submitting the form,
|
97
|
+
# so you should verify the contents of it before using it.
|
98
|
+
#
|
99
|
+
# payment.custom_data # => { :username => 'dave' }
|
100
|
+
#
|
69
101
|
def custom_data
|
70
102
|
if @custom_data.nil?
|
71
103
|
if @fields[:custom].strip.start_with? '{'
|
@@ -81,29 +113,97 @@ class PaypalRequest
|
|
81
113
|
return @custom_data
|
82
114
|
end
|
83
115
|
|
116
|
+
# an alias for +custom_data+
|
84
117
|
def data
|
85
118
|
custom_data
|
86
119
|
end
|
87
120
|
|
121
|
+
# The payment status. The most common is Completed, but you might also see
|
122
|
+
#
|
123
|
+
# - Refunded
|
124
|
+
# - Pending
|
125
|
+
# - Reversed
|
126
|
+
# - Canceled_Reversal
|
127
|
+
# - Completed
|
128
|
+
#
|
129
|
+
# payment.status # => Pending
|
130
|
+
#
|
88
131
|
def status
|
89
132
|
@fields[:payment_status]
|
90
133
|
end
|
91
134
|
|
135
|
+
# Returns true if +status+ is Completed
|
92
136
|
def complete?
|
93
137
|
self.status == 'Completed'
|
94
138
|
end
|
95
139
|
|
96
|
-
#
|
97
|
-
#
|
140
|
+
# Returns true if the transaction results in a change of the merchant account balance. This
|
141
|
+
# doesn't apply to all IPN notifications - some (such as those with status Pending) are simply
|
142
|
+
# notifications that do not actually result in money chaning hands
|
98
143
|
def is_accountable?
|
144
|
+
# these are payment statues that actually result in the paypal balance changing, so we should set them as
|
145
|
+
# accountable in the payment_log
|
99
146
|
(self.complete? || self.status == 'Refunded' || self.status == 'Reversed' || self.status == 'Canceled_Reversal')
|
100
147
|
end
|
101
148
|
|
149
|
+
# returns the reason code for noticiations that have one (usually Pending or Refunded transactions)
|
150
|
+
# if a reason code is not applicable, this will return nil
|
102
151
|
def reason_code
|
103
152
|
@fields[:reason_code] || @fields[:pending_reason]
|
104
153
|
end
|
105
154
|
|
155
|
+
# A Hash with the raw data for the paypal transaction, this contains many less useful
|
156
|
+
# fields not listed above
|
157
|
+
#
|
158
|
+
# Here is a list of the fields for a Completed transaction. These fields differ slightly
|
159
|
+
# between the main transaction types.
|
160
|
+
#
|
161
|
+
# :business: you@yourcompany.com
|
162
|
+
# :charset: UTF-8
|
163
|
+
# :cmd: _notify-validate
|
164
|
+
# :custom: ! '{"thread_id":"3f3fc5","username":"customer_user_name"}'
|
165
|
+
# :first_name: Bill
|
166
|
+
# :handling_amount: '0.00'
|
167
|
+
# :ipn_track_id: a5d596fffd057
|
168
|
+
# :item_name: Describe your item
|
169
|
+
# :item_number: I01
|
170
|
+
# :last_name: Davies
|
171
|
+
# :mc_currency: USD
|
172
|
+
# :mc_fee: '0.37'
|
173
|
+
# :mc_gross: '2.00'
|
174
|
+
# :notify_version: '3.8'
|
175
|
+
# :payer_email: customer@gmail.com
|
176
|
+
# :payer_id: 9AFYVBV9TTT5L
|
177
|
+
# :payer_status: verified
|
178
|
+
# :payment_date: 18:01:26 Jul 29, 2015 PDT
|
179
|
+
# :payment_fee: '0.37'
|
180
|
+
# :payment_gross: '2.00'
|
181
|
+
# :payment_status: Completed
|
182
|
+
# :payment_type: instant
|
183
|
+
# :protection_eligibility: Ineligible
|
184
|
+
# :quantity: '1'
|
185
|
+
# :receiver_email: you@yourcompany.com
|
186
|
+
# :receiver_id: 4SUZXXXXFMC28
|
187
|
+
# :residence_country: US
|
188
|
+
# :shipping: '0.00'
|
189
|
+
# :tax: '0.00'
|
190
|
+
# :transaction_subject: ! '{"thread_id":"3f3fc5","username":"customer_user_name"}'
|
191
|
+
# :txn_id: 6FH51036BB6776017
|
192
|
+
# :txn_type: web_accept
|
193
|
+
# :verify_sign: AmydMwaMHzmxRimnFMnKy3o9n-ElAWWRtiJ9TEixE0iGouC6EaMS0mWI
|
194
|
+
#
|
195
|
+
# See https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNandPDTVariables/ for
|
196
|
+
# a full description of the notification fields
|
197
|
+
#
|
106
198
|
def fields
|
107
199
|
@fields
|
108
200
|
end
|
109
201
|
end
|
202
|
+
|
203
|
+
# extensions needed for the paypal request to work
|
204
|
+
class String
|
205
|
+
def sha1
|
206
|
+
Digest::SHA1.hexdigest self
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
data/sinatra-paypal.gemspec
CHANGED
@@ -14,14 +14,17 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
18
|
spec.bindir = "exe"
|
18
19
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
20
|
spec.require_paths = ["lib"]
|
20
21
|
|
21
22
|
spec.add_development_dependency "bundler", "~> 1.9"
|
22
|
-
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
24
|
spec.add_development_dependency "test-unit"
|
24
25
|
spec.add_development_dependency "rack-test"
|
26
|
+
spec.add_development_dependency "minitest"
|
27
|
+
|
25
28
|
spec.add_runtime_dependency "sinatra"
|
26
29
|
spec.add_runtime_dependency "rest-client"
|
27
30
|
end
|
data/test/app.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.require
|
4
|
+
|
5
|
+
require 'sinatra'
|
6
|
+
require 'sinatra/paypal'
|
7
|
+
|
8
|
+
# this allows us to handle errors in test without the default page
|
9
|
+
# getting generated (which isn't very useful inside unit-tests)
|
10
|
+
set :raise_errors, false
|
11
|
+
set :show_exceptions, false
|
12
|
+
|
13
|
+
# dump the error description - this will make it appear nicely in the
|
14
|
+
# unit test description
|
15
|
+
error do
|
16
|
+
e = request.env['sinatra.error']
|
17
|
+
return "#{e.class}: #{e.message}" unless e.nil?
|
18
|
+
return "unknown error"
|
19
|
+
end
|
20
|
+
|
21
|
+
payment :repeated? do |p|
|
22
|
+
path = '/tmp/test.sinatra-payment.log'
|
23
|
+
data = File.read path
|
24
|
+
id = "#{p.id}\n"
|
25
|
+
|
26
|
+
if data.include? id
|
27
|
+
true
|
28
|
+
else
|
29
|
+
File.append path, "#{p.id}\n"
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
get '/payment/form/empty' do
|
35
|
+
return html_payment_form nil
|
36
|
+
end
|
37
|
+
|
38
|
+
get '/payment/form' do
|
39
|
+
item = {}
|
40
|
+
item[:code] = params[:item_code]
|
41
|
+
item[:price] = params[:item_price]
|
42
|
+
item[:name] = params[:item_name]
|
43
|
+
item = OpenStruct.new item
|
44
|
+
|
45
|
+
return html_payment_form(item)
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Extensions to make the the test app simpler
|
50
|
+
#
|
51
|
+
class File
|
52
|
+
def self.append(path, data)
|
53
|
+
File.open(path, 'a:UTF-8') do |file|
|
54
|
+
file.write data
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/test/form_test.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'development'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'rack/test'
|
6
|
+
require 'test/unit'
|
7
|
+
|
8
|
+
require_relative './app'
|
9
|
+
|
10
|
+
class RedditStreamTest < Test::Unit::TestCase
|
11
|
+
include Rack::Test::Methods
|
12
|
+
|
13
|
+
def app
|
14
|
+
Sinatra::Application
|
15
|
+
end
|
16
|
+
|
17
|
+
def page_error(desc)
|
18
|
+
"#{desc} (HTTP #{last_response.status})\n" +
|
19
|
+
"#{last_response.body.truncate 256}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_form_helper_no_email
|
23
|
+
app.paypal.email = nil
|
24
|
+
|
25
|
+
begin
|
26
|
+
get '/payment/form/empty'
|
27
|
+
assert false, 'no email payment form didn\'t raise an exception'
|
28
|
+
rescue => e
|
29
|
+
raise if !e.message.include? 'email'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_form_helper_no_item_code
|
34
|
+
app.paypal.email = 'reednj@gmail.com'
|
35
|
+
|
36
|
+
get '/payment/form'
|
37
|
+
assert !last_response.ok?, 'form created without item code'
|
38
|
+
assert last_response.body.include?('item.code'), 'no error about missing item.code'
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_form_helper_invalid_price
|
42
|
+
app.paypal.email = 'reednj@gmail.com'
|
43
|
+
|
44
|
+
get '/payment/form', { :item_code => 'ITEM-0', :item_price => 'xxx'}
|
45
|
+
assert !last_response.ok?, 'form created with invalid price'
|
46
|
+
assert last_response.body.include?('item.price'), 'no error about invalid price'
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_empty_form_helper
|
50
|
+
app.paypal.email = 'reednj@gmail.com'
|
51
|
+
|
52
|
+
get '/payment/form/empty'
|
53
|
+
assert last_response.ok?, page_error('could not generate payment form')
|
54
|
+
assert last_response.body.include?(app.paypal.email), 'account email not found in form'
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_form_helper_with_item
|
58
|
+
app.paypal.email = 'reednj@gmail.com'
|
59
|
+
|
60
|
+
code = 'ITEM-0'
|
61
|
+
get '/payment/form', { :item_code => code, :item_price => '5.00'}
|
62
|
+
assert last_response.ok?, page_error('could not generate payment form')
|
63
|
+
assert last_response.body.include?(app.paypal.email), 'account email not found in form'
|
64
|
+
assert last_response.body.include?(code), 'item_code not found in form'
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_form_helper_with_item_description
|
68
|
+
app.paypal.email = 'reednj@gmail.com'
|
69
|
+
|
70
|
+
code = 'ITEM-0'
|
71
|
+
desc = 'item description'
|
72
|
+
get '/payment/form', { :item_code => code, :item_price => '5.00', :item_name => desc}
|
73
|
+
assert last_response.ok?, page_error('could not generate payment form')
|
74
|
+
assert last_response.body.include?(app.paypal.email), 'account email not found in form'
|
75
|
+
assert last_response.body.include?(code), 'item_code not found in form'
|
76
|
+
assert last_response.body.include?(desc), 'item_name not found in form'
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
class Array
|
82
|
+
def rand
|
83
|
+
self[Object.send(:rand, self.length)]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class String
|
88
|
+
def truncate(max_len = 32)
|
89
|
+
append = '...'
|
90
|
+
return self if self.length < max_len
|
91
|
+
return self[0...(max_len - append.length)] + append
|
92
|
+
end
|
93
|
+
end
|
data/test/paypal_test.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'development'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'rack/test'
|
6
|
+
require 'test/unit'
|
7
|
+
|
8
|
+
require_relative './app'
|
9
|
+
|
10
|
+
class RedditStreamTest < Test::Unit::TestCase
|
11
|
+
include Rack::Test::Methods
|
12
|
+
|
13
|
+
def app
|
14
|
+
Sinatra::Application
|
15
|
+
end
|
16
|
+
|
17
|
+
def standard_payment_data
|
18
|
+
{
|
19
|
+
:tax=>"0.00",
|
20
|
+
:receiver_email=>"accounts@reddit-stream.com",
|
21
|
+
:payment_gross=>"1.49",
|
22
|
+
:transaction_subject=>"rs-test",
|
23
|
+
:receiver_id=>"TDCZRKH9NXETE",
|
24
|
+
:quantity=>"1",
|
25
|
+
:business=>"accounts@reddit-stream.com",
|
26
|
+
:mc_currency=>"USD",
|
27
|
+
:payment_fee=>"0.44",
|
28
|
+
:notify_version=>"3.7",
|
29
|
+
:shipping=>"0.00",
|
30
|
+
:verify_sign=>"AVQ7PVd2w7LAwpsg5Yh7hFxe9SywAuiBQIp9fScf77n48fA8WF21KG2i",
|
31
|
+
:cmd=>"_notify-validate",
|
32
|
+
:test_ipn=>"1",
|
33
|
+
:txn_type=>"web_accept",
|
34
|
+
:charset=>"UTF-8",
|
35
|
+
:payer_id=>"649KQR22GAU4W",
|
36
|
+
:payer_status=>"verified",
|
37
|
+
:ipn_track_id=>"d1992aaea45a",
|
38
|
+
:handling_amount=>"0.00",
|
39
|
+
:residence_country=>"US",
|
40
|
+
:payer_email=>"user1@reddit-stream.com",
|
41
|
+
:payment_date=>"09:26:13 Sep 23, 2013 PDT",
|
42
|
+
:protection_eligibility=>"Ineligible",
|
43
|
+
:payment_type=>"instant",
|
44
|
+
|
45
|
+
:first_name=>"Nathan",
|
46
|
+
:last_name=>"Reed",
|
47
|
+
:custom=>"rs-test",
|
48
|
+
|
49
|
+
:txn_id=>random_string(),
|
50
|
+
:payment_status=>"Completed",
|
51
|
+
:item_number=>"RS1",
|
52
|
+
:item_name=>"reddit-stream.com Unlimited Account",
|
53
|
+
:mc_gross=>"1.49",
|
54
|
+
:mc_fee=>"0.44"
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def random_string(len = 5)
|
59
|
+
space = ['a', 'b', 'c', 'd', 'e','f', '1', '2', '3', '4', '5', '6', '7', '8', '9']
|
60
|
+
return (0..5).map {|a| space.rand }.join
|
61
|
+
end
|
62
|
+
|
63
|
+
def page_error(desc)
|
64
|
+
"#{desc} (HTTP #{last_response.status})\n" +
|
65
|
+
"#{last_response.body.truncate 256}"
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Now we start the testing of the payment processing
|
70
|
+
#
|
71
|
+
def test_payment_rejects_double_processing
|
72
|
+
data = standard_payment_data
|
73
|
+
|
74
|
+
post '/payment/validate', data
|
75
|
+
assert last_response.ok?, page_error("Payment rejected")
|
76
|
+
assert !last_response.body.include?('already processed')
|
77
|
+
|
78
|
+
post '/payment/validate', data
|
79
|
+
assert last_response.ok?
|
80
|
+
assert last_response.body.include?('already processed'), "duplicate payment accepted!"
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_payment_thread_id
|
84
|
+
data = standard_payment_data
|
85
|
+
data[:custom] = {
|
86
|
+
:username => 'njr123',
|
87
|
+
:thread_id => '2hqk1o'
|
88
|
+
}.to_json
|
89
|
+
|
90
|
+
header 'Accept', 'text/plain'
|
91
|
+
post '/payment/validate', data
|
92
|
+
assert last_response.ok?, page_error("Payment not accepted")
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_payment_thread_id_only
|
96
|
+
data = standard_payment_data
|
97
|
+
data[:custom] = {
|
98
|
+
:thread_id => '2hqk1o'
|
99
|
+
}.to_json
|
100
|
+
|
101
|
+
post '/payment/validate', data
|
102
|
+
assert_equal last_response.status, 400
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_payment_accepted_json_custom_data
|
106
|
+
data = standard_payment_data
|
107
|
+
data[:custom] = {:username => data[:custom]}.to_json
|
108
|
+
|
109
|
+
post '/payment/validate', data
|
110
|
+
assert last_response.ok?, page_error("Payment not accepted")
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_payment_accepted
|
114
|
+
data = standard_payment_data
|
115
|
+
username = data[:custom]
|
116
|
+
|
117
|
+
post '/payment/validate', data
|
118
|
+
assert last_response.ok?, page_error("Payment not accepted")
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
class Array
|
124
|
+
def rand
|
125
|
+
self[Object.send(:rand, self.length)]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class String
|
130
|
+
def truncate(max_len = 32)
|
131
|
+
append = '...'
|
132
|
+
return self if self.length < max_len
|
133
|
+
return self[0...(max_len - append.length)] + append
|
134
|
+
end
|
135
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sinatra-paypal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Reed
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-10-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -26,6 +26,20 @@ dependencies:
|
|
26
26
|
version: '1.9'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: test-unit
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
30
44
|
requirements:
|
31
45
|
- - ">="
|
@@ -39,7 +53,7 @@ dependencies:
|
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
|
-
name: test
|
56
|
+
name: rack-test
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
59
|
- - ">="
|
@@ -53,7 +67,7 @@ dependencies:
|
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
70
|
+
name: minitest
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
73
|
- - ">="
|
@@ -115,6 +129,9 @@ files:
|
|
115
129
|
- lib/sinatra/paypal/paypal-request.rb
|
116
130
|
- lib/sinatra/paypal/version.rb
|
117
131
|
- sinatra-paypal.gemspec
|
132
|
+
- test/app.rb
|
133
|
+
- test/form_test.rb
|
134
|
+
- test/paypal_test.rb
|
118
135
|
homepage: http://github.com/reednj/sinatra-paypal
|
119
136
|
licenses:
|
120
137
|
- MIT
|
@@ -135,8 +152,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
152
|
version: '0'
|
136
153
|
requirements: []
|
137
154
|
rubyforge_project:
|
138
|
-
rubygems_version: 2.4.5
|
155
|
+
rubygems_version: 2.4.5
|
139
156
|
signing_key:
|
140
157
|
specification_version: 4
|
141
158
|
summary: Easy validation and processing of Paypal IPN payments
|
142
|
-
test_files:
|
159
|
+
test_files:
|
160
|
+
- test/app.rb
|
161
|
+
- test/form_test.rb
|
162
|
+
- test/paypal_test.rb
|