mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-25 20:46:48 +00:00
The code here runs from a callback which was originally designed to make sure the checkout page was set up correctly in the "normal" checkout workflow. It wasn't really designed to be run when the page is being loaded a second time due to the user being redirected back from Stripe (with SCA). The things it's doing here are necessary in the former case, but a really bad idea in the latter (potentially messing up the order's ship and bill addresses in certain cases like guest checkout).
470 lines
18 KiB
Ruby
470 lines
18 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
describe CheckoutController, type: :controller do
|
|
include StripeStubs
|
|
|
|
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) }
|
|
let(:order_cycle) { create(:simple_order_cycle) }
|
|
let(:order) { create(:order) }
|
|
let(:reset_order_service) { double(OrderCompletionReset) }
|
|
|
|
before do
|
|
allow(order).to receive(:checkout_allowed?).and_return true
|
|
allow(controller).to receive(:check_authorization).and_return true
|
|
end
|
|
|
|
it "redirects home when no distributor is selected" do
|
|
get :edit
|
|
expect(response).to redirect_to root_path
|
|
end
|
|
|
|
it "redirects to the shop when no order cycle is selected" do
|
|
allow(controller).to receive(:current_distributor).and_return(distributor)
|
|
get :edit
|
|
expect(response).to redirect_to shop_path
|
|
end
|
|
|
|
it "redirects home with message if hub is not ready for checkout" do
|
|
allow(distributor).to receive(:ready_for_checkout?) { false }
|
|
allow(order).to receive_messages(distributor: distributor, order_cycle: order_cycle)
|
|
allow(controller).to receive(:current_order).and_return(order)
|
|
|
|
expect(order).to receive(:empty!)
|
|
expect(order).to receive(:set_distribution!).with(nil, nil)
|
|
|
|
get :edit
|
|
|
|
expect(response).to redirect_to root_url
|
|
expect(flash[:info]).to eq(I18n.t('order_cycles_closed_for_hub'))
|
|
end
|
|
|
|
describe "#update" do
|
|
let(:user) { order.user }
|
|
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) }
|
|
let(:order_cycle) { create(:order_cycle, distributors: [distributor]) }
|
|
let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) }
|
|
let(:payment_method) { distributor.payment_methods.first }
|
|
let(:shipping_method) { distributor.shipping_methods.first }
|
|
|
|
before do
|
|
order.contents.add(order_cycle.variants_distributed_by(distributor).first)
|
|
|
|
allow(controller).to receive(:current_distributor).and_return(distributor)
|
|
allow(controller).to receive(:current_order_cycle).and_return(order_cycle)
|
|
allow(controller).to receive(:current_order).and_return(order)
|
|
allow(controller).to receive(:spree_current_user).and_return(user)
|
|
|
|
user.bill_address = create(:address)
|
|
user.ship_address = create(:address)
|
|
user.save!
|
|
end
|
|
|
|
it "completes the order and redirects to the order confirmation page" do
|
|
params = {
|
|
"order" => {
|
|
"bill_address_attributes" => order.bill_address.attributes,
|
|
"default_bill_address" => false,
|
|
"default_ship_address" => false,
|
|
"email" => user.email,
|
|
"payments_attributes" => [{ "payment_method_id" => payment_method.id }],
|
|
"ship_address_attributes" => order.bill_address.attributes,
|
|
"shipping_method_id" => shipping_method.id
|
|
}
|
|
}
|
|
expect { post :update, params: params }.
|
|
to change { Customer.count }.by(1)
|
|
expect(order.completed?).to be true
|
|
expect(response).to redirect_to order_path(order)
|
|
end
|
|
end
|
|
|
|
describe "redirection to cart and stripe" do
|
|
let(:order_cycle_distributed_variants) { double(:order_cycle_distributed_variants) }
|
|
|
|
before do
|
|
allow(controller).to receive(:current_order).and_return(order)
|
|
allow(order).to receive(:distributor).and_return(distributor)
|
|
order.update(order_cycle: order_cycle)
|
|
|
|
allow(OrderCycleDistributedVariants).to receive(:new).and_return(order_cycle_distributed_variants)
|
|
end
|
|
|
|
context "running out of stock" do
|
|
it "redirects when some items are out of stock" do
|
|
allow(order).to receive_message_chain(:insufficient_stock_lines, :empty?).and_return false
|
|
|
|
get :edit
|
|
expect(response).to redirect_to cart_path
|
|
end
|
|
|
|
it "redirects when some items are not available" do
|
|
allow(order).to receive_message_chain(:insufficient_stock_lines, :empty?).and_return true
|
|
expect(order_cycle_distributed_variants).to receive(:distributes_order_variants?).with(order).and_return(false)
|
|
|
|
get :edit
|
|
expect(response).to redirect_to cart_path
|
|
end
|
|
|
|
context "after redirecting back from Stripe" do
|
|
let(:order) { create(:order_with_totals_and_distribution) }
|
|
let!(:payment) { create(:payment, state: "pending", amount: order.total, order: order) }
|
|
let!(:transaction_fee) {
|
|
create(:adjustment, state: "open", amount: 10, order: order, adjustable: payment)
|
|
}
|
|
|
|
before do
|
|
allow(order).to receive_message_chain(:insufficient_stock_lines, :empty?).and_return(false)
|
|
allow(order_cycle_distributed_variants).to receive(:distributes_order_variants?).
|
|
with(order).and_return(true)
|
|
allow(controller).to receive(:valid_payment_intent_provided?) { true }
|
|
order.save
|
|
allow(order).to receive_message_chain(:payments, :completed) { [] }
|
|
allow(order).to receive_message_chain(:payments, :incomplete) { [payment] }
|
|
allow(payment).to receive(:adjustment) { transaction_fee }
|
|
end
|
|
|
|
it "cancels the payment and resets the order to cart" do
|
|
expect(payment).to receive(:void_transaction!).and_call_original
|
|
|
|
spree_post :edit
|
|
|
|
expect(response).to redirect_to cart_path
|
|
expect(flash[:notice]).to eq I18n.t('checkout.payment_cancelled_due_to_stock')
|
|
|
|
expect(order.state).to eq "cart"
|
|
expect(payment.state).to eq "void"
|
|
expect(transaction_fee.reload.eligible).to eq false
|
|
expect(transaction_fee.state).to eq "finalized"
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "when items are available and in stock" do
|
|
before do
|
|
allow(order).to receive_message_chain(:insufficient_stock_lines, :empty?).and_return true
|
|
end
|
|
|
|
describe "order variants are distributed in the OC" do
|
|
before do
|
|
expect(order_cycle_distributed_variants).to receive(:distributes_order_variants?).with(order).and_return(true)
|
|
end
|
|
|
|
it "does not redirect" do
|
|
get :edit
|
|
expect(response.status).to eq 200
|
|
end
|
|
|
|
it "returns a specific flash message when Spree::Core::GatewayError occurs" do
|
|
order_checkout_restart = double(:order_checkout_restart)
|
|
allow(OrderCheckoutRestart).to receive(:new) { order_checkout_restart }
|
|
call_count = 0
|
|
allow(order_checkout_restart).to receive(:call) do
|
|
call_count += 1
|
|
raise Spree::Core::GatewayError, "Gateway blow up" if call_count == 1
|
|
end
|
|
|
|
spree_post :edit
|
|
|
|
expect(response.status).to eq(200)
|
|
flash_message = I18n.t(:spree_gateway_error_flash_for_checkout, error: "Gateway blow up")
|
|
expect(flash[:error]).to eq flash_message
|
|
end
|
|
end
|
|
|
|
describe "when the order is in payment state and a stripe payment intent is provided" do
|
|
let(:user) { order.user }
|
|
let(:order) { create(:order_with_totals) }
|
|
let(:payment_method) { create(:stripe_sca_payment_method) }
|
|
let(:payment) {
|
|
create(
|
|
:payment,
|
|
amount: order.total,
|
|
state: "requires_authorization",
|
|
payment_method: payment_method,
|
|
response_code: "pi_123"
|
|
)
|
|
}
|
|
|
|
before do
|
|
Stripe.api_key = "sk_test_12345"
|
|
stub_payment_intent_get_request
|
|
stub_successful_capture_request(order: order)
|
|
|
|
allow(controller).to receive(:spree_current_user).and_return(user)
|
|
user.bill_address = create(:address)
|
|
user.ship_address = create(:address)
|
|
user.save!
|
|
|
|
order.update_attribute :state, "payment"
|
|
order.payments << payment
|
|
|
|
# this is called a 2nd time after order completion from the reset_order_service
|
|
expect(order_cycle_distributed_variants).to receive(:distributes_order_variants?).twice.and_return(true)
|
|
end
|
|
|
|
it "completes the order and redirects to the order confirmation page" do
|
|
get :edit, params: { payment_intent: "pi_123" }
|
|
expect(order.completed?).to be true
|
|
expect(response).to redirect_to order_path(order)
|
|
end
|
|
|
|
it "does not attempt to load different addresses" do
|
|
expect(OpenFoodNetwork::AddressFinder).to_not receive(:new)
|
|
|
|
get :edit, params: { payment_intent: "pi_123" }
|
|
end
|
|
|
|
it "creates a customer record" do
|
|
order.update_columns(customer_id: nil)
|
|
Customer.delete_all
|
|
|
|
expect {
|
|
get :edit, params: { payment_intent: "pi_123" }
|
|
}.to change { Customer.count }.by(1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "building the order" do
|
|
before do
|
|
allow(controller).to receive(:current_distributor).and_return(distributor)
|
|
allow(controller).to receive(:current_order_cycle).and_return(order_cycle)
|
|
allow(controller).to receive(:current_order).and_return(order)
|
|
end
|
|
|
|
it "set shipping_address_from_distributor when re-rendering edit" do
|
|
expect(order.updater).to receive(:shipping_address_from_distributor)
|
|
allow(order).to receive(:update).and_return false
|
|
spree_post :update, format: :json, order: {}
|
|
end
|
|
|
|
it "set shipping_address_from_distributor when the order state cannot be advanced" do
|
|
expect(order.updater).to receive(:shipping_address_from_distributor)
|
|
allow(order).to receive(:update).and_return true
|
|
allow(order).to receive(:next).and_return false
|
|
spree_post :update, format: :json, order: {}
|
|
end
|
|
|
|
context "#update with shipping_method_id" do
|
|
let(:test_shipping_method_id) { "111" }
|
|
|
|
before do
|
|
# stub order and OrderCompletionReset
|
|
allow(OrderCompletionReset).to receive(:new).with(controller, order) { reset_order_service }
|
|
allow(reset_order_service).to receive(:call)
|
|
allow(order).to receive(:update).and_return true
|
|
allow(controller).to receive(:current_order).and_return order
|
|
|
|
# make order workflow pass through delivery
|
|
allow(order).to receive(:next).twice do
|
|
if order.state == 'cart'
|
|
order.update_column :state, 'delivery'
|
|
else
|
|
order.update_column :state, 'complete'
|
|
end
|
|
end
|
|
end
|
|
|
|
it "does not fail to update" do
|
|
expect(controller).to_not receive(:clear_ship_address)
|
|
spree_post :update, order: { shipping_method_id: test_shipping_method_id }
|
|
end
|
|
|
|
it "does not send shipping_method_id to the order model as an attribute" do
|
|
expect(order).to receive(:update).with({})
|
|
spree_post :update, order: { shipping_method_id: test_shipping_method_id }
|
|
end
|
|
|
|
it "selects the shipping_method in the order" do
|
|
expect(order).to receive(:select_shipping_method).with(test_shipping_method_id)
|
|
spree_post :update, order: { shipping_method_id: test_shipping_method_id }
|
|
end
|
|
end
|
|
|
|
context 'when completing the order' do
|
|
before do
|
|
order.state = 'complete'
|
|
order.save!
|
|
allow(order).to receive(:update).and_return(true)
|
|
allow(order).to receive(:next).and_return(true)
|
|
allow(order).to receive(:set_distributor!).and_return(true)
|
|
end
|
|
|
|
it "sets the new order's token to the same as the old order" do
|
|
order = controller.current_order(true)
|
|
spree_post :update, order: {}
|
|
expect(controller.current_order.token).to eq order.token
|
|
end
|
|
|
|
it 'expires the current order' do
|
|
allow(controller).to receive(:expire_current_order)
|
|
put :update, params: { order: {} }
|
|
expect(controller).to have_received(:expire_current_order)
|
|
end
|
|
|
|
it 'sets the access_token of the session' do
|
|
put :update, params: { order: {} }
|
|
expect(session[:access_token]).to eq(controller.current_order.token)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#expire_current_order' do
|
|
it 'empties the order_id of the session' do
|
|
expect(session).to receive(:[]=).with(:order_id, nil)
|
|
controller.expire_current_order
|
|
end
|
|
|
|
it 'resets the @current_order ivar' do
|
|
controller.expire_current_order
|
|
expect(controller.instance_variable_get(:@current_order)).to be_nil
|
|
end
|
|
end
|
|
|
|
context "via xhr" do
|
|
before do
|
|
allow(controller).to receive(:current_distributor).and_return(distributor)
|
|
|
|
allow(controller).to receive(:current_order_cycle).and_return(order_cycle)
|
|
allow(controller).to receive(:current_order).and_return(order)
|
|
end
|
|
|
|
it "returns errors and flash if order.update fails" do
|
|
spree_post :update, format: :json, order: {}
|
|
expect(response.status).to eq(400)
|
|
expect(response.body).to eq({ errors: assigns[:order].errors,
|
|
flash: { error: order.errors.full_messages.to_sentence } }.to_json)
|
|
end
|
|
|
|
it "returns errors and flash if order.next fails" do
|
|
allow(order).to receive(:update).and_return true
|
|
allow(order).to receive(:next).and_return false
|
|
spree_post :update, format: :json, order: {}
|
|
expect(response.body).to eq({ errors: assigns[:order].errors,
|
|
flash: { error: "Payment could not be processed, please check the details you entered" } }.to_json)
|
|
end
|
|
|
|
it "returns order confirmation url on success" do
|
|
allow(OrderCompletionReset).to receive(:new).with(controller, order) { reset_order_service }
|
|
expect(reset_order_service).to receive(:call)
|
|
|
|
allow(order).to receive(:update).and_return true
|
|
allow(order).to receive(:state).and_return "complete"
|
|
|
|
spree_post :update, format: :json, order: {}
|
|
expect(response.status).to eq(200)
|
|
expect(response.body).to eq({ path: order_path(order) }.to_json)
|
|
end
|
|
|
|
it "returns an error on unexpected failure" do
|
|
allow(order).to receive(:update).and_raise
|
|
|
|
spree_post :update, format: :json, order: {}
|
|
expect(response.status).to eq(400)
|
|
expect(response.body).to eq({ errors: {},
|
|
flash: { error: I18n.t("checkout.failed") } }.to_json)
|
|
end
|
|
|
|
it "returns a specific error on Spree::Core::GatewayError" do
|
|
allow(order).to receive(:update).and_raise(Spree::Core::GatewayError.new("Gateway blow up"))
|
|
spree_post :update, format: :json, order: {}
|
|
|
|
expect(response.status).to eq(400)
|
|
flash_message = I18n.t(:spree_gateway_error_flash_for_checkout, error: "Gateway blow up")
|
|
expect(json_response["flash"]["error"]).to eq flash_message
|
|
end
|
|
|
|
describe "stale object handling" do
|
|
it "retries when a stale object error is encountered" do
|
|
allow(OrderCompletionReset).to receive(:new).with(controller, order) { reset_order_service }
|
|
expect(reset_order_service).to receive(:call)
|
|
|
|
allow(order).to receive(:update).and_return true
|
|
allow(controller).to receive(:state_callback)
|
|
|
|
# The first time, raise a StaleObjectError. The second time, succeed.
|
|
allow(order).to receive(:next).once.
|
|
and_raise(ActiveRecord::StaleObjectError.new(Spree::Variant.new, 'update'))
|
|
allow(order).to receive(:next).once do
|
|
order.update_column :state, 'complete'
|
|
true
|
|
end
|
|
|
|
spree_post :update, format: :json, order: {}
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "tries a maximum of 3 times before giving up and returning an error" do
|
|
allow(order).to receive(:update).and_return true
|
|
allow(order).to receive(:next) {
|
|
raise ActiveRecord::StaleObjectError.new(Spree::Variant.new, 'update')
|
|
}
|
|
|
|
spree_post :update, format: :json, order: {}
|
|
expect(response.status).to eq(400)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Payment redirects" do
|
|
before do
|
|
allow(controller).to receive(:current_distributor) { distributor }
|
|
allow(controller).to receive(:current_order_cycle) { order_cycle }
|
|
allow(controller).to receive(:current_order) { order }
|
|
allow(order).to receive(:update) { true }
|
|
allow(order).to receive(:state) { "payment" }
|
|
end
|
|
|
|
describe "paypal redirect" do
|
|
let(:payment_method) { create(:payment_method, type: "Spree::Gateway::PayPalExpress") }
|
|
let(:paypal_redirect) { instance_double(Checkout::PaypalRedirect) }
|
|
|
|
it "should call Paypal redirect and redirect if a path is provided" do
|
|
expect(Checkout::PaypalRedirect).to receive(:new).and_return(paypal_redirect)
|
|
expect(paypal_redirect).to receive(:path).and_return("test_path")
|
|
|
|
spree_post :update,
|
|
order: { payments_attributes: [{ payment_method_id: payment_method.id }] }
|
|
|
|
expect(response.body).to eq({ path: "test_path" }.to_json)
|
|
end
|
|
end
|
|
|
|
describe "stripe redirect" do
|
|
let(:payment_method) { create(:payment_method, type: "Spree::Gateway::StripeSCA") }
|
|
let(:stripe_redirect) { instance_double(Checkout::StripeRedirect) }
|
|
|
|
it "should call Stripe redirect and redirect if a path is provided" do
|
|
expect(Checkout::StripeRedirect).to receive(:new).and_return(stripe_redirect)
|
|
expect(stripe_redirect).to receive(:path).and_return("test_path")
|
|
|
|
spree_post :update,
|
|
order: { payments_attributes: [{ payment_method_id: payment_method.id }] }
|
|
|
|
expect(response.body).to eq({ path: "test_path" }.to_json)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#action_failed" do
|
|
let(:restart_checkout) { instance_double(OrderCheckoutRestart, call: true) }
|
|
|
|
before do
|
|
controller.instance_variable_set(:@order, order)
|
|
allow(OrderCheckoutRestart).to receive(:new) { restart_checkout }
|
|
allow(controller).to receive(:current_order) { order }
|
|
end
|
|
|
|
it "set shipping_address_from_distributor and restarts the checkout" do
|
|
expect(order.updater).to receive(:shipping_address_from_distributor)
|
|
expect(restart_checkout).to receive(:call)
|
|
expect(controller).to receive(:respond_to)
|
|
|
|
controller.send(:action_failed)
|
|
end
|
|
end
|
|
end
|