boutique 0.0.1 → 0.0.2
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.
- data/README.md +58 -20
- data/boutique +23 -2
- data/config.ru +16 -1
- data/lib/boutique.rb +298 -3
- data/test/app_test.rb +56 -10
- data/test/config_test.rb +18 -0
- data/test/helper.rb +45 -0
- data/test/model_test.rb +67 -0
- metadata +11 -8
data/README.md
CHANGED
@@ -8,6 +8,15 @@ Installation
|
|
8
8
|
|
9
9
|
% gem install boutique
|
10
10
|
|
11
|
+
You'll need some private/public certificates:
|
12
|
+
|
13
|
+
% openssl genrsa -out private.pem 1024
|
14
|
+
% openssl req -new -key private.pem -x509 -days 365 -out public.pem
|
15
|
+
|
16
|
+
You can download PayPal's public certificate under `My Account > Encrypted
|
17
|
+
Payment Settings`. Click the download button for PayPal Public Certificate.
|
18
|
+
Rename this certificate to something like `paypal.pem`.
|
19
|
+
|
11
20
|
Setup a `config.ru` file and run it like any other Sinatra application. You
|
12
21
|
can configure what products you sell here:
|
13
22
|
|
@@ -15,25 +24,46 @@ can configure what products you sell here:
|
|
15
24
|
require 'boutique'
|
16
25
|
|
17
26
|
Boutique.configure do |c|
|
18
|
-
c.
|
19
|
-
c.
|
20
|
-
c.
|
21
|
-
c.
|
22
|
-
c.
|
27
|
+
c.pem_private '/path/to/private.pem'
|
28
|
+
c.pem_public '/path/to/public.pem'
|
29
|
+
c.pem_paypal '/path/to/paypal.pem'
|
30
|
+
c.download_dir '/path/to/download'
|
31
|
+
c.download_path '/download'
|
32
|
+
c.db_adapter 'mysql'
|
33
|
+
c.db_host 'localhost'
|
34
|
+
c.db_username 'root'
|
35
|
+
c.db_password 'secret'
|
36
|
+
c.db_database 'boutique'
|
37
|
+
c.pp_email 'paypal_biz@mailinator.com'
|
38
|
+
c.pp_url 'http://https://www.sandbox.paypal.com/cgi-bin/webscr'
|
23
39
|
end
|
24
40
|
|
25
|
-
Boutique.
|
26
|
-
p.
|
27
|
-
p.
|
28
|
-
p.price
|
29
|
-
p.return_url
|
30
|
-
p.description '50 different icons in three sizes and four colors'
|
41
|
+
Boutique.product('icon-set') do |p|
|
42
|
+
p.name 'Icon Set'
|
43
|
+
p.files '/path/to/icon-set.tgz' # array for multiple files
|
44
|
+
p.price 10.5
|
45
|
+
p.return_url 'http://zincmade.com/thankyou'
|
31
46
|
end
|
32
47
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
48
|
+
run Boutique::App if !ENV['BOUTIQUE_CMD']
|
49
|
+
|
50
|
+
Stick this in your `bashrc` or `zshrc`:
|
51
|
+
|
52
|
+
BOUTIQUE_CONFIG='/path/to/config.ru'
|
53
|
+
|
54
|
+
Now setup the database tables (assuming you've already created the database and
|
55
|
+
credentials):
|
56
|
+
|
57
|
+
% boutique --migrate
|
58
|
+
|
59
|
+
With the settings above, a normal flow would look like:
|
60
|
+
|
61
|
+
1. On your site, link the user to `/boutique/buy/icon-set/` to purchase
|
62
|
+
2. User is redirected to paypal
|
63
|
+
3. After completing paypal, user is redirected to
|
64
|
+
`http://zincmade.com/thankyou?b=order-id`
|
65
|
+
4. On this page, issue an AJAX request to `/boutique/purchases/order-id`.
|
66
|
+
The `downloads` field of the JSON will include the download URLs.
|
37
67
|
|
38
68
|
Usage
|
39
69
|
=====
|
@@ -41,15 +71,23 @@ Usage
|
|
41
71
|
The web application is for customers, to get information about your products use
|
42
72
|
the included command line application.
|
43
73
|
|
44
|
-
% boutique --
|
45
|
-
% boutique --
|
74
|
+
% boutique --stats productcode
|
75
|
+
% boutique --expire
|
76
|
+
% boutique --expire id
|
77
|
+
% boutique --remove id
|
46
78
|
|
47
79
|
Development
|
48
80
|
===========
|
49
81
|
|
50
|
-
|
51
|
-
|
52
|
-
|
82
|
+
Tests are setup to run individually via `ruby test/*_test.rb` or run them all
|
83
|
+
via `rake`.
|
84
|
+
|
85
|
+
TODO
|
86
|
+
====
|
87
|
+
|
88
|
+
* boutique command line
|
89
|
+
* email customer + recover action
|
90
|
+
* email exceptions to developer
|
53
91
|
|
54
92
|
License
|
55
93
|
=======
|
data/boutique
CHANGED
@@ -1,4 +1,25 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
require
|
2
|
+
require 'optparse'
|
3
|
+
require File.expand_path('lib/boutique', File.dirname(__FILE__))
|
4
|
+
require 'dm-migrations'
|
3
5
|
|
4
|
-
|
6
|
+
ENV['BOUTIQUE_CMD'] = '1'
|
7
|
+
load(ENV['BOUTIQUE_CONFIG'] || 'config.ru')
|
8
|
+
|
9
|
+
module Boutique
|
10
|
+
class Command
|
11
|
+
def self.migrate
|
12
|
+
DataMapper.auto_upgrade!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
ARGV.options do |o|
|
18
|
+
o.set_summary_indent(' ')
|
19
|
+
o.banner = "Usage: #{File.basename($0)} [OPTION]"
|
20
|
+
o.define_head "Admin for boutique"
|
21
|
+
o.on('-m', '--migrate', 'initial migration') { Boutique::Command.migrate; exit }
|
22
|
+
o.on('-h', '--help', 'show this help message') { puts o; exit }
|
23
|
+
o.parse!
|
24
|
+
puts o
|
25
|
+
end
|
data/config.ru
CHANGED
@@ -1,3 +1,18 @@
|
|
1
1
|
require File.expand_path('lib/boutique', File.dirname(__FILE__))
|
2
2
|
|
3
|
-
|
3
|
+
Boutique.configure(!ENV['BOUTIQUE_CMD'].nil?) do |c|
|
4
|
+
c.pem_private File.expand_path('certs/private.pem', File.dirname(__FILE__))
|
5
|
+
c.pem_public File.expand_path('certs/public.pem', File.dirname(__FILE__))
|
6
|
+
c.pem_paypal File.expand_path('certs/paypal.pem', File.dirname(__FILE__))
|
7
|
+
c.download_path '/download'
|
8
|
+
c.download_dir File.expand_path('temp', File.dirname(__FILE__))
|
9
|
+
c.db_adapter 'sqlite3'
|
10
|
+
c.db_host 'localhost'
|
11
|
+
c.db_username 'root'
|
12
|
+
c.db_password 'secret'
|
13
|
+
c.db_database 'db.sqlite3'
|
14
|
+
c.pp_email 'paypal_biz@mailinator.com'
|
15
|
+
c.pp_url 'http://localhost'
|
16
|
+
end
|
17
|
+
|
18
|
+
run Boutique::App if !ENV['BOUTIQUE_CMD']
|
data/lib/boutique.rb
CHANGED
@@ -1,12 +1,307 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'sinatra/base'
|
3
|
+
require 'dm-core'
|
4
|
+
require 'dm-types'
|
5
|
+
require 'cgi'
|
6
|
+
require 'date'
|
7
|
+
require 'digest/sha1'
|
8
|
+
require 'json'
|
9
|
+
require 'openssl'
|
3
10
|
|
4
11
|
module Boutique
|
5
|
-
VERSION = '0.0.
|
12
|
+
VERSION = '0.0.2'
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def configure(setup_db=true)
|
16
|
+
yield Config
|
17
|
+
DataMapper.setup(:default,
|
18
|
+
:adapter => config.db_adapter,
|
19
|
+
:host => config.db_host,
|
20
|
+
:username => config.db_username,
|
21
|
+
:password => config.db_password,
|
22
|
+
:database => config.db_database
|
23
|
+
) if setup_db
|
24
|
+
end
|
25
|
+
|
26
|
+
def config
|
27
|
+
Config
|
28
|
+
end
|
29
|
+
|
30
|
+
def product(code)
|
31
|
+
builder = ProductBuilder.new
|
32
|
+
builder.code(code)
|
33
|
+
yield builder
|
34
|
+
product = Product.first_or_create({:code => code}, builder.to_hash)
|
35
|
+
product.save
|
36
|
+
product
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Config
|
41
|
+
def self.pem_private(value=nil)
|
42
|
+
@pem_private = value if !value.nil?
|
43
|
+
@pem_private
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.pem_public(value=nil)
|
47
|
+
@pem_public = value if !value.nil?
|
48
|
+
@pem_public
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.pem_paypal(value=nil)
|
52
|
+
@pem_paypal = value if !value.nil?
|
53
|
+
@pem_paypal
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.download_path(value=nil)
|
57
|
+
@download_path = value if !value.nil?
|
58
|
+
@download_path
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.download_dir(value=nil)
|
62
|
+
@download_dir = value if !value.nil?
|
63
|
+
@download_dir
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.db_adapter(value=nil)
|
67
|
+
@db_adapter = value if !value.nil?
|
68
|
+
@db_adapter
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.db_host(value=nil)
|
72
|
+
@db_host = value if !value.nil?
|
73
|
+
@db_host
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.db_username(value=nil)
|
77
|
+
@db_username = value if !value.nil?
|
78
|
+
@db_username
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.db_password(value=nil)
|
82
|
+
@db_password = value if !value.nil?
|
83
|
+
@db_password
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.db_database(value=nil)
|
87
|
+
@db_database = value if !value.nil?
|
88
|
+
@db_database
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.pp_email(value=nil)
|
92
|
+
@pp_email = value if !value.nil?
|
93
|
+
@pp_email
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.pp_url(value=nil)
|
97
|
+
@pp_url = value if !value.nil?
|
98
|
+
@pp_url
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class ProductBuilder
|
103
|
+
def code(value=nil)
|
104
|
+
@code = value if !value.nil?
|
105
|
+
@code
|
106
|
+
end
|
107
|
+
|
108
|
+
def name(value=nil)
|
109
|
+
@name = value if !value.nil?
|
110
|
+
@name
|
111
|
+
end
|
112
|
+
|
113
|
+
def files(value=nil)
|
114
|
+
@files = value if !value.nil?
|
115
|
+
@files
|
116
|
+
end
|
117
|
+
|
118
|
+
def price(value=nil)
|
119
|
+
@price = value if !value.nil?
|
120
|
+
@price
|
121
|
+
end
|
122
|
+
|
123
|
+
def return_url(value=nil)
|
124
|
+
@return_url = value if !value.nil?
|
125
|
+
@return_url
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_hash
|
129
|
+
{:code => @code,
|
130
|
+
:name => @name,
|
131
|
+
:files => @files,
|
132
|
+
:price => @price,
|
133
|
+
:return_url => @return_url}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class Product
|
138
|
+
include DataMapper::Resource
|
139
|
+
|
140
|
+
property :id, Serial
|
141
|
+
property :code, String, :required => true, :unique => true
|
142
|
+
property :name, String, :required => true, :unique => true
|
143
|
+
property :files, CommaSeparatedList, :required => true
|
144
|
+
property :price, Decimal, :required => true
|
145
|
+
property :return_url, String, :required => true
|
146
|
+
|
147
|
+
has n, :purchases
|
148
|
+
end
|
149
|
+
|
150
|
+
class Purchase
|
151
|
+
include DataMapper::Resource
|
152
|
+
|
153
|
+
property :id, Serial
|
154
|
+
property :created_at, DateTime
|
155
|
+
property :counter, Integer, :required => true
|
156
|
+
property :secret, String, :required => true
|
157
|
+
property :transaction_id, String
|
158
|
+
property :email, String
|
159
|
+
property :name, String
|
160
|
+
property :completed_at, DateTime
|
161
|
+
property :downloads, CommaSeparatedList
|
162
|
+
|
163
|
+
belongs_to :product
|
164
|
+
|
165
|
+
def initialize(attr = {})
|
166
|
+
attr[:counter] ||= 0
|
167
|
+
attr[:secret] ||= random_hash
|
168
|
+
super
|
169
|
+
end
|
170
|
+
|
171
|
+
def complete(txn_id, email, name)
|
172
|
+
self.transaction_id = txn_id
|
173
|
+
self.email = email
|
174
|
+
self.name = name
|
175
|
+
self.completed_at = DateTime.now
|
176
|
+
link_downloads!
|
177
|
+
end
|
178
|
+
|
179
|
+
def completed?
|
180
|
+
!completed_at.nil? && !transaction_id.nil?
|
181
|
+
end
|
182
|
+
|
183
|
+
def maybe_refresh_downloads!
|
184
|
+
if self.completed? &&
|
185
|
+
(self.downloads.nil? ||
|
186
|
+
self.downloads.any? {|d| !File.exist?(d) })
|
187
|
+
self.link_downloads!
|
188
|
+
self.save
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def link_downloads!
|
193
|
+
return if !completed?
|
194
|
+
self.downloads = product.files.map do |file|
|
195
|
+
linked_file = "/#{Date.today.strftime('%Y%m%d')}-#{random_hash}/#{File.basename(file)}"
|
196
|
+
full_dir = File.dirname("#{Boutique.config.download_dir}#{linked_file}")
|
197
|
+
`mkdir -p #{full_dir}`
|
198
|
+
`ln -s #{file} #{Boutique.config.download_dir}#{linked_file}`
|
199
|
+
"#{Boutique.config.download_path}#{linked_file}"
|
200
|
+
end
|
201
|
+
self.counter += 1
|
202
|
+
end
|
203
|
+
|
204
|
+
def boutique_id
|
205
|
+
(self.id.nil? || self.secret.nil?) ?
|
206
|
+
raise('Cannot get boutique_id for unsaved purchase') :
|
207
|
+
"#{self.id}-#{self.secret}"
|
208
|
+
end
|
209
|
+
|
210
|
+
def random_hash
|
211
|
+
Digest::SHA1.hexdigest("#{DateTime.now}#{rand}")[0..9]
|
212
|
+
end
|
213
|
+
|
214
|
+
def to_json
|
215
|
+
{
|
216
|
+
:id => id,
|
217
|
+
:counter => counter,
|
218
|
+
:completed => completed?,
|
219
|
+
:name => product.name,
|
220
|
+
:code => product.code,
|
221
|
+
:downloads => downloads
|
222
|
+
}.to_json
|
223
|
+
end
|
224
|
+
|
225
|
+
def paypal_form(notify_url)
|
226
|
+
values = {
|
227
|
+
:business => Boutique.config.pp_email,
|
228
|
+
:cmd => '_xclick',
|
229
|
+
:item_name => product.name,
|
230
|
+
:item_number => product.code,
|
231
|
+
:amount => product.price,
|
232
|
+
:currency_code => 'USD',
|
233
|
+
:notify_url => "#{notify_url}/#{boutique_id}",
|
234
|
+
:return => "#{product.return_url}?b=#{boutique_id}"
|
235
|
+
}
|
236
|
+
{'action' => Boutique.config.pp_url,
|
237
|
+
'cmd' => '_s-xclick',
|
238
|
+
'encrypted' => encrypt(values)}
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
def encrypt(values)
|
243
|
+
signed = OpenSSL::PKCS7::sign(
|
244
|
+
OpenSSL::X509::Certificate.new(File.read(Boutique.config.pem_public)),
|
245
|
+
OpenSSL::PKey::RSA.new(File.read(Boutique.config.pem_private), ''),
|
246
|
+
values.map { |k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("\n"),
|
247
|
+
[],
|
248
|
+
OpenSSL::PKCS7::BINARY)
|
249
|
+
OpenSSL::PKCS7::encrypt(
|
250
|
+
[OpenSSL::X509::Certificate.new(File.read(Boutique.config.pem_paypal))],
|
251
|
+
signed.to_der,
|
252
|
+
OpenSSL::Cipher::Cipher::new("DES3"),
|
253
|
+
OpenSSL::PKCS7::BINARY).to_s.gsub("\n", "")
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
DataMapper.finalize
|
6
258
|
|
7
259
|
class App < Sinatra::Base
|
8
|
-
|
9
|
-
|
260
|
+
post '/boutique/buy/:code' do
|
261
|
+
product = Boutique::Product.first(:code => params[:code])
|
262
|
+
if product.nil?
|
263
|
+
halt(404, "product #{params[:code]} not found")
|
264
|
+
end
|
265
|
+
purchase = Boutique::Purchase.new
|
266
|
+
product.purchases << purchase
|
267
|
+
product.save
|
268
|
+
form = purchase.paypal_form("http://#{request.host}/notify")
|
269
|
+
"<!doctype html><html><head><title>Redirecting to PayPal...</title>" +
|
270
|
+
"<meta charset='utf-8'></head><body>" +
|
271
|
+
"<form name='paypal' action='#{form['action']}'>" +
|
272
|
+
"<input type='hidden' name='cmd' value='#{form['cmd']}'>" +
|
273
|
+
"<input type='hidden' name='encrypted' value='#{form['encrypted']}'>" +
|
274
|
+
"<input type='submit' value='submit' style='visibility:hidden'>" +
|
275
|
+
"</form><script>document.paypal.submit();</script></body></html>"
|
276
|
+
end
|
277
|
+
|
278
|
+
post '/boutique/notify/:boutique_id' do
|
279
|
+
purchase = get_purchase(params[:boutique_id])
|
280
|
+
if !purchase.completed? &&
|
281
|
+
params['txn_id'] &&
|
282
|
+
params['payment_status'] &&
|
283
|
+
params['receiver_email'] == Boutique.config.pp_email
|
284
|
+
purchase.complete(params['txn_id'], params['payer_email'], params['first_name'])
|
285
|
+
purchase.save
|
286
|
+
end
|
287
|
+
''
|
288
|
+
end
|
289
|
+
|
290
|
+
get '/boutique/record/:boutique_id' do
|
291
|
+
purchase = get_purchase(params[:boutique_id])
|
292
|
+
purchase.maybe_refresh_downloads!
|
293
|
+
params['jsonp'].nil? ?
|
294
|
+
purchase.to_json :
|
295
|
+
"#{params['jsonp']}(#{purchase.to_json})"
|
296
|
+
end
|
297
|
+
|
298
|
+
def get_purchase(boutique_id)
|
299
|
+
id, secret = boutique_id.split('-')
|
300
|
+
purchase = Boutique::Purchase.get(id)
|
301
|
+
if purchase.nil? || purchase.secret != secret
|
302
|
+
halt(404, "purchase #{params[:boutique_id]} not found")
|
303
|
+
end
|
304
|
+
purchase
|
10
305
|
end
|
11
306
|
end
|
12
307
|
end
|
data/test/app_test.rb
CHANGED
@@ -1,16 +1,62 @@
|
|
1
|
-
require '
|
2
|
-
require 'minitest/autorun'
|
3
|
-
require 'rack'
|
4
|
-
require 'rack/test'
|
5
|
-
require 'rack/server'
|
1
|
+
require File.expand_path('helper', File.dirname(__FILE__))
|
6
2
|
|
7
|
-
class
|
3
|
+
class AppTest < BoutiqueTest
|
8
4
|
include Rack::Test::Methods
|
9
5
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
def test_redirect_to_paypal
|
7
|
+
ebook_product.save
|
8
|
+
post '/boutique/buy/ebook'
|
9
|
+
|
10
|
+
purchase = Boutique::Purchase.first
|
11
|
+
refute(purchase.nil?)
|
12
|
+
assert(last_response.ok?)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_purchase_non_existing_product
|
16
|
+
post '/boutique/buy/non-existing-product'
|
17
|
+
assert(last_response.not_found?)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_notify
|
21
|
+
product = ebook_product
|
22
|
+
purchase = Boutique::Purchase.new
|
23
|
+
product.purchases << purchase
|
24
|
+
product.save
|
25
|
+
refute(purchase.completed?)
|
26
|
+
|
27
|
+
post "/boutique/notify/#{purchase.boutique_id}?payment_status=Completed&txn_id=1337&receiver_email=#{Boutique.config.pp_email}"
|
28
|
+
assert(last_response.ok?)
|
29
|
+
|
30
|
+
purchase.reload
|
31
|
+
assert(purchase.completed?)
|
32
|
+
assert_equal('1337', purchase.transaction_id)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_notify_not_found
|
36
|
+
post "/boutique/notify/99-notfound"
|
37
|
+
assert(last_response.not_found?)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_record
|
41
|
+
product = ebook_product
|
42
|
+
purchase = Boutique::Purchase.new
|
43
|
+
product.purchases << purchase
|
44
|
+
product.save
|
45
|
+
|
46
|
+
get "/boutique/record/#{purchase.boutique_id}"
|
47
|
+
assert(last_response.ok?)
|
48
|
+
|
49
|
+
json = JSON.parse(last_response.body)
|
50
|
+
assert(json['id'])
|
51
|
+
assert_equal('ebook', json['code'])
|
52
|
+
assert_equal('Ebook', json['name'])
|
53
|
+
assert_equal(0, json['counter'])
|
54
|
+
refute(json['completed'])
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_record_not_found
|
58
|
+
get "/boutique/record/99-notfound"
|
59
|
+
assert(last_response.not_found?)
|
14
60
|
end
|
15
61
|
|
16
62
|
private
|
data/test/config_test.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.expand_path('helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class ConfigTest < BoutiqueTest
|
4
|
+
def test_db
|
5
|
+
assert_equal(File.expand_path('../certs/private.pem', File.dirname(__FILE__)), Boutique.config.pem_private)
|
6
|
+
assert_equal(File.expand_path('../certs/public.pem', File.dirname(__FILE__)), Boutique.config.pem_public)
|
7
|
+
assert_equal(File.expand_path('../certs/private.pem', File.dirname(__FILE__)), Boutique.config.pem_private)
|
8
|
+
assert_equal('/download', Boutique.config.download_path)
|
9
|
+
assert_equal(File.expand_path('../temp', File.dirname(__FILE__)), Boutique.config.download_dir)
|
10
|
+
assert_equal('sqlite3', Boutique.config.db_adapter)
|
11
|
+
assert_equal('localhost', Boutique.config.db_host)
|
12
|
+
assert_equal('root', Boutique.config.db_username)
|
13
|
+
assert_equal('secret', Boutique.config.db_password)
|
14
|
+
assert_equal('db.sqlite3', Boutique.config.db_database)
|
15
|
+
assert_equal('paypal_biz@mailinator.com', Boutique.config.pp_email)
|
16
|
+
assert_equal('http://localhost', Boutique.config.pp_url)
|
17
|
+
end
|
18
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require File.expand_path('../lib/boutique', File.dirname(__FILE__))
|
2
|
+
require 'dm-migrations'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'rack'
|
5
|
+
require 'rack/test'
|
6
|
+
require 'rack/server'
|
7
|
+
require 'fileutils'
|
8
|
+
|
9
|
+
DataMapper.setup(:default, 'sqlite::memory:')
|
10
|
+
DataMapper.auto_migrate!
|
11
|
+
|
12
|
+
class BoutiqueTest < MiniTest::Unit::TestCase
|
13
|
+
def setup
|
14
|
+
Boutique::Purchase.all.destroy
|
15
|
+
Boutique::Product.all.destroy
|
16
|
+
Boutique.configure(false) do |c|
|
17
|
+
c.pem_private File.expand_path('../certs/private.pem', File.dirname(__FILE__))
|
18
|
+
c.pem_public File.expand_path('../certs/public.pem', File.dirname(__FILE__))
|
19
|
+
c.pem_paypal File.expand_path('../certs/paypal.pem', File.dirname(__FILE__))
|
20
|
+
c.download_path '/download'
|
21
|
+
c.download_dir File.expand_path('../temp', File.dirname(__FILE__))
|
22
|
+
c.db_adapter 'sqlite3'
|
23
|
+
c.db_host 'localhost'
|
24
|
+
c.db_username 'root'
|
25
|
+
c.db_password 'secret'
|
26
|
+
c.db_database 'db.sqlite3'
|
27
|
+
c.pp_email 'paypal_biz@mailinator.com'
|
28
|
+
c.pp_url 'http://localhost'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def teardown
|
33
|
+
FileUtils.rm_rf(File.expand_path('../temp', File.dirname(__FILE__)))
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def ebook_product
|
38
|
+
Boutique::Product.new(
|
39
|
+
:code => 'ebook',
|
40
|
+
:name => 'Ebook',
|
41
|
+
:files => [File.expand_path('../README.md', File.dirname(__FILE__))],
|
42
|
+
:price => 10.5,
|
43
|
+
:return_url => 'http://zincmade.com')
|
44
|
+
end
|
45
|
+
end
|
data/test/model_test.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.expand_path('helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class ModelTest < BoutiqueTest
|
4
|
+
def test_purchase_create
|
5
|
+
product = ebook_product
|
6
|
+
product.save
|
7
|
+
count = Boutique::Purchase.count
|
8
|
+
purchase = Boutique::Purchase.create({})
|
9
|
+
product.purchases << purchase
|
10
|
+
product.save
|
11
|
+
|
12
|
+
assert_equal(count + 1, Boutique::Purchase.count)
|
13
|
+
assert_equal(0, purchase.counter)
|
14
|
+
assert_nil(purchase.transaction_id)
|
15
|
+
assert_nil(purchase.completed_at)
|
16
|
+
assert_nil(purchase.downloads)
|
17
|
+
refute_nil(purchase.secret)
|
18
|
+
refute(purchase.completed?)
|
19
|
+
|
20
|
+
purchase.complete('1', 'john@mailinator.com', 'John')
|
21
|
+
purchase.save
|
22
|
+
assert_equal('1', purchase.transaction_id)
|
23
|
+
refute_nil(purchase.completed_at)
|
24
|
+
assert(purchase.completed?)
|
25
|
+
assert_match(%r(/download/[^/]+/README.md), purchase.downloads[0])
|
26
|
+
|
27
|
+
old_download = purchase.downloads[0]
|
28
|
+
`rm #{Boutique.config.download_dir}#{old_download.sub(Boutique.config.download_path, '')}`
|
29
|
+
purchase.maybe_refresh_downloads!
|
30
|
+
refute_equal(old_download, purchase.downloads[0])
|
31
|
+
|
32
|
+
bid = purchase.boutique_id
|
33
|
+
assert_equal(purchase.id, bid.split('-')[0].to_i)
|
34
|
+
assert_equal(10, bid.split('-')[1].size)
|
35
|
+
|
36
|
+
form = purchase.paypal_form('http://localhost/notify')
|
37
|
+
assert_equal('http://localhost', form['action'])
|
38
|
+
assert_equal('_s-xclick', form['cmd'])
|
39
|
+
refute_nil(form['encrypted'])
|
40
|
+
|
41
|
+
json = JSON.parse(purchase.to_json)
|
42
|
+
assert_equal(purchase.id, json['id'])
|
43
|
+
assert_equal(2, json['counter'])
|
44
|
+
assert(json['completed'])
|
45
|
+
assert_equal('Ebook', json['name'])
|
46
|
+
assert_equal('ebook', json['code'])
|
47
|
+
refute_nil(json['downloads'])
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_product_create
|
51
|
+
count = Boutique::Product.count
|
52
|
+
Boutique.product('icon-set') do |p|
|
53
|
+
p.name 'Icon Set'
|
54
|
+
p.files [File.expand_path('../README.md', File.dirname(__FILE__))]
|
55
|
+
p.price 10.5
|
56
|
+
p.return_url 'http://zincmade.com'
|
57
|
+
end
|
58
|
+
assert_equal(count + 1, Boutique::Product.count)
|
59
|
+
|
60
|
+
set = Boutique::Product.first(:code => 'icon-set')
|
61
|
+
assert_equal('Icon Set', set.name)
|
62
|
+
assert_equal(File.expand_path('../README.md', File.dirname(__FILE__)), set.files[0])
|
63
|
+
assert_equal(10.5, set.price)
|
64
|
+
assert_equal('http://zincmade.com', set.return_url)
|
65
|
+
assert_equal(0, set.purchases.size)
|
66
|
+
end
|
67
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: boutique
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: .
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-01-
|
12
|
+
date: 2012-01-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sinatra
|
16
|
-
requirement: &
|
16
|
+
requirement: &70111143120740 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70111143120740
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: minitest
|
27
|
-
requirement: &
|
27
|
+
requirement: &70111143120020 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70111143120020
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: shotgun
|
38
|
-
requirement: &
|
38
|
+
requirement: &70111143119480 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,7 +43,7 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70111143119480
|
47
47
|
description: A Sinatra module which accepts payments via PayPal and gives customers
|
48
48
|
a secret URL to download your product.
|
49
49
|
email:
|
@@ -59,6 +59,9 @@ files:
|
|
59
59
|
- boutique
|
60
60
|
- lib/boutique.rb
|
61
61
|
- test/app_test.rb
|
62
|
+
- test/config_test.rb
|
63
|
+
- test/helper.rb
|
64
|
+
- test/model_test.rb
|
62
65
|
- ./boutique
|
63
66
|
homepage: https://github.com/hughbien/boutique
|
64
67
|
licenses: []
|