From 2c40252edb2aeb013d6b2e396909d54e32ca56ec Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Wed, 17 Jan 2018 13:31:05 +1100 Subject: [PATCH] Extract validation logic for standing orders into service object --- app/services/standing_order_form.rb | 97 +--- app/services/standing_order_validator.rb | 115 +++++ spec/services/standing_order_form_spec.rb | 7 - .../services/standing_order_validator_spec.rb | 466 ++++++++++++++++++ 4 files changed, 584 insertions(+), 101 deletions(-) create mode 100644 app/services/standing_order_validator.rb create mode 100644 spec/services/standing_order_validator_spec.rb diff --git a/app/services/standing_order_form.rb b/app/services/standing_order_form.rb index ba14d72217..e2b5bd069b 100644 --- a/app/services/standing_order_form.rb +++ b/app/services/standing_order_form.rb @@ -1,11 +1,7 @@ require 'open_food_network/proxy_order_syncer' class StandingOrderForm - include ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Validations - - attr_accessor :standing_order, :params, :fee_calculator, :order_update_issues + attr_accessor :standing_order, :params, :fee_calculator, :order_update_issues, :validator delegate :orders, :order_cycles, :bill_address, :ship_address, :standing_line_items, to: :standing_order delegate :shop, :shop_id, :customer, :customer_id, :begins_at, :ends_at, :proxy_orders, to: :standing_order @@ -14,22 +10,14 @@ class StandingOrderForm delegate :shipping_method_id_changed?, :shipping_method_id_was, :payment_method_id_changed?, :payment_method_id_was, to: :standing_order delegate :credit_card_id, :credit_card, to: :standing_order - validates_presence_of :shop, :customer, :schedule, :payment_method, :shipping_method - validates_presence_of :bill_address, :ship_address, :begins_at - validate :ends_at_after_begins_at? - validate :customer_allowed? - validate :schedule_allowed? - validate :payment_method_allowed? - validate :shipping_method_allowed? - validate :standing_line_items_present? - validate :standing_line_items_available? - validate :credit_card_ok? + delegate :json_errors, :valid?, to: :validator def initialize(standing_order, params = {}, fee_calculator = nil) @standing_order = standing_order @params = params @fee_calculator = fee_calculator @order_update_issues = {} + @validator = StandingOrderValidator.new(standing_order) end def save @@ -43,12 +31,6 @@ class StandingOrderForm end end - def json_errors - errors.messages.each_with_object({}) do |(k, v), errors| - errors[k] = v.map { |msg| build_msg_from(k, msg) } - end - end - private def update_initialised_orders @@ -190,77 +172,4 @@ class StandingOrderForm errors.add(k, msg) end end - - def ends_at_after_begins_at? - # Does not add error even if ends_at is nil - # Note: presence of begins_at validated on the model - return if begins_at.blank? || ends_at.blank? - return if ends_at > begins_at - errors.add(:ends_at, :after_begins_at) - end - - def customer_allowed? - return unless customer - return if customer.enterprise == shop - errors.add(:customer, :does_not_belong_to_shop, shop: shop.name) - end - - def schedule_allowed? - return unless schedule - return if schedule.coordinators.include?(shop) - errors.add(:schedule, :not_coordinated_by_shop, shop: shop.name) - end - - def payment_method_allowed? - return unless payment_method - - if payment_method.distributors.exclude?(shop) - errors.add(:payment_method, :not_available_to_shop, shop: shop.name) - end - - return if StandingOrder::ALLOWED_PAYMENT_METHOD_TYPES.include? payment_method.type - errors.add(:payment_method, :invalid_type) - end - - def shipping_method_allowed? - return unless shipping_method - return if shipping_method.distributors.include?(shop) - errors.add(:shipping_method, :not_available_to_shop, shop: shop.name) - end - - def standing_line_items_present? - return if standing_line_items.reject(&:marked_for_destruction?).any? - errors.add(:standing_line_items, :at_least_one_product) - end - - def standing_line_items_available? - available_variant_ids = variant_ids_for_shop_and_schedule - standing_line_items.each do |sli| - unless available_variant_ids.include? sli.variant_id - name = "#{sli.variant.product.name} - #{sli.variant.full_name}" - errors.add(:standing_line_items, :not_available, name: name) - end - end - end - - def credit_card_ok? - return unless payment_method.andand.type == "Spree::Gateway::StripeConnect" - return errors.add(:credit_card, :blank) unless credit_card_id - return if customer.andand.user.andand.credit_card_ids.andand.include? credit_card_id - errors.add(:credit_card, :not_available) - end - - def variant_ids_for_shop_and_schedule - Spree::Variant.joins(exchanges: { order_cycle: :schedules}) - .where(id: standing_line_items.map(&:variant_id)) - .where(schedules: { id: schedule}, exchanges: { incoming: false, receiver_id: shop }) - .merge(OrderCycle.not_closed) - .select('DISTINCT spree_variants.id') - .pluck(:id) - end - - def build_msg_from(k, msg) - return msg[1..-1] if msg.starts_with?("^") - errors.full_message(k, msg) - end end diff --git a/app/services/standing_order_validator.rb b/app/services/standing_order_validator.rb new file mode 100644 index 0000000000..292ea7376b --- /dev/null +++ b/app/services/standing_order_validator.rb @@ -0,0 +1,115 @@ +# Encapsulation of all of the validation logic required for standing orders +# Public interface consists of #valid? method provided by ActiveModel::Validations +# and #json_errors which compiles a serializable hash of errors + +class StandingOrderValidator + include ActiveModel::Naming + include ActiveModel::Conversion + include ActiveModel::Validations + + attr_reader :standing_order + + validates_presence_of :shop, :customer, :schedule, :shipping_method, :payment_method + validates_presence_of :bill_address, :ship_address, :begins_at + validate :shipping_method_allowed? + validate :payment_method_allowed? + validate :payment_method_type_allowed? + validate :ends_at_after_begins_at? + validate :customer_allowed? + validate :schedule_allowed? + validate :credit_card_ok? + validate :standing_line_items_present? + validate :requested_variants_available? + + delegate :shop, :customer, :schedule, :shipping_method, :payment_method, to: :standing_order + delegate :bill_address, :ship_address, :begins_at, :ends_at, to: :standing_order + delegate :credit_card, :credit_card_id, to: :standing_order + delegate :standing_line_items, to: :standing_order + + def initialize(standing_order) + @standing_order = standing_order + end + + def json_errors + errors.messages.each_with_object({}) do |(k, v), errors| + errors[k] = v.map { |msg| build_msg_from(k, msg) } + end + end + + private + + def shipping_method_allowed? + return unless shipping_method + return if shipping_method.distributors.include?(shop) + errors.add(:shipping_method, :not_available_to_shop, shop: shop.name) + end + + def payment_method_allowed? + return unless payment_method + return if payment_method.distributors.include?(shop) + errors.add(:payment_method, :not_available_to_shop, shop: shop.name) + end + + def payment_method_type_allowed? + return unless payment_method + return if StandingOrder::ALLOWED_PAYMENT_METHOD_TYPES.include? payment_method.type + errors.add(:payment_method, :invalid_type) + end + + def ends_at_after_begins_at? + # Only validates ends_at if it is present + return if begins_at.blank? || ends_at.blank? + return if ends_at > begins_at + errors.add(:ends_at, :after_begins_at) + end + + def customer_allowed? + return unless customer + return if customer.enterprise == shop + errors.add(:customer, :does_not_belong_to_shop, shop: shop.name) + end + + def schedule_allowed? + return unless schedule + return if schedule.coordinators.include?(shop) + errors.add(:schedule, :not_coordinated_by_shop, shop: shop.name) + end + + def credit_card_ok? + return unless payment_method.andand.type == "Spree::Gateway::StripeConnect" + return errors.add(:credit_card, :blank) unless credit_card_id + return if customer.andand.user.andand.credit_card_ids.andand.include? credit_card_id + errors.add(:credit_card, :not_available) + end + + def standing_line_items_present? + return if standing_line_items.reject(&:marked_for_destruction?).any? + errors.add(:standing_line_items, :at_least_one_product) + end + + def requested_variants_available? + standing_line_items.each { |sli| verify_availability_of(sli.variant) } + end + + def verify_availability_of(variant) + return if available_variant_ids.include? variant.id + name = "#{variant.product.name} - #{variant.full_name}" + errors.add(:standing_line_items, :not_available, name: name) + end + + # TODO: Extract this into a separate class + def available_variant_ids + @available_variant_ids ||= + Spree::Variant.joins(exchanges: { order_cycle: :schedules }) + .where(id: standing_line_items.map(&:variant_id)) + .where(schedules: { id: schedule}, exchanges: { incoming: false, receiver_id: shop }) + .merge(OrderCycle.not_closed) + .select('DISTINCT spree_variants.id') + .pluck(:id) + end + + def build_msg_from(k, msg) + return msg[1..-1] if msg.starts_with?("^") + errors.full_message(k, msg) + end +end diff --git a/spec/services/standing_order_form_spec.rb b/spec/services/standing_order_form_spec.rb index 793b8d1903..b257e640c9 100644 --- a/spec/services/standing_order_form_spec.rb +++ b/spec/services/standing_order_form_spec.rb @@ -1,6 +1,4 @@ describe StandingOrderForm do - let(:error_t_scope) { 'activemodel.errors.models.standing_order_form.attributes' } - describe "creating a new standing order" do let!(:shop) { create(:distributor_enterprise) } let!(:customer) { create(:customer, enterprise: shop) } @@ -225,7 +223,6 @@ describe StandingOrderForm do expect(payments.with_state('void').count).to be 0 expect(payments.with_state('checkout').count).to be 1 expect(payments.with_state('checkout').first.payment_method).to eq payment_method - expect(form.errors[:credit_card]).to include I18n.t("#{error_t_scope}.credit_card.blank") end end end @@ -241,7 +238,6 @@ describe StandingOrderForm do expect(payments.with_state('void').count).to be 0 expect(payments.with_state('checkout').count).to be 1 expect(payments.with_state('checkout').first.payment_method).to eq payment_method - expect(form.errors[:payment_method]).to include I18n.t("#{error_t_scope}.payment_method.invalid_type") end end end @@ -257,7 +253,6 @@ describe StandingOrderForm do expect(payments.with_state('void').count).to be 0 expect(payments.with_state('checkout').count).to be 1 expect(payments.with_state('checkout').first.payment_method).to eq payment_method - expect(form.errors[:payment_method]).to include I18n.t("#{error_t_scope}.payment_method.not_available_to_shop", shop: standing_order.shop.name) end end end @@ -603,7 +598,6 @@ describe StandingOrderForm do line_items = Spree::LineItem.where(order_id: standing_order.orders, variant_id: variant.id) expect(line_items.count).to be 0 expect(order.reload.total.to_f).to eq 59.97 - expect(form.json_errors.keys).to eq [:standing_line_items] end end end @@ -640,7 +634,6 @@ describe StandingOrderForm do line_items = Spree::LineItem.where(order_id: standing_order.orders, variant_id: variant.id) expect(line_items.count).to be 1 expect(order.reload.total.to_f).to eq 19.99 - expect(form.json_errors.keys).to eq [:standing_line_items] end end end diff --git a/spec/services/standing_order_validator_spec.rb b/spec/services/standing_order_validator_spec.rb new file mode 100644 index 0000000000..b648daf719 --- /dev/null +++ b/spec/services/standing_order_validator_spec.rb @@ -0,0 +1,466 @@ +describe StandingOrderValidator do + let(:shop) { instance_double(Enterprise, name: "Shop") } + + describe "delegation" do + let(:standing_order) { create(:standing_order) } + let(:validator) { StandingOrderValidator.new(standing_order) } + + it "delegates to standing_order" do + expect(validator.shop).to eq standing_order.shop + expect(validator.customer).to eq standing_order.customer + expect(validator.schedule).to eq standing_order.schedule + expect(validator.shipping_method).to eq standing_order.shipping_method + expect(validator.payment_method).to eq standing_order.payment_method + expect(validator.bill_address).to eq standing_order.bill_address + expect(validator.ship_address).to eq standing_order.ship_address + expect(validator.begins_at).to eq standing_order.begins_at + expect(validator.ends_at).to eq standing_order.ends_at + end + end + + describe "validations" do + let(:standing_order_stubs) do + { + shop: shop, + customer: true, + schedule: true, + shipping_method: true, + payment_method: true, + bill_address: true, + ship_address: true, + begins_at: true, + ends_at: true, + credit_card: true + } + end + + let(:validation_stubs) do + { + shipping_method_allowed?: true, + payment_method_allowed?: true, + payment_method_type_allowed?: true, + ends_at_after_begins_at?: true, + customer_allowed?: true, + schedule_allowed?: true, + credit_card_ok?: true, + standing_line_items_present?: true, + requested_variants_available?: true + } + end + + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs) } + let(:validator) { StandingOrderValidator.new(standing_order) } + + def stub_validations(validator, methods) + methods.each do |name, value| + allow(validator).to receive(name) { value } + end + end + + describe "shipping method validation" do + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:shipping_method)) } + before { stub_validations(validator, validation_stubs.except(:shipping_method_allowed?)) } + + context "when no shipping method is present" do + before { expect(standing_order).to receive(:shipping_method).at_least(:once) { nil } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:shipping_method]).to_not be_empty + end + end + + context "when a shipping method is present" do + let(:shipping_method) { instance_double(Spree::ShippingMethod, distributors: [shop]) } + before { expect(standing_order).to receive(:shipping_method).at_least(:once) { shipping_method } } + + context "and the shipping method is not associated with the shop" do + before { allow(shipping_method).to receive(:distributors) { [double(:enterprise)] } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:shipping_method]).to_not be_empty + end + end + + context "and the shipping method is associated with the shop" do + before { allow(shipping_method).to receive(:distributors) { [shop] } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:shipping_method]).to be_empty + end + end + end + end + + describe "payment method validation" do + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:payment_method)) } + before { stub_validations(validator, validation_stubs.except(:payment_method_allowed?)) } + + context "when no payment method is present" do + before { expect(standing_order).to receive(:payment_method).at_least(:once) { nil } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:payment_method]).to_not be_empty + end + end + + context "when a payment method is present" do + let(:payment_method) { instance_double(Spree::PaymentMethod, distributors: [shop]) } + before { expect(standing_order).to receive(:payment_method).at_least(:once) { payment_method } } + + context "and the payment method is not associated with the shop" do + before { allow(payment_method).to receive(:distributors) { [double(:enterprise)] } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:payment_method]).to_not be_empty + end + end + + context "and the payment method is associated with the shop" do + before { allow(payment_method).to receive(:distributors) { [shop] } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:payment_method]).to be_empty + end + end + end + end + + describe "payment method type validation" do + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:payment_method)) } + before { stub_validations(validator, validation_stubs.except(:payment_method_type_allowed?)) } + + context "when a payment method is present" do + let(:payment_method) { instance_double(Spree::PaymentMethod, distributors: [shop]) } + before { expect(standing_order).to receive(:payment_method).at_least(:once) { payment_method } } + + context "and the payment method type is not in the approved list" do + before { allow(payment_method).to receive(:type) { "Blah" } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:payment_method]).to_not be_empty + end + end + + context "and the payment method is in the approved list" do + let(:approved_type) { StandingOrder::ALLOWED_PAYMENT_METHOD_TYPES.first } + before { allow(payment_method).to receive(:type) { approved_type } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:payment_method]).to be_empty + end + end + end + end + + describe "dates" do + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:begins_at, :ends_at)) } + before { stub_validations(validator, validation_stubs.except(:ends_at_after_begins_at?)) } + before { expect(standing_order).to receive(:begins_at).at_least(:once) { begins_at } } + + context "when no begins_at is present" do + let(:begins_at) { nil } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:begins_at]).to_not be_empty + end + end + + context "when a start date is present" do + let(:begins_at) { Time.zone.today } + before { expect(standing_order).to receive(:ends_at).at_least(:once) { ends_at } } + + context "when no ends_at is present" do + let(:ends_at) { nil } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:ends_at]).to be_empty + end + end + + context "when ends_at is equal to begins_at" do + let(:ends_at) { Time.zone.today } + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:ends_at]).to_not be_empty + end + end + + context "when ends_at is before begins_at" do + let(:ends_at) { Time.zone.today - 1.day } + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:ends_at]).to_not be_empty + end + end + + context "when ends_at is after begins_at" do + let(:ends_at) { Time.zone.today + 1.day } + it "adds an error and returns false" do + expect(validator.valid?).to be true + expect(validator.errors[:ends_at]).to be_empty + end + end + end + end + + describe "addresses" do + before { stub_validations(validator, validation_stubs) } + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:bill_address, :ship_address)) } + before { expect(standing_order).to receive(:bill_address).at_least(:once) { bill_address } } + before { expect(standing_order).to receive(:ship_address).at_least(:once) { ship_address } } + + context "when bill_address and ship_address are not present" do + let(:bill_address) { nil } + let(:ship_address) { nil } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:bill_address]).to_not be_empty + expect(validator.errors[:ship_address]).to_not be_empty + end + end + + context "when bill_address and ship_address are present" do + let(:bill_address) { instance_double(Spree::Address) } + let(:ship_address) { instance_double(Spree::Address) } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:bill_address]).to be_empty + expect(validator.errors[:ship_address]).to be_empty + end + end + end + + describe "customer" do + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:customer)) } + before { stub_validations(validator, validation_stubs.except(:customer_allowed?)) } + before { expect(standing_order).to receive(:customer).at_least(:once) { customer } } + + context "when no customer is present" do + let(:customer) { nil } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:customer]).to_not be_empty + end + end + + context "when a customer is present" do + let(:customer) { instance_double(Customer) } + + context "and the customer is not associated with the shop" do + before { allow(customer).to receive(:enterprise) { double(:enterprise) } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:customer]).to_not be_empty + end + end + + context "and the customer is associated with the shop" do + before { allow(customer).to receive(:enterprise) { shop } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:customer]).to be_empty + end + end + end + end + + describe "schedule" do + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:schedule)) } + before { stub_validations(validator, validation_stubs.except(:schedule_allowed?)) } + before { expect(standing_order).to receive(:schedule).at_least(:once) { schedule } } + + context "when no schedule is present" do + let(:schedule) { nil } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:schedule]).to_not be_empty + end + end + + context "when a schedule is present" do + let(:schedule) { instance_double(Schedule) } + + context "and the schedule is not associated with the shop" do + before { allow(schedule).to receive(:coordinators) { [double(:enterprise)] } } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:schedule]).to_not be_empty + end + end + + context "and the schedule is associated with the shop" do + before { allow(schedule).to receive(:coordinators) { [shop] } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:schedule]).to be_empty + end + end + end + end + + describe "credit card" do + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs.except(:payment_method)) } + before { stub_validations(validator, validation_stubs.except(:credit_card_ok?)) } + before { expect(standing_order).to receive(:payment_method).at_least(:once) { payment_method } } + + context "when using a Check payment method" do + let(:payment_method) { instance_double(Spree::PaymentMethod, type: "Spree::PaymentMethod::Check") } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:standing_line_items]).to be_empty + end + end + + context "when using the StripeConnect payment gateway" do + let(:payment_method) { instance_double(Spree::PaymentMethod, type: "Spree::Gateway::StripeConnect") } + before { expect(standing_order).to receive(:credit_card_id).at_least(:once) { credit_card_id } } + + context "when a credit card is not present" do + let(:credit_card_id) { nil } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:credit_card]).to_not be_empty + end + end + + context "when a credit card is present" do + let(:credit_card_id) { 12 } + before { expect(standing_order).to receive(:customer).at_least(:once) { customer } } + + context "and the customer is not associated with a user" do + let(:customer) { instance_double(Customer, user: nil) } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:credit_card]).to_not be_empty + end + end + + context "and the customer is associated with a user" do + let(:customer) { instance_double(Customer, user: user) } + + context "and the user has no credit cards which match that specified" do + let(:user) { instance_double(Spree::User, credit_card_ids: [1, 2, 3, 4]) } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:credit_card]).to_not be_empty + end + end + + context "and the user has a credit card which matches that specified" do + let(:user) { instance_double(Spree::User, credit_card_ids: [1, 2, 3, 12]) } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:credit_card]).to be_empty + end + end + end + end + end + end + + describe "standing line items" do + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs) } + before { stub_validations(validator, validation_stubs.except(:standing_line_items_present?)) } + before { expect(standing_order).to receive(:standing_line_items).at_least(:once) { standing_line_items } } + + context "when no standing line items are present" do + let(:standing_line_items) { [] } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:standing_line_items]).to_not be_empty + end + end + + context "when standing line items are present but they are all marked for destruction" do + let(:standing_line_item1) { instance_double(StandingLineItem, marked_for_destruction?: true) } + let(:standing_line_items) { [standing_line_item1] } + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:standing_line_items]).to_not be_empty + end + end + + context "when standing line items are present and some and not marked for destruction" do + let(:standing_line_item1) { instance_double(StandingLineItem, marked_for_destruction?: true) } + let(:standing_line_item2) { instance_double(StandingLineItem, marked_for_destruction?: false) } + let(:standing_line_items) { [standing_line_item1, standing_line_item2] } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:standing_line_items]).to be_empty + end + end + end + + describe "variant availability" do + let(:standing_order) { instance_double(StandingOrder, standing_order_stubs) } + before { stub_validations(validator, validation_stubs.except(:requested_variants_available?)) } + before { expect(standing_order).to receive(:standing_line_items).at_least(:once) { standing_line_items } } + + context "when no standing line items are present" do + let(:standing_line_items) { [] } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:standing_line_items]).to be_empty + end + end + + context "when standing line items are present" do + let(:variant1) { instance_double(Spree::Variant, id: 1) } + let(:variant2) { instance_double(Spree::Variant, id: 2) } + let(:standing_line_item1) { instance_double(StandingLineItem, variant: variant1) } + let(:standing_line_item2) { instance_double(StandingLineItem, variant: variant2) } + let(:standing_line_items) { [standing_line_item1] } + + context "but some variants are unavailable" do + let(:product) { instance_double(Spree::Product, name: "some_name") } + before do + allow(validator).to receive(:available_variant_ids) { [variant2.id] } + allow(variant1).to receive(:product) { product } + allow(variant1).to receive(:full_name) { "some name" } + end + + it "adds an error and returns false" do + expect(validator.valid?).to be false + expect(validator.errors[:standing_line_items]).to_not be_empty + end + end + + context "and all requested variants are available" do + before { allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] } } + + it "returns true" do + expect(validator.valid?).to be true + expect(validator.errors[:standing_line_items]).to be_empty + end + end + end + end + end +end