require 'rails_helper' RSpec.describe Spree::CreditCard, type: :model do let(:valid_credit_card_attributes) do { number: '4111111111111111', verification_value: '123', expiry: "12 / #{(Time.current.year + 1).to_s.last(2)}", name: 'Spree Commerce' } end def self.payment_states Spree::Payment.state_machine.states.keys end let(:credit_card) { Spree::CreditCard.new } it_behaves_like 'a payment source' before(:each) do @order = create(:order) @payment = Spree::Payment.create(amount: 100, order: @order) @success_response = double('gateway_response', success?: true, authorization: '123', avs_result: { 'code' => 'avs-code' }) @fail_response = double('gateway_response', success?: false) @payment_gateway = mock_model(Spree::PaymentMethod, payment_profiles_supported?: true, authorize: @success_response, purchase: @success_response, capture: @success_response, void: @success_response, credit: @success_response ) allow(@payment).to receive_messages payment_method: @payment_gateway end describe "#valid?" do it "should validate presence of number" do credit_card.attributes = valid_credit_card_attributes.except(:number) expect(credit_card).not_to be_valid expect(credit_card.errors[:number]).to eq(["can't be blank"]) end it "should validate presence of security code" do credit_card.attributes = valid_credit_card_attributes.except(:verification_value) expect(credit_card).not_to be_valid expect(credit_card.errors[:verification_value]).to eq(["can't be blank"]) end it "validates name presence" do credit_card.valid? expect(credit_card.error_on(:name).size).to eq(1) end it "should only validate on create" do credit_card.attributes = valid_credit_card_attributes credit_card.save expect(credit_card).to be_valid end context "encrypted data is present" do it "does not validate presence of number or cvv" do credit_card.encrypted_data = "$fdgsfgdgfgfdg&gfdgfdgsf-" credit_card.valid? expect(credit_card.errors[:number]).to be_empty expect(credit_card.errors[:verification_value]).to be_empty end end context "imported is true" do it "does not validate presence of number or cvv" do credit_card.imported = true credit_card.valid? expect(credit_card.errors[:number]).to be_empty expect(credit_card.errors[:verification_value]).to be_empty end end end describe "#save" do before do credit_card.attributes = valid_credit_card_attributes credit_card.save! end let!(:persisted_card) { Spree::CreditCard.find(credit_card.id) } let(:valid_address_attributes) do { firstname: "Hugo", lastname: "Furst", address1: "123 Main", city: "Somewhere", country_id: 1, zipcode: 55_555, phone: "1234567890" } end it "should not actually store the number" do expect(persisted_card.number).to be_blank end it "should not actually store the security code" do expect(persisted_card.verification_value).to be_blank end it "should save and update addresses through nested attributes" do persisted_card.update_attributes({ address_attributes: valid_address_attributes }) persisted_card.save! updated_attributes = { id: persisted_card.address.id, address1: "123 Main St." } persisted_card.update_attributes({ address_attributes: updated_attributes }) expect(persisted_card.address.address1).to eq "123 Main St." end end describe "#number=" do it "should strip non-numeric characters from card input" do credit_card.number = "6011000990139424" expect(credit_card.number).to eq("6011000990139424") credit_card.number = " 6011-0009-9013-9424 " expect(credit_card.number).to eq("6011000990139424") end it "should not raise an exception on non-string input" do credit_card.number = {} expect(credit_card.number).to be_nil end end describe "#verification_value=" do it "accepts a valid 3-digit value" do credit_card.verification_value = "123" expect(credit_card.verification_value).to eq("123") end it "accepts a valid 4-digit value" do credit_card.verification_value = "1234" expect(credit_card.verification_value).to eq("1234") end it "stringifies an integer" do credit_card.verification_value = 123 expect(credit_card.verification_value).to eq("123") end it "strips any whitespace" do credit_card.verification_value = ' 1 2 3 ' expect(credit_card.verification_value).to eq('123') end end # Regression test for https://github.com/spree/spree/issues/3847 and https://github.com/spree/spree/issues/3896 describe "#expiry=" do it "can set with a 2-digit month and year" do credit_card.expiry = '04 / 15' expect(credit_card.month).to eq('4') expect(credit_card.year).to eq('2015') end it "can set with a 2-digit month and 4-digit year" do credit_card.expiry = '04 / 2015' expect(credit_card.month).to eq('4') expect(credit_card.year).to eq('2015') end it "can set with a 2-digit month and 4-digit year without whitespace" do credit_card.expiry = '04/15' expect(credit_card.month).to eq('4') expect(credit_card.year).to eq('2015') end it "can set with a 2-digit month and 4-digit year without whitespace" do credit_card.expiry = '04/2015' expect(credit_card.month).to eq('4') expect(credit_card.year).to eq('2015') end it "can set with a 2-digit month and 4-digit year without whitespace and slash" do credit_card.expiry = '042015' expect(credit_card.month).to eq('4') expect(credit_card.year).to eq('2015') end it "can set with a 2-digit month and 2-digit year without whitespace and slash" do credit_card.expiry = '0415' expect(credit_card.month).to eq('4') expect(credit_card.year).to eq('2015') end it "does not blow up when passed an empty string" do credit_card.expiry = '' end # Regression test for https://github.com/spree/spree/issues/4725 it "does not blow up when passed one number" do credit_card.expiry = '12' end end describe "#cc_type=" do it "converts between the different types" do credit_card.cc_type = 'mastercard' expect(credit_card.cc_type).to eq('master') credit_card.cc_type = 'maestro' expect(credit_card.cc_type).to eq('master') credit_card.cc_type = 'amex' expect(credit_card.cc_type).to eq('american_express') credit_card.cc_type = 'dinersclub' expect(credit_card.cc_type).to eq('diners_club') credit_card.cc_type = 'some_outlandish_cc_type' expect(credit_card.cc_type).to eq('some_outlandish_cc_type') end it "assigns the type based on card number in the event of js failure" do credit_card.number = '4242424242424242' credit_card.cc_type = '' expect(credit_card.cc_type).to eq('visa') credit_card.number = '5555555555554444' credit_card.cc_type = '' expect(credit_card.cc_type).to eq('master') credit_card.number = '2221000000000000' credit_card.cc_type = '' expect(credit_card.cc_type).to eq('master') credit_card.number = '378282246310005' credit_card.cc_type = '' expect(credit_card.cc_type).to eq('american_express') credit_card.number = '30569309025904' credit_card.cc_type = '' expect(credit_card.cc_type).to eq('diners_club') credit_card.number = '3530111333300000' credit_card.cc_type = '' expect(credit_card.cc_type).to eq('jcb') credit_card.number = '' credit_card.cc_type = '' expect(credit_card.cc_type).to eq('') credit_card.number = nil credit_card.cc_type = '' expect(credit_card.cc_type).to eq('') end end describe "#to_active_merchant" do before do credit_card.number = "4111111111111111" credit_card.year = Time.current.year credit_card.month = Time.current.month credit_card.name = "Ludwig van Beethoven" credit_card.verification_value = 123 end it "converts to an ActiveMerchant::Billing::CreditCard object" do am_card = credit_card.to_active_merchant expect(am_card.number).to eq("4111111111111111") expect(am_card.year).to eq(Time.current.year) expect(am_card.month).to eq(Time.current.month) expect(am_card.first_name).to eq("Ludwig") expect(am_card.last_name).to eq("van Beethoven") expect(am_card.verification_value).to eq("123") end end # TODO: Remove these specs once default is removed describe 'default' do def default_with_silence(card) Spree::Deprecation.silence { card.default } end context 'with a user' do let(:user) { create(:user) } let(:credit_card) { create(:credit_card, user: user) } it 'uses the wallet information' do wallet_payment_source = user.wallet.add(credit_card) user.wallet.default_wallet_payment_source = wallet_payment_source expect(default_with_silence(credit_card)).to be_truthy end end context 'without a user' do let(:credit_card) { create(:credit_card) } it 'returns false' do expect(default_with_silence(credit_card)).to eq(false) end end end # TODO: Remove these specs once default= is removed describe 'default=' do def default_with_silence(card) Spree::Deprecation.silence { card.default } end context 'with a user' do let(:user) { create(:user) } let(:credit_card) { create(:credit_card, user: user) } it 'updates the wallet information' do Spree::Deprecation.silence do credit_card.default = true end expect(user.wallet.default_wallet_payment_source.payment_source).to eq(credit_card) end end context 'with multiple cards for one user' do let(:user) { create(:user) } let(:first_card) { create(:credit_card, user: user) } let(:second_card) { create(:credit_card, user: user) } it 'ensures only one default' do Spree::Deprecation.silence do first_card.default = true second_card.default = true end expect(default_with_silence(first_card)).to be_falsey expect(default_with_silence(second_card)).to be_truthy Spree::Deprecation.silence do first_card.default = true end expect(default_with_silence(first_card)).to be_truthy expect(default_with_silence(second_card)).to be_falsey end end context 'with multiple cards for different users' do let(:first_card) { create(:credit_card, user: create(:user)) } let(:second_card) { create(:credit_card, user: create(:user)) } it 'allows multiple defaults' do Spree::Deprecation.silence do first_card.default = true second_card.default = true end expect(default_with_silence(first_card)).to be_truthy expect(default_with_silence(second_card)).to be_truthy end end context 'without a user' do let(:credit_card) { create(:credit_card) } it 'raises' do expect { Spree::Deprecation.silence do credit_card.default = true end }.to raise_error("Cannot set 'default' on a credit card without a user") end end end end