boutique 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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.store_path 'boutique'
19
- c.download_path 'download'
20
- c.paypal_key 'paypalapikey'
21
- c.db_username 'username'
22
- c.db_password 'password'
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.add('Icon Set') do |p|
26
- p.id 'icon-set'
27
- p.file '/home/hugh/icon-set.tgz'
28
- p.price 10.5
29
- p.return_url 'http://zincmade.com/'
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
- # start purchase at /boutique/icon-set/
34
- # download page at /boutique/long-random-hex-hash/
35
- # actual file at /download/long-random-hex-hash/icon-set.tgz
36
- run Boutique::App
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 --list
45
- % boutique --stats id --after date --before date
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
- The `config.ru` is for local development. Start the server via `shotgun`
51
- command. Tests are setup to run individually via `ruby test/*_test.rb` or
52
- run them all via `rake`.
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 File.expand_path('../lib/boutique', File.dirname(__FILE__))
2
+ require 'optparse'
3
+ require File.expand_path('lib/boutique', File.dirname(__FILE__))
4
+ require 'dm-migrations'
3
5
 
4
- Boutique.run!
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
- run Boutique::App
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.1'
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
- get '/' do
9
- 'test'
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 'rubygems'
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 BoutiqueTest < MiniTest::Unit::TestCase
3
+ class AppTest < BoutiqueTest
8
4
  include Rack::Test::Methods
9
5
 
10
- def test_ok
11
- get '/'
12
- assert last_response.ok?
13
- assert_equal 'test', last_response.body
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
@@ -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
@@ -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.1
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-26 00:00:00.000000000 Z
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: &70112066891200 !ruby/object:Gem::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: *70112066891200
24
+ version_requirements: *70111143120740
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: minitest
27
- requirement: &70112066876420 !ruby/object:Gem::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: *70112066876420
35
+ version_requirements: *70111143120020
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: shotgun
38
- requirement: &70112066875860 !ruby/object:Gem::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: *70112066875860
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: []