sinatra-paypal 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|