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 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: []