Merge pull request #6050 from Matt-Yorkley/stripe-checkout-spec

Stripe checkout spec
This commit is contained in:
Luis Ramos
2020-10-02 18:57:22 +01:00
committed by GitHub
20 changed files with 257 additions and 26 deletions

View File

@@ -40,7 +40,8 @@
= render "layouts/bugsnag_js"
- if Spree::Config.stripe_connect_enabled
%script{:src => "https://js.stripe.com/v3/", :type => "text/javascript"}
= render "shared/stripe_js"
- if !ContentConfig.open_street_map_enabled
%script{src: "//maps.googleapis.com/maps/api/js?libraries=places,geometry#{ ENV['GOOGLE_MAPS_API_KEY'] ? '&key=' + ENV['GOOGLE_MAPS_API_KEY'] : ''} "}

View File

@@ -0,0 +1,5 @@
- if Rails.env.test?
%script{type: "text/javascript"}
= render file: "spec/support/fixtures/stripejs-mock.js"
- else
%script{src: "https://js.stripe.com/v3/", type: "text/javascript"}

View File

@@ -1,6 +1,7 @@
-# = render "spree/admin/payments/source_forms/gateway", payment_method: payment_method
.stripe
%script{:src => "https://js.stripe.com/v3/", :type => "text/javascript"}
= render "shared/stripe_js"
- if Stripe.publishable_key
:javascript
angular.module('admin.payments').value("stripeObject", Stripe("#{Stripe.publishable_key}"))

View File

@@ -1,5 +1,6 @@
.stripe
%script{:src => "https://js.stripe.com/v3/", :type => "text/javascript"}
= render "shared/stripe_js"
- if Stripe.publishable_key
:javascript
angular.module('admin.payments').value("stripeObject", Stripe("#{Stripe.publishable_key}"))

View File

@@ -34,7 +34,7 @@ module OrderManagement
end
context "when the payment method is a stripe payment method" do
let(:payment_method) { create(:stripe_payment_method) }
let(:payment_method) { create(:stripe_connect_payment_method) }
context "and the card is already set (the payment source is a credit card)" do
it "returns the pending payment with no change" do

View File

@@ -704,7 +704,7 @@ describe Admin::SubscriptionsController, type: :controller do
end
context "when other payment methods exist" do
let!(:stripe) { create(:stripe_payment_method, distributors: [shop]) }
let!(:stripe) { create(:stripe_connect_payment_method, distributors: [shop]) }
let!(:paypal) { Spree::Gateway::PayPalExpress.create!(name: "PayPalExpress", distributor_ids: [shop.id]) }
let!(:bogus) { create(:bogus_payment_method, distributors: [shop]) }

View File

@@ -21,7 +21,7 @@ describe Spree::Admin::PaymentsController, type: :controller do
before { @request.env['HTTP_REFERER'] = spree.admin_order_payments_url(payment) }
context "that was processed by stripe" do
let!(:payment_method) { create(:stripe_payment_method, distributors: [shop]) }
let!(:payment_method) { create(:stripe_connect_payment_method, distributors: [shop]) }
let!(:payment) do
create(:payment, order: order, state: 'completed', payment_method: payment_method,
response_code: 'ch_1a2b3c', amount: order.total)
@@ -80,7 +80,7 @@ describe Spree::Admin::PaymentsController, type: :controller do
before { @request.env['HTTP_REFERER'] = spree.admin_order_payments_url(payment) }
context "that was processed by stripe" do
let!(:payment_method) { create(:stripe_payment_method, distributors: [shop]) }
let!(:payment_method) { create(:stripe_connect_payment_method, distributors: [shop]) }
let!(:payment) do
create(:payment, order: order, state: 'completed', payment_method: payment_method,
response_code: 'ch_1a2b3c', amount: order.total + 5)

View File

@@ -45,7 +45,7 @@ describe Spree::Admin::PaymentsController, type: :controller do
end
context "with Stripe payment where payment.process! errors out" do
let!(:payment_method) { create(:stripe_payment_method, distributors: [shop]) }
let!(:payment_method) { create(:stripe_connect_payment_method, distributors: [shop]) }
before do
allow_any_instance_of(Spree::Payment).
to receive(:process!).

View File

@@ -68,7 +68,7 @@ module Spree
let!(:user) { create(:user, enterprise_limit: 2) }
let!(:enterprise1) { create(:distributor_enterprise, owner: user) }
let!(:enterprise2) { create(:distributor_enterprise, owner: create(:user)) }
let!(:payment_method) { create(:stripe_payment_method, distributor_ids: [enterprise1.id, enterprise2.id], preferred_enterprise_id: enterprise2.id) }
let!(:payment_method) { create(:stripe_connect_payment_method, distributor_ids: [enterprise1.id, enterprise2.id], preferred_enterprise_id: enterprise2.id) }
before { allow(controller).to receive(:spree_current_user) { user } }

View File

@@ -23,8 +23,8 @@ FactoryBot.define do
environment 'test'
end
factory :stripe_payment_method, class: Spree::Gateway::StripeConnect do
name 'Stripe'
factory :stripe_connect_payment_method, class: Spree::Gateway::StripeConnect do
name 'StripeConnect'
environment 'test'
distributors { [FactoryBot.create(:enterprise)] }
preferred_enterprise_id { distributors.first.id }

View File

@@ -172,7 +172,7 @@ feature 'Subscriptions' do
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [test_variant, shop_variant], enterprise_fees: [enterprise_fee]) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop]) }
let!(:payment_method) { create(:stripe_connect_payment_method, name: 'Credit Card', distributors: [shop]) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
before do
@@ -319,7 +319,7 @@ feature 'Subscriptions' do
let!(:variant3_oc) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) }
let!(:variant3_ex) { variant3_oc.exchanges.create(sender: shop, receiver: shop, variants: [variant3]) }
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
let!(:stripe_payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop]) }
let!(:stripe_payment_method) { create(:stripe_connect_payment_method, name: 'Credit Card', distributors: [shop]) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
let!(:subscription) {
create(:subscription,
@@ -457,7 +457,7 @@ feature 'Subscriptions' do
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:payment_method) { create(:stripe_payment_method, distributors: [shop]) }
let!(:payment_method) { create(:stripe_connect_payment_method, distributors: [shop]) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
before do

View File

@@ -1,9 +1,12 @@
# frozen_string_literal: true
require 'spec_helper'
feature "Check out with Stripe", js: true do
include AuthenticationHelper
include ShopWorkflow
include CheckoutHelper
include StripeHelper
let(:distributor) { create(:distributor_enterprise) }
let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], variants: [variant]) }
@@ -12,12 +15,14 @@ feature "Check out with Stripe", js: true do
let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor, bill_address_id: nil, ship_address_id: nil) }
let(:shipping_with_fee) { create(:shipping_method, require_ship_address: false, name: "Donkeys", calculator: Calculator::FlatRate.new(preferred_amount: 4.56)) }
let(:free_shipping) { create(:shipping_method) }
let!(:check_with_fee) { create(:payment_method, distributors: [distributor], calculator: Calculator::FlatRate.new(preferred_amount: 5.67)) }
before do
setup_stripe
set_order order
add_product_to_cart order, product
distributor.shipping_methods << shipping_with_fee
distributor.shipping_methods << [shipping_with_fee, free_shipping]
end
context 'login in as user' do
@@ -27,9 +32,9 @@ feature "Check out with Stripe", js: true do
login_as(user)
end
context "with Stripe" do
context "with Stripe Connect" do
let!(:stripe_pm) do
create(:stripe_payment_method, distributors: [distributor])
create(:stripe_connect_payment_method, distributors: [distributor])
end
let!(:saved_card) do
@@ -54,9 +59,6 @@ feature "Check out with Stripe", js: true do
end
before do
allow(Stripe).to receive(:api_key) { "sk_test_12345" }
allow(Stripe).to receive(:publishable_key) { "some_key" }
Spree::Config.set(stripe_connect_enabled: true)
stub_request(:post, "https://api.stripe.com/v1/charges")
.with(basic_auth: ["sk_test_12345", ""])
.to_return(status: 200, body: JSON.generate(response_mock))
@@ -79,4 +81,76 @@ feature "Check out with Stripe", js: true do
end
end
end
describe "using Stripe SCA" do
let!(:stripe_account) { create(:stripe_account, enterprise: distributor) }
let!(:stripe_sca_payment_method) {
create(:stripe_sca_payment_method, distributors: [distributor])
}
let!(:shipping_method) { create(:shipping_method) }
context "with guest checkout" do
context "when the card is accepted" do
before do
stub_payment_intents_post_request order: order
stub_payment_intent_get_request
stub_hub_payment_methods_request
stub_successful_capture_request order: order
end
it "completes checkout successfully" do
visit checkout_path
checkout_as_guest
fill_out_form(
free_shipping.name,
stripe_sca_payment_method.name,
save_default_addresses: false
)
fill_out_card_details
place_order
expect(page).to have_content "Confirmed"
expect(order.reload.completed?).to eq true
expect(order.payments.first.state).to eq "completed"
end
end
context "when the card is rejected" do
let(:error_message) { "Card was declined: insufficient funds." }
before do
stub_payment_intents_post_request order: order
stub_payment_intent_get_request
stub_hub_payment_methods_request
stub_failed_capture_request order: order, response: { message: error_message }
end
it "shows an error message from the Stripe response" do
visit checkout_path
checkout_as_guest
fill_out_form(
free_shipping.name,
stripe_sca_payment_method.name,
save_default_addresses: false
)
fill_out_card_details
place_order
expect(page).to have_content error_message
expect(order.reload.state).to eq "cart"
expect(order.payments.first.state).to eq "failed"
end
end
end
end
end

View File

@@ -237,8 +237,8 @@ describe EnterprisesHelper, type: :helper do
end
context "when StripeConnect payment methods are present" do
let!(:pm3) { create(:stripe_payment_method, distributors: [distributor], preferred_enterprise_id: distributor.id) }
let!(:pm4) { create(:stripe_payment_method, distributors: [distributor], preferred_enterprise_id: some_other_distributor.id) }
let!(:pm3) { create(:stripe_connect_payment_method, distributors: [distributor], preferred_enterprise_id: distributor.id) }
let!(:pm4) { create(:stripe_connect_payment_method, distributors: [distributor], preferred_enterprise_id: some_other_distributor.id) }
let(:available_payment_methods) { helper.available_payment_methods }
around do |example|

View File

@@ -6,7 +6,7 @@ module Stripe
describe ProfileStorer do
describe "create_customer_from_token" do
let(:payment) { create(:payment) }
let(:stripe_payment_method) { create(:stripe_payment_method) }
let(:stripe_payment_method) { create(:stripe_connect_payment_method) }
let(:profile_storer) { Stripe::ProfileStorer.new(payment, stripe_payment_method.provider) }
let(:customer_id) { "cus_A123" }

View File

@@ -730,7 +730,7 @@ describe Spree::Order do
describe "determining checkout steps for an order" do
let!(:enterprise) { create(:enterprise) }
let!(:order) { create(:order, distributor: enterprise) }
let!(:payment_method) { create(:stripe_payment_method, distributor_ids: [enterprise.id]) }
let!(:payment_method) { create(:stripe_connect_payment_method, distributor_ids: [enterprise.id]) }
let!(:payment) { create(:payment, order: order, payment_method: payment_method) }
it "does not include the :confirm step" do

View File

@@ -847,7 +847,7 @@ describe Spree::Payment do
context "to Stripe payments" do
let(:shop) { create(:enterprise) }
let(:payment_method) { create(:stripe_payment_method, distributor_ids: [create(:distributor_enterprise).id], preferred_enterprise_id: shop.id) }
let(:payment_method) { create(:stripe_connect_payment_method, distributor_ids: [create(:distributor_enterprise).id], preferred_enterprise_id: shop.id) }
let(:payment) { create(:payment, order: order, payment_method: payment_method, amount: order.total) }
let(:calculator) { ::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) }

View File

@@ -14,7 +14,7 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
distributors: [enterprise]
)
end
let!(:payment_method) { create(:stripe_payment_method, distributors: [enterprise]) }
let!(:payment_method) { create(:stripe_connect_payment_method, distributors: [enterprise]) }
let!(:stripe_account) { create(:stripe_account, enterprise: enterprise) }
let!(:line_item) { create(:line_item, price: 12.34) }
let!(:order) { line_item.order }

View File

@@ -130,6 +130,15 @@ RSpec.configure do |config|
ActionController::Base.perform_caching = caching
end
# Show javascript errors in test output with `js_debug: true`
config.after(:each, :js_debug) do
errors = page.driver.browser.manage.logs.get(:browser)
if errors.present?
message = errors.map(&:message).join("\n")
puts message
end
end
config.before(:all) { restart_driver }
# Geocoding

View File

@@ -0,0 +1,59 @@
// StripeJS fixture for using Stripe in feature specs. Mimics credit card form and Element objects.
// Based on: https://github.com/thoughtbot/fake_stripe/blob/v0.3.0/lib/fake_stripe/assets/v3.js
// The original has been adapted to work with OFN (see commit history for details).
class Element {
mount(el) {
if (typeof el === "string") {
el = document.querySelector(el);
}
el.classList.add('StripeElement');
el.innerHTML = `
<input id="stripe-cardnumber" name="cardnumber" placeholder="Card number" size="16" type="text">
<input name="exp-date" placeholder="MM / YY" size="6" type="text">
<input name="cvc" placeholder="CVC" size="3" type="text">
`;
}
addEventListener(event) {
return true;
}
}
window.Stripe = () => {
const fetchLastFour = () => {
return document.getElementById("stripe-cardnumber").value.substr(-4, 4);
};
return {
createPaymentMethod: () => {
return new Promise(resolve => {
resolve({
paymentMethod: {
id: "pm_123",
card: {
brand: 'visa',
last4: fetchLastFour(),
exp_month: "10",
exp_year: "2050"
}
}
});
});
},
elements: () => {
return {
create: (type, options) => new Element()
};
},
createToken: card => {
return new Promise(resolve => {
resolve({ token: { id: "tok_123", card: { last4: fetchLastFour() } } });
});
}
};
};

View File

@@ -0,0 +1,81 @@
# frozen_string_literal: true
module StripeHelper
def fill_out_card_details
expect(page).to have_css("input[name='cardnumber']")
fill_in 'Card number', with: '4242424242424242'
fill_in 'MM / YY', with: "01/#{DateTime.now.year + 1}"
fill_in 'CVC', with: '123'
end
def setup_stripe
allow(Stripe).to receive(:api_key) { "sk_test_12345" }
allow(Stripe).to receive(:publishable_key) { "pk_test_12345" }
Spree::Config.set(stripe_connect_enabled: true)
end
def stub_payment_intents_post_request(order:, response: {})
stub_request(:post, "https://api.stripe.com/v1/payment_intents")
.with(basic_auth: ["sk_test_12345", ""], body: /.*#{order.number}/)
.to_return(payment_intent_authorize_response_mock(response))
end
def stub_payment_intent_get_request(response: {})
stub_request(:get, "https://api.stripe.com/v1/payment_intents/pi_123")
.with(headers: { 'Stripe-Account' => 'abc123' })
.to_return(payment_intent_authorize_response_mock(response))
end
def stub_hub_payment_methods_request(response: {})
stub_request(:post, "https://api.stripe.com/v1/payment_methods")
.with(body: { payment_method: "pm_123" },
headers: { 'Stripe-Account' => 'abc123' })
.to_return(hub_payment_method_response_mock(response))
end
def stub_successful_capture_request(order:, response: {})
stub_capture_request(order, payment_successful_capture_mock(response))
end
def stub_failed_capture_request(order:, response: {})
stub_capture_request(order, payment_failed_capture_mock(response))
end
def stub_capture_request(order, response_mock)
stub_request(:post, "https://api.stripe.com/v1/payment_intents/pi_123/capture")
.with(body: { amount_to_capture: Spree::Money.new(order.total).cents },
headers: { 'Stripe-Account' => 'abc123' })
.to_return(response_mock)
end
private
def payment_intent_authorize_response_mock(options)
{ status: options[:code] || 200,
body: JSON.generate(id: "pi_123",
object: "payment_intent",
amount: 2000,
status: options[:intent_status] || "requires_capture",
last_payment_error: nil,
charges: { data: [{ id: "ch_1234", amount: 2000 }] }) }
end
def payment_successful_capture_mock(options)
{ status: options[:code] || 200,
body: JSON.generate(object: "payment_intent",
amount: 2000,
charges: { data: [{ id: "ch_1234", amount: 2000 }] }) }
end
def payment_failed_capture_mock(options)
{ status: options[:code] || 402,
body: JSON.generate(error: { message:
options[:message] || "payment-method-failure" }) }
end
def hub_payment_method_response_mock(options)
{ status: options[:code] || 200,
body: JSON.generate(id: "pm_456", customer: "cus_A123") }
end
end