forked from solidusio/solidus
-
Notifications
You must be signed in to change notification settings - Fork 3
/
product_spec.rb
466 lines (386 loc) · 14.6 KB
/
product_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# coding: UTF-8
require 'spec_helper'
module ThirdParty
class Extension < Spree::Base
# nasty hack so we don't have to create a table to back this fake model
self.table_name = 'spree_products'
end
end
describe Spree::Product, :type => :model do
context 'product instance' do
let(:product) { create(:product) }
let(:variant) { create(:variant, :product => product) }
context '#duplicate' do
before do
allow(product).to receive_messages :taxons => [create(:taxon)]
end
it 'duplicates product' do
clone = product.duplicate
expect(clone.name).to eq('COPY OF ' + product.name)
expect(clone.master.sku).to eq('COPY OF ' + product.master.sku)
expect(clone.taxons).to eq(product.taxons)
expect(clone.images.size).to eq(product.images.size)
end
it 'calls #duplicate_extra' do
expect_any_instance_of(Spree::Product).to receive(:duplicate_extra) do |product, old_product|
product.name = old_product.name.reverse
end
clone = product.duplicate
expect(clone.name).to eq(product.name.reverse)
end
end
context "master variant" do
context "when master variant changed" do
before do
product.master.sku = "Something changed"
end
it "saves the master" do
expect(product.master).to receive(:save!)
product.save
end
end
context "when master default price changed" do
before do
master = product.master
master.default_price.price = 11
master.save!
product.master.default_price.price = 12
end
it "saves the master" do
expect(product.master).to receive(:save!)
product.save
end
it "saves the default price" do
expect(product.master.default_price).to receive(:save)
product.save
end
end
context "when master variant and price haven't changed" do
it "does not save the master" do
expect(product.master).not_to receive(:save!)
product.save
end
end
end
context "product has no variants" do
context "#destroy" do
it "should set deleted_at value" do
product.destroy
expect(product.deleted_at).not_to be_nil
expect(product.master.reload.deleted_at).not_to be_nil
end
end
end
context "product has variants" do
before do
create(:variant, :product => product)
end
context "#destroy" do
it "should set deleted_at value" do
product.destroy
expect(product.deleted_at).not_to be_nil
expect(product.variants_including_master.all? { |v| !v.deleted_at.nil? }).to be true
end
end
end
context "#price" do
# Regression test for #1173
it 'strips non-price characters' do
product.price = "$10"
expect(product.price).to eq(10.0)
end
end
context "#display_price" do
before { product.price = 10.55 }
it "shows the amount" do
expect(product.display_price.to_s).to eq("$10.55")
end
context "with currency set to JPY" do
before do
product.master.default_price.currency = 'JPY'
product.master.default_price.save!
Spree::Config[:currency] = 'JPY'
end
it "displays the currency in yen" do
expect(product.display_price.to_s).to eq("¥11")
end
end
end
context "#available?" do
it "should be available if date is in the past" do
product.available_on = 1.day.ago
expect(product).to be_available
end
it "should not be available if date is nil or in the future" do
product.available_on = nil
expect(product).not_to be_available
product.available_on = 1.day.from_now
expect(product).not_to be_available
end
it "should not be available if destroyed" do
product.destroy
expect(product).not_to be_available
end
end
context "variants_and_option_values" do
let!(:high) { create(:variant, product: product) }
let!(:low) { create(:variant, product: product) }
before { high.option_values.destroy_all }
it "returns only variants with option values" do
expect(product.variants_and_option_values).to eq([low])
end
end
describe 'Variants sorting' do
let(:master){ product.master }
let!(:second) { create(:variant, product: product) }
let!(:third) { create(:variant, product: product) }
let!(:first) { create(:variant, product: product) }
before do
first.update_columns(position: 2)
second.update_columns(position: 3)
third.update_columns(position: 4)
end
context 'without master variant' do
it 'sorts variants by position' do
expect(product.variants).to eq([first, second, third])
end
end
context 'with master variant' do
it 'sorts variants by position' do
expect(product.variants_including_master).to eq([master, first, second, third])
end
end
end
context "has stock movements" do
let(:product) { create(:product) }
let(:variant) { product.master }
let(:stock_item) { variant.stock_items.first }
it "doesnt raise ReadOnlyRecord error" do
Spree::StockMovement.create!(stock_item: stock_item, quantity: 1)
expect { product.destroy }.not_to raise_error
end
end
# Regression test for #3737
context "has stock items" do
let(:product) { create(:product) }
it "can retrieve stock items" do
expect(product.master.stock_items.first).not_to be_nil
expect(product.stock_items.first).not_to be_nil
end
end
context "slugs" do
it "normalizes slug on update validation" do
product.slug = "hey//joe"
product.valid?
expect(product.slug).not_to match "/"
end
it "renames slug on destroy" do
old_slug = product.slug
product.destroy
expect(old_slug).to_not eq product.slug
end
it "validates slug uniqueness" do
existing_product = product
new_product = create(:product)
new_product.slug = existing_product.slug
expect(new_product.valid?).to eq false
end
it "falls back to 'name-sku' for slug if regular name-based slug already in use" do
product1 = build(:product)
product1.name = "test"
product1.sku = "123"
product1.save!
product2 = build(:product)
product2.name = "test"
product2.sku = "456"
product2.save!
expect(product2.slug).to eq 'test-456'
end
end
context "associations" do
describe "product_option_types" do
it "touches the product instance when an option type is added" do
expect {
product.product_option_types.create(option_type: create(:option_type, name: 'new-option-type'))
product.reload
}.to change { product.updated_at }
end
it "touches product instance when an option type is removed" do
product.product_option_types.create(option_type: create(:option_type, name: 'new-option-type'))
expect {
product.product_option_types = []
product.reload
}.to change { product.updated_at }
end
end
end
end
context "properties" do
let(:product) { create(:product) }
it "should properly assign properties" do
product.set_property('the_prop', 'value1')
expect(product.property('the_prop')).to eq('value1')
product.set_property('the_prop', 'value2')
expect(product.property('the_prop')).to eq('value2')
end
it "should not create duplicate properties when set_property is called" do
expect {
product.set_property('the_prop', 'value2')
product.save
product.reload
}.not_to change(product.properties, :length)
expect {
product.set_property('the_prop_new', 'value')
product.save
product.reload
expect(product.property('the_prop_new')).to eq('value')
}.to change { product.properties.length }.by(1)
end
# Regression test for #2455
it "should not overwrite properties' presentation names" do
Spree::Property.where(:name => 'foo').first_or_create!(:presentation => "Foo's Presentation Name")
product.set_property('foo', 'value1')
product.set_property('bar', 'value2')
expect(Spree::Property.where(:name => 'foo').first.presentation).to eq("Foo's Presentation Name")
expect(Spree::Property.where(:name => 'bar').first.presentation).to eq("bar")
end
# Regression test for #4416
context "#possible_promotions" do
let!(:promotion) do
create(:promotion, advertise: true, starts_at: 1.day.ago)
end
let!(:rule) do
Spree::Promotion::Rules::Product.create(
promotion: promotion,
products: [product]
)
end
it "lists the promotion as a possible promotion" do
expect(product.possible_promotions).to include(promotion)
end
end
end
context '#create' do
let!(:prototype) { create(:prototype) }
let!(:product) { Spree::Product.new(name: "Foo", price: 1.99, shipping_category_id: create(:shipping_category).id) }
before { product.prototype_id = prototype.id }
context "when prototype is supplied" do
it "should create properties based on the prototype" do
product.save
expect(product.properties.count).to eq(1)
end
end
context "when prototype with option types is supplied" do
def build_option_type_with_values(name, values)
ot = create(:option_type, :name => name)
values.each do |val|
ot.option_values.create(:name => val.downcase, :presentation => val)
end
ot
end
let(:prototype) do
size = build_option_type_with_values("size", %w(Small Medium Large))
create(:prototype, :name => "Size", :option_types => [ size ])
end
let(:option_values_hash) do
hash = {}
prototype.option_types.each do |i|
hash[i.id.to_s] = i.option_value_ids
end
hash
end
it "should create option types based on the prototype" do
product.save
expect(product.option_type_ids.length).to eq(1)
expect(product.option_type_ids).to eq(prototype.option_type_ids)
end
it "should create product option types based on the prototype" do
product.save
expect(product.product_option_types.pluck(:option_type_id)).to eq(prototype.option_type_ids)
end
it "should create variants from an option values hash with one option type" do
product.option_values_hash = option_values_hash
product.save
expect(product.variants.length).to eq(3)
end
it "should still create variants when option_values_hash is given but prototype id is nil" do
product.option_values_hash = option_values_hash
product.prototype_id = nil
product.save
expect(product.option_type_ids.length).to eq(1)
expect(product.option_type_ids).to eq(prototype.option_type_ids)
expect(product.variants.length).to eq(3)
end
it "should create variants from an option values hash with multiple option types" do
color = build_option_type_with_values("color", %w(Red Green Blue))
logo = build_option_type_with_values("logo", %w(Ruby Rails Nginx))
option_values_hash[color.id.to_s] = color.option_value_ids
option_values_hash[logo.id.to_s] = logo.option_value_ids
product.option_values_hash = option_values_hash
product.save
product.reload
expect(product.option_type_ids.length).to eq(3)
expect(product.variants.length).to eq(27)
end
end
end
context "#images" do
let(:product) { create(:product) }
let(:image) { File.open(File.expand_path('../../../fixtures/thinking-cat.jpg', __FILE__)) }
let(:params) { {:viewable_id => product.master.id, :viewable_type => 'Spree::Variant', :attachment => image, :alt => "position 2", :position => 2} }
before do
Spree::Image.create(params)
Spree::Image.create(params.merge({:alt => "position 1", :position => 1}))
Spree::Image.create(params.merge({:viewable_type => 'ThirdParty::Extension', :alt => "position 1", :position => 2}))
end
it "only looks for variant images" do
expect(product.images.size).to eq(2)
end
it "should be sorted by position" do
expect(product.images.pluck(:alt)).to eq(["position 1", "position 2"])
end
end
# Regression tests for #2352
context "classifications and taxons" do
it "is joined through classifications" do
reflection = Spree::Product.reflect_on_association(:taxons)
expect(reflection.options[:through]).to eq(:classifications)
end
it "will delete all classifications" do
reflection = Spree::Product.reflect_on_association(:classifications)
expect(reflection.options[:dependent]).to eq(:delete_all)
end
end
context '#total_on_hand' do
it 'should be infinite if track_inventory_levels is false' do
Spree::Config[:track_inventory_levels] = false
expect(build(:product, :variants_including_master => [build(:master_variant)]).total_on_hand).to eql(Float::INFINITY)
end
it 'should be infinite if variant is on demand' do
Spree::Config[:track_inventory_levels] = true
expect(build(:product, :variants_including_master => [build(:on_demand_master_variant)]).total_on_hand).to eql(Float::INFINITY)
end
it 'should return sum of stock items count_on_hand' do
product = create(:product)
product.stock_items.first.set_count_on_hand 5
product.variants_including_master(true) # force load association
expect(product.total_on_hand).to eql(5)
end
it 'should return sum of stock items count_on_hand when variants_including_master is not loaded' do
product = create(:product)
product.stock_items.first.set_count_on_hand 5
expect(product.reload.total_on_hand).to eql(5)
end
end
# Regression spec for https://github.com/spree/spree/issues/5588
context '#validate_master when duplicate SKUs entered' do
let!(:first_product) { create(:product, sku: 'a-sku') }
let(:second_product) { build(:product, sku: 'a-sku') }
subject { second_product }
it { is_expected.to be_invalid }
end
it "initializes a master variant when building a product" do
product = Spree::Product.new
expect(product.master.is_master).to be true
end
end