Files
openfoodnetwork/spec/models/spree/variant_spec.rb
Pau Perez 96b8c8ac2c Refresh products cache also on Variant#delete
Note that, as explained in
https://apidock.com/rails/v3.2.13/ActiveRecord/Relation/delete, `delete` does
not trigger callbacks and so it skips the products cache logic.

If we still want to avoid instantiating the AR object, we need to explicitly
call that logic for the cache to be up-to-date.
2019-03-22 09:57:15 +01:00

576 lines
21 KiB
Ruby

require 'spec_helper'
require 'open_food_network/option_value_namer'
require 'open_food_network/products_cache'
module Spree
describe Variant do
describe "double loading" do
# app/models/spree/variant_decorator.rb may be double-loaded in delayed job environment,
# so we need to be able to do so without error.
it "succeeds without error" do
load "#{Rails.root}/app/models/spree/variant_decorator.rb"
end
end
describe "scopes" do
it "finds non-deleted variants" do
v_not_deleted = create(:variant)
v_deleted = create(:variant, deleted_at: Time.zone.now)
Spree::Variant.not_deleted.should include v_not_deleted
Spree::Variant.not_deleted.should_not include v_deleted
end
describe "finding variants in a distributor" do
let!(:d1) { create(:distributor_enterprise) }
let!(:d2) { create(:distributor_enterprise) }
let!(:p1) { create(:simple_product) }
let!(:p2) { create(:simple_product) }
let!(:oc1) { create(:simple_order_cycle, distributors: [d1], variants: [p1.master]) }
let!(:oc2) { create(:simple_order_cycle, distributors: [d2], variants: [p2.master]) }
it "shows variants in an order cycle distribution" do
Variant.in_distributor(d1).should == [p1.master]
end
it "doesn't show duplicates" do
oc_dup = create(:simple_order_cycle, distributors: [d1], variants: [p1.master])
Variant.in_distributor(d1).should == [p1.master]
end
end
describe "finding variants in an order cycle" do
let!(:d1) { create(:distributor_enterprise) }
let!(:d2) { create(:distributor_enterprise) }
let!(:p1) { create(:product) }
let!(:p2) { create(:product) }
let!(:oc1) { create(:simple_order_cycle, distributors: [d1], variants: [p1.master]) }
let!(:oc2) { create(:simple_order_cycle, distributors: [d2], variants: [p2.master]) }
it "shows variants in an order cycle" do
Variant.in_order_cycle(oc1).should == [p1.master]
end
it "doesn't show duplicates" do
ex = create(:exchange, order_cycle: oc1, sender: oc1.coordinator, receiver: d2)
ex.variants << p1.master
Variant.in_order_cycle(oc1).should == [p1.master]
end
end
describe "finding variants for an order cycle and hub" do
let(:oc) { create(:simple_order_cycle) }
let(:s) { create(:supplier_enterprise) }
let(:d1) { create(:distributor_enterprise) }
let(:d2) { create(:distributor_enterprise) }
let(:p1) { create(:simple_product) }
let(:p2) { create(:simple_product) }
let(:v1) { create(:variant, product: p1) }
let(:v2) { create(:variant, product: p2) }
let(:p_external) { create(:simple_product) }
let(:v_external) { create(:variant, product: p_external) }
let!(:ex_in) { create(:exchange, order_cycle: oc, sender: s, receiver: oc.coordinator,
incoming: true, variants: [v1, v2])
}
let!(:ex_out1) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d1,
incoming: false, variants: [v1])
}
let!(:ex_out2) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d2,
incoming: false, variants: [v2])
}
it "returns variants in the order cycle and distributor" do
p1.variants.for_distribution(oc, d1).should == [v1]
p2.variants.for_distribution(oc, d2).should == [v2]
end
it "does not return variants in the order cycle but not the distributor" do
p1.variants.for_distribution(oc, d2).should be_empty
p2.variants.for_distribution(oc, d1).should be_empty
end
it "does not return variants not in the order cycle" do
p_external.variants.for_distribution(oc, d1).should be_empty
end
end
describe "finding variants based on visiblity in inventory" do
let(:enterprise) { create(:distributor_enterprise) }
let!(:new_variant) { create(:variant) }
let!(:hidden_variant) { create(:variant) }
let!(:visible_variant) { create(:variant) }
let!(:hidden_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: hidden_variant, visible: false ) }
let!(:visible_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: visible_variant, visible: true ) }
context "finding variants that are not hidden from an enterprise's inventory" do
context "when the enterprise given is nil" do
let!(:variants) { Spree::Variant.not_hidden_for(nil) }
it "returns an empty list" do
expect(variants).to eq []
end
end
context "when an enterprise is given" do
let!(:variants) { Spree::Variant.not_hidden_for(enterprise) }
it "lists any variants that are not listed as visible=false" do
expect(variants).to include new_variant, visible_variant
expect(variants).to_not include hidden_variant
end
context "when inventory items exist for other enterprises" do
let(:other_enterprise) { create(:distributor_enterprise) }
let!(:new_inventory_item) { create(:inventory_item, enterprise: other_enterprise, variant: new_variant, visible: true ) }
let!(:hidden_inventory_item2) { create(:inventory_item, enterprise: other_enterprise, variant: visible_variant, visible: false ) }
let!(:visible_inventory_item2) { create(:inventory_item, enterprise: other_enterprise, variant: hidden_variant, visible: true ) }
it "lists any variants that are not listed as visible=false only for the relevant enterprise" do
expect(variants).to include new_variant, visible_variant
expect(variants).to_not include hidden_variant
end
end
end
end
context "finding variants that are visible in an enterprise's inventory" do
let!(:variants) { Spree::Variant.visible_for(enterprise) }
it "lists any variants that are listed as visible=true" do
expect(variants).to include visible_variant
expect(variants).to_not include new_variant, hidden_variant
end
end
end
describe 'stockable_by' do
let(:shop) { create(:distributor_enterprise) }
let(:add_to_oc_producer) { create(:supplier_enterprise) }
let(:other_producer) { create(:supplier_enterprise) }
let!(:v1) { create(:variant, product: create(:simple_product, supplier: shop ) ) }
let!(:v2) { create(:variant, product: create(:simple_product, supplier: add_to_oc_producer ) ) }
let!(:v3) { create(:variant, product: create(:simple_product, supplier: other_producer ) ) }
before do
create(:enterprise_relationship, parent: add_to_oc_producer, child: shop, permissions_list: [:add_to_order_cycle])
create(:enterprise_relationship, parent: other_producer, child: shop, permissions_list: [:manage_products])
end
it 'shows variants produced by the enterprise and any producers granting P-OC' do
stockable_variants = Spree::Variant.stockable_by(shop)
expect(stockable_variants).to include v1, v2
expect(stockable_variants).to_not include v3
end
end
end
describe "callbacks" do
let(:variant) { create(:variant) }
it "refreshes the products cache on save" do
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).with(variant)
variant.sku = 'abc123'
variant.save
end
it "refreshes the products cache on destroy" do
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
variant.destroy
end
context "when it is the master variant" do
let(:product) { create(:simple_product) }
let(:master) { product.master }
it "refreshes the products cache for the entire product on save" do
expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product)
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).never
master.sku = 'abc123'
master.save
end
it "refreshes the products cache for the entire product on destroy" do
expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product)
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).never
master.destroy
end
end
end
describe "indexing variants by id" do
let!(:v1) { create(:variant) }
let!(:v2) { create(:variant) }
let!(:v3) { create(:variant) }
it "indexes variants by id" do
Variant.where(id: [v1, v2, v3]).indexed.should ==
{v1.id => v1, v2.id => v2, v3.id => v3}
end
end
describe "generating the product and variant name" do
let(:v) { Variant.new }
let(:p) { double(:product, name: 'product') }
before { allow(v).to receive(:product) { p } }
context "when full_name starts with the product name" do
before { allow(v).to receive(:full_name) { p.name + " - something" } }
it "does not show the product name twice" do
v.product_and_full_name.should == 'product - something'
end
end
context "when full_name does not start with the product name" do
before { allow(v).to receive(:full_name) { "display_name (unit)" } }
it "prepends the product name to the full name" do
v.product_and_full_name.should == 'product - display_name (unit)'
end
end
end
describe "calculating the price with enterprise fees" do
it "returns the price plus the fees" do
distributor = double(:distributor)
order_cycle = double(:order_cycle)
variant = Variant.new price: 100
variant.should_receive(:fees_for).with(distributor, order_cycle) { 23 }
variant.price_with_fees(distributor, order_cycle).should == 123
end
end
describe "calculating the fees" do
it "delegates to EnterpriseFeeCalculator" do
distributor = double(:distributor)
order_cycle = double(:order_cycle)
variant = Variant.new
OpenFoodNetwork::EnterpriseFeeCalculator.any_instance.should_receive(:fees_for).with(variant) { 23 }
variant.fees_for(distributor, order_cycle).should == 23
end
end
describe "calculating fees broken down by fee type" do
it "delegates to EnterpriseFeeCalculator" do
distributor = double(:distributor)
order_cycle = double(:order_cycle)
variant = Variant.new
fees = double(:fees)
OpenFoodNetwork::EnterpriseFeeCalculator.any_instance.should_receive(:fees_by_type_for).with(variant) { fees }
variant.fees_by_type_for(distributor, order_cycle).should == fees
end
end
context "when the product has variants" do
let!(:product) { create(:simple_product) }
let!(:variant) { create(:variant, product: product) }
%w(weight volume).each do |unit|
context "when the product's unit is #{unit}" do
before do
product.update_attribute :variant_unit, unit
product.reload
end
it "is valid when unit value is set and unit description is not" do
variant.unit_value = 1
variant.unit_description = nil
variant.should be_valid
end
it "is invalid when unit value is not set" do
variant.unit_value = nil
variant.should_not be_valid
end
it "has a valid master variant" do
product.master.should be_valid
end
end
end
context "when the product's unit is items" do
before do
product.update_attribute :variant_unit, 'items'
product.reload
end
it "is valid with only unit value set" do
variant.unit_value = 1
variant.unit_description = nil
variant.should be_valid
end
it "is valid with only unit description set" do
variant.unit_value = nil
variant.unit_description = 'Medium'
variant.should be_valid
end
it "is invalid when neither unit value nor unit description are set" do
variant.unit_value = nil
variant.unit_description = nil
variant.should_not be_valid
end
it "has a valid master variant" do
product.master.should be_valid
end
end
end
describe "unit value/description" do
describe "generating the full name" do
let(:v) { Variant.new }
before do
v.stub(:display_name) { 'display_name' }
v.stub(:unit_to_display) { 'unit_to_display' }
end
it "returns unit_to_display when display_name is blank" do
v.stub(:display_name) { '' }
v.full_name.should == 'unit_to_display'
end
it "returns display_name when it contains unit_to_display" do
v.stub(:display_name) { 'DiSpLaY_name' }
v.stub(:unit_to_display) { 'name' }
v.full_name.should == 'DiSpLaY_name'
end
it "returns unit_to_display when it contains display_name" do
v.stub(:display_name) { '_to_' }
v.stub(:unit_to_display) { 'unit_TO_display' }
v.full_name.should == 'unit_TO_display'
end
it "returns a combination otherwise" do
v.stub(:display_name) { 'display_name' }
v.stub(:unit_to_display) { 'unit_to_display' }
v.full_name.should == 'display_name (unit_to_display)'
end
it "is resilient to regex chars" do
v = Variant.new display_name: ")))"
v.stub(:unit_to_display) { ")))" }
v.full_name.should == ")))"
end
end
describe "getting name for display" do
it "returns display_name if present" do
v = create(:variant, display_name: "foo")
v.name_to_display.should == "foo"
end
it "returns product name if display_name is empty" do
v = create(:variant, product: create(:product))
v.name_to_display.should == v.product.name
v1 = create(:variant, display_name: "", product: create(:product))
v1.name_to_display.should == v1.product.name
end
end
describe "getting unit for display" do
it "returns display_as if present" do
v = create(:variant, display_as: "foo")
v.unit_to_display.should == "foo"
end
it "returns options_text if display_as is blank" do
v = create(:variant)
v1 = create(:variant, display_as: "")
v.stub(:options_text).and_return "ponies"
v1.stub(:options_text).and_return "ponies"
v.unit_to_display.should == "ponies"
v1.unit_to_display.should == "ponies"
end
end
describe "setting the variant's weight from the unit value" do
it "sets the variant's weight when unit is weight" do
p = create(:simple_product, variant_unit: 'volume')
v = create(:variant, product: p, weight: nil)
p.update_attributes! variant_unit: 'weight', variant_unit_scale: 1
v.update_attributes! unit_value: 10, unit_description: 'foo'
v.reload.weight.should == 0.01
end
it "does nothing when unit is not weight" do
p = create(:simple_product, variant_unit: 'volume')
v = create(:variant, product: p, weight: 123)
p.update_attributes! variant_unit: 'volume', variant_unit_scale: 1
v.update_attributes! unit_value: 10, unit_description: 'foo'
v.reload.weight.should == 123
end
it "does nothing when unit_value is not set" do
p = create(:simple_product, variant_unit: 'volume')
v = create(:variant, product: p, weight: 123)
p.update_attributes! variant_unit: 'weight', variant_unit_scale: 1
# Although invalid, this calls the before_validation callback, which would
# error if not handling unit_value == nil case
v.update_attributes(unit_value: nil, unit_description: 'foo').should be false
v.reload.weight.should == 123
end
end
context "when the variant already has a value set (and all required option values do not exist)" do
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar') }
it "removes the old option value and assigns the new one" do
ov_orig = v.option_values.last
expect {
v.update_attributes!(unit_value: 10, unit_description: 'foo')
}.to change(Spree::OptionValue, :count).by(1)
v.option_values.should_not include ov_orig
end
end
context "when the variant already has a value set (and all required option values exist)" do
let!(:p0) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:v0) { create(:variant, product: p0, unit_value: 10, unit_description: 'foo') }
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar') }
it "removes the old option value and assigns the new one" do
ov_orig = v.option_values.last
ov_new = v0.option_values.last
expect {
v.update_attributes!(unit_value: 10, unit_description: 'foo')
}.to change(Spree::OptionValue, :count).by(0)
v.option_values.should_not include ov_orig
v.option_values.should include ov_new
end
end
context "when the variant does not have a display_as value set" do
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar', display_as: '') }
it "requests the name of the new option_value from OptionValueName" do
OpenFoodNetwork::OptionValueNamer.any_instance.should_receive(:name).exactly(1).times.and_call_original
v.update_attributes(unit_value: 10, unit_description: 'foo')
ov = v.option_values.last
ov.name.should == "10g foo"
end
end
context "when the variant has a display_as value set" do
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar', display_as: 'FOOS!') }
it "does not request the name of the new option_value from OptionValueName" do
OpenFoodNetwork::OptionValueNamer.any_instance.should_not_receive(:name)
v.update_attributes!(unit_value: 10, unit_description: 'foo')
ov = v.option_values.last
ov.name.should == "FOOS!"
end
end
end
describe "deleting unit option values" do
before do
p = create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1)
ot = Spree::OptionType.find_by_name 'unit_weight'
@v = create(:variant, product: p)
end
it "removes option value associations for unit option types" do
expect {
@v.delete_unit_option_values
}.to change(@v.option_values, :count).by(-1)
end
it "does not delete option values" do
expect {
@v.delete_unit_option_values
}.to change(Spree::OptionValue, :count).by(0)
end
end
context "extends LocalizedNumber" do
subject! { build(:variant) }
it_behaves_like "a model using the LocalizedNumber module", [:price, :cost_price, :weight]
end
end
describe "destruction" do
it "destroys exchange variants" do
v = create(:variant)
e = create(:exchange, variants: [v])
v.destroy
e.reload.variant_ids.should be_empty
end
context "as the last variant of a product" do
let!(:extra_variant) { create(:variant) }
let!(:product) { extra_variant.product }
let!(:first_variant) { product.variants.first }
before { product.reload }
it "cannot be deleted" do
expect(product.variants.length).to eq 2
expect(extra_variant.delete).to eq extra_variant
expect(product.variants(:reload).length).to eq 1
expect(first_variant.delete).to be false
expect(product.variants(:reload).length).to eq 1
expect(first_variant.errors[:product]).to include "must have at least one variant"
end
end
end
describe '#delete_and_refresh_cache' do
context 'when it is not the master variant' do
let(:variant) { create(:variant) }
it 'refreshes the products cache on delete' do
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
variant.delete_and_refresh_cache
end
end
context "when it is the master variant" do
let(:product) { create(:simple_product) }
let(:master) { product.master }
it 'refreshes the products cache for the entire product on delete' do
expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product)
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).never
master.delete_and_refresh_cache
end
end
end
end