├── .rspec ├── lib └── stripe_mock │ ├── version.rb │ ├── errors │ ├── unsupported_request_error.rb │ ├── closed_client_connection_error.rb │ ├── uninitialized_instance_error.rb │ ├── unstarted_state_error.rb │ ├── stripe_mock_error.rb │ └── server_timeout_error.rb │ ├── api │ ├── debug.rb │ ├── account_balance.rb │ ├── conversion_rate.rb │ ├── bank_tokens.rb │ ├── card_tokens.rb │ ├── live.rb │ ├── global_id_prefix.rb │ ├── test_helpers.rb │ ├── instance.rb │ ├── client.rb │ └── server.rb │ ├── webhook_fixtures │ ├── account.application.deauthorized.json │ ├── coupon.created.json │ ├── coupon.deleted.json │ ├── invoiceitem.deleted.json │ ├── charge.dispute.closed.json │ ├── account.updated.json │ ├── charge.dispute.updated.json │ ├── customer.discount.created.json │ ├── customer.discount.deleted.json │ ├── account.external_account.created.json │ ├── account.external_account.deleted.json │ ├── account.external_account.updated.json │ ├── balance.available.json │ ├── customer.source.deleted.json │ ├── mandate.updated.json │ ├── product.deleted.json │ ├── plan.deleted.json │ ├── charge.refund.updated.json │ ├── product.created.json │ ├── plan.created.json │ ├── price.deleted.json │ ├── plan.updated.json │ ├── customer.source.created.json │ ├── product.updated.json │ ├── price.created.json │ ├── customer.deleted.json │ ├── customer.discount.updated.json │ ├── payout.paid.json │ ├── payout.created.json │ ├── customer.created.json │ ├── customer.source.updated.json │ ├── price.updated.json │ ├── customer.updated.json │ ├── payout.updated.json │ ├── checkout.session.completed.setup_mode.json │ ├── payment_link.created.json │ ├── setup_intent.canceled.json │ ├── setup_intent.succeeded.json │ ├── payment_link.updated.json │ ├── setup_intent.created.json │ ├── checkout.session.completed.payment_mode.json │ ├── charge.updated.json │ ├── payment_method.attached.json │ ├── customer.subscription.trial_will_end.json │ ├── invoice.upcoming.json │ ├── charge.dispute.created.json │ ├── payment_intent.canceled.json │ ├── checkout.session.completed.json │ ├── transfer.paid.json │ ├── transfer.created.json │ ├── transfer.failed.json │ ├── quote.created.json │ ├── quote.canceled.json │ ├── transfer.updated.json │ ├── quote.finalized.json │ ├── payment_intent.created.json │ ├── quote.accepted.json │ ├── invoiceitem.created.json │ ├── invoiceitem.updated.json │ ├── charge.dispute.funds_reinstated.json │ ├── charge.dispute.funds_withdrawn.json │ └── setup_intent.setup_failed.json │ ├── request_handlers │ ├── ephemeral_key.rb │ ├── account_links.rb │ ├── balance.rb │ ├── express_login_links.rb │ ├── helpers │ │ ├── charge_helpers.rb │ │ ├── bank_account_helpers.rb │ │ ├── coupon_helpers.rb │ │ ├── token_helpers.rb │ │ └── external_account_helpers.rb │ ├── country_spec.rb │ ├── payouts.rb │ ├── disputes.rb │ ├── tax_rates.rb │ ├── cards.rb │ ├── subscription_items.rb │ ├── coupons.rb │ ├── plans.rb │ ├── balance_transactions.rb │ ├── invoice_items.rb │ ├── events.rb │ ├── products.rb │ ├── prices.rb │ ├── recipients.rb │ ├── external_accounts.rb │ ├── transfers.rb │ ├── sources.rb │ ├── orders.rb │ ├── accounts.rb │ └── setup_intents.rb │ ├── error_queue.rb │ ├── test_strategies │ ├── mock.rb │ └── live.rb │ ├── util.rb │ └── server.rb ├── spec ├── _dummy │ └── webhooks │ │ └── dummy.event.json ├── fixtures │ └── stripe_webhooks │ │ ├── custom.account.updated.json │ │ └── account.updated.json ├── support │ ├── shared_contexts │ │ └── stripe_validator_spec.rb │ └── stripe_examples.rb ├── shared_stripe_examples │ ├── express_login_link_examples.rb │ ├── account_link_examples.rb │ ├── balance_examples.rb │ ├── ephemeral_key_examples.rb │ ├── validation_examples.rb │ ├── country_specs_examples.rb │ ├── extra_features_examples.rb │ ├── tax_rate_examples.rb │ ├── bank_token_examples.rb │ ├── invoice_item_examples.rb │ ├── payout_examples.rb │ ├── setup_intent_examples.rb │ ├── balance_transaction_examples.rb │ ├── coupon_examples.rb │ ├── subscription_items_examples.rb │ └── account_examples.rb ├── api │ └── instance_spec.rb ├── integration_examples │ ├── prepare_error_examples.rb │ ├── completing_checkout_sessions_example.rb │ ├── charge_token_examples.rb │ └── customer_card_examples.rb ├── spec_helper.rb ├── readme_spec.rb └── instance_spec.rb ├── .env ├── .gitignore ├── Gemfile ├── Rakefile ├── bin └── stripe-mock-server ├── .travis.yml ├── LICENSE.txt └── stripe-ruby-mock.gemspec /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format progress 3 | -------------------------------------------------------------------------------- /lib/stripe_mock/version.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | # stripe-ruby-mock version 3 | VERSION = "3.1.0.rc3" 4 | end 5 | -------------------------------------------------------------------------------- /spec/_dummy/webhooks/dummy.event.json: -------------------------------------------------------------------------------- 1 | { 2 | "val": "success", 3 | "data": { 4 | "object": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/fixtures/stripe_webhooks/custom.account.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "object": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | STRIPE_TEST_SECRET_KEY=sk_test_eFvAvN5rz4GqAbsWxg63Jx79 2 | STRIPE_TEST_OAUTH_ACCESS_TOKEN=sk_test_WnZmEBIHhMcDltNe98sqWN7z -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | html/ 2 | pkg/ 3 | .DS_Store 4 | stripe-mock-server.pid 5 | Gemfile.lock 6 | stripe-mock-server.log 7 | .idea 8 | .ruby-version 9 | -------------------------------------------------------------------------------- /lib/stripe_mock/errors/unsupported_request_error.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | class UnsupportedRequestError < StripeMockError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/stripe_webhooks/account.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_123", 3 | "type": "account.updated", 4 | "data": { 5 | "object": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby "~> 2.7.0" 4 | 5 | platforms :ruby_19 do 6 | gem 'mime-types', '~> 2.6' 7 | gem 'rest-client', '~> 1.8' 8 | end 9 | 10 | group :test do 11 | gem 'rake' 12 | gem 'dotenv' 13 | end 14 | 15 | gemspec 16 | -------------------------------------------------------------------------------- /lib/stripe_mock/api/debug.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | def self.toggle_debug(toggle) 4 | if @state == 'local' 5 | @instance.debug = toggle 6 | elsif @state == 'remote' 7 | @client.set_server_debug(toggle) 8 | end 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /lib/stripe_mock/errors/closed_client_connection_error.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | class ClosedClientConnectionError < StripeMockError 3 | 4 | def initialize 5 | super("This StripeMock client has already been closed.") 6 | end 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/shared_contexts/stripe_validator_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | class StripeValidator 3 | include StripeMock::RequestHandlers::ParamValidators 4 | end 5 | 6 | RSpec.shared_context "stripe validator", shared_context: :metadata do 7 | let(:stripe_validator) { StripeValidator.new } 8 | end -------------------------------------------------------------------------------- /lib/stripe_mock/errors/uninitialized_instance_error.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | class UninitializedInstanceError < StripeMockError 3 | 4 | def initialize 5 | super("StripeMock instance is nil (did you forget to call `StripeMock.start`?)") 6 | end 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/stripe_mock/errors/unstarted_state_error.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | class UnstartedStateError < StripeMockError 3 | 4 | def initialize 5 | super("StripeMock has not been started. Please call StripeMock.start or StripeMock.start_client") 6 | end 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/stripe_mock/errors/stripe_mock_error.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | class StripeMockError < StandardError 3 | 4 | attr_reader :message 5 | 6 | def initialize(message) 7 | @message = message 8 | end 9 | 10 | def to_s 11 | @message 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'rake' 5 | 6 | begin 7 | gem 'rubygems-tasks', '~> 0.2' 8 | require 'rubygems/tasks' 9 | 10 | Gem::Tasks.new 11 | rescue LoadError => e 12 | warn e.message 13 | warn "Run `gem install rubygems-tasks` to install Gem::Tasks." 14 | end 15 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/account.application.deauthorized.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "account.application.deauthorized", 3 | "object": "event", 4 | "created": 1326853478, 5 | "livemode": false, 6 | "id": "evt_00000000000000", 7 | "data": { 8 | "object": { 9 | "id": "cus_00000000000000" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/stripe_mock/api/account_balance.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | def self.set_account_balance(value) 4 | case @state 5 | when 'local' 6 | instance.account_balance = value 7 | when 'remote' 8 | client.set_account_balance(value) 9 | else 10 | raise UnstartedStateError 11 | end 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /lib/stripe_mock/api/conversion_rate.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | def self.set_conversion_rate(value) 4 | case @state 5 | when 'local' 6 | instance.conversion_rate = value 7 | when 'remote' 8 | client.set_conversion_rate(value) 9 | else 10 | raise UnstartedStateError 11 | end 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /lib/stripe_mock/api/bank_tokens.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | def self.generate_bank_token(bank_params = {}) 4 | case @state 5 | when 'local' 6 | instance.generate_bank_token(bank_params) 7 | when 'remote' 8 | client.generate_bank_token(bank_params) 9 | else 10 | raise UnstartedStateError 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/stripe_mock/api/card_tokens.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | def self.generate_card_token(card_params = {}) 4 | case @state 5 | when 'local' 6 | instance.generate_card_token(card_params) 7 | when 'remote' 8 | client.generate_card_token(card_params) 9 | else 10 | raise UnstartedStateError 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/stripe_mock/errors/server_timeout_error.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | class ServerTimeoutError < StripeMockError 3 | 4 | attr_reader :associated_error 5 | 6 | def initialize(associated_error) 7 | @associated_error = associated_error 8 | super("Unable to connect to stripe mock server (did you forget to run `$ stripe-mock-server`?)") 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/ephemeral_key.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module EphemeralKey 4 | def self.included(klass) 5 | klass.add_handler 'post /v1/ephemeral_keys', :create_ephemeral_key 6 | end 7 | 8 | def create_ephemeral_key(route, method_url, params, headers) 9 | Data.mock_ephemeral_key(**params) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/account_links.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module AccountLinks 4 | 5 | def AccountLinks.included(klass) 6 | klass.add_handler 'post /v1/account_links', :new_account_link 7 | end 8 | 9 | def new_account_link(route, method_url, params, headers) 10 | route =~ method_url 11 | Data.mock_account_link(params) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/stripe_mock/api/live.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | def self.toggle_live(toggle) 4 | if @state != 'ready' && @state != 'live' 5 | raise "You cannot toggle StripeMock live when it has already started." 6 | end 7 | if toggle 8 | @state = 'live' 9 | StripeMock.set_default_test_helper_strategy(:live) 10 | else 11 | @state = 'ready' 12 | StripeMock.set_default_test_helper_strategy(:mock) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/express_login_link_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Express Login Link API' do 4 | describe 'create an Express Login Link' do 5 | it 'creates a login link' do 6 | account_link = Stripe::Account.create_login_link('acct_103ED82ePvKYlo2C') 7 | 8 | expect(account_link).to be_a Stripe::LoginLink 9 | expect(account_link.url).to start_with('https://connect.stripe.com/express/') 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/balance.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Balance 4 | 5 | def Balance.included(klass) 6 | klass.add_handler 'get /v1/balance', :get_balance 7 | end 8 | 9 | def get_balance(route, method_url, params, headers) 10 | route =~ method_url 11 | 12 | return_balance = Data.mock_balance(account_balance) 13 | return_balance 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/express_login_links.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module ExpressLoginLinks 4 | 5 | def ExpressLoginLinks.included(klass) 6 | klass.add_handler 'post /v1/accounts/(.*)/login_links', :new_account_login_link 7 | end 8 | 9 | def new_account_login_link(route, method_url, params, headers) 10 | route =~ method_url 11 | Data.mock_express_login_link(params) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/helpers/charge_helpers.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Helpers 4 | 5 | def add_refund_to_charge(refund, charge) 6 | refunds = charge[:refunds] 7 | refunds[:data] << refund 8 | refunds[:total_count] = refunds[:data].count 9 | 10 | charge[:amount_refunded] = refunds[:data].reduce(0) {|sum, r| sum + r[:amount].to_i } 11 | charge[:refunded] = true 12 | end 13 | 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/account_link_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Account Link API' do 4 | describe 'create account link' do 5 | it 'creates an account link' do 6 | account_link = Stripe::AccountLink.create( 7 | type: 'onboarding', 8 | account: 'acct_103ED82ePvKYlo2C', 9 | failure_url: 'https://stripe.com', 10 | success_url: 'https://stripe.com' 11 | ) 12 | 13 | expect(account_link).to be_a Stripe::AccountLink 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/balance_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Balance API' do 4 | 5 | it "retrieves a stripe balance" do 6 | StripeMock.set_account_balance(2000) 7 | balance = Stripe::Balance.retrieve() 8 | expect(balance.available[0].amount).to eq(2000) 9 | end 10 | 11 | it "retrieves a stripe instant balance" do 12 | StripeMock.set_account_balance(2000) 13 | balance = Stripe::Balance.retrieve() 14 | expect(balance.instant_available[0].amount).to eq(2000) 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/ephemeral_key_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'EphemeralKey API' do 4 | describe 'Create a new key' do 5 | let(:customer) { Stripe::Customer.create email: 'test@example.com' } 6 | let(:version) { '2016-07-06' } 7 | 8 | it 'creates a new key' do 9 | key = Stripe::EphemeralKey.create( 10 | { customer: customer.id }, 11 | { stripe_version: version } 12 | ) 13 | 14 | expect(key[:associated_objects][0][:id]).to eq customer.id 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/helpers/bank_account_helpers.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Helpers 4 | 5 | def verify_bank_account(object, bank_account_id, class_name='Customer') 6 | bank_accounts = object[:external_accounts] || object[:bank_accounts] || object[:sources] 7 | bank_account = bank_accounts[:data].find{|acc| acc[:id] == bank_account_id } 8 | return if bank_account.nil? 9 | bank_account['status'] = 'verified' 10 | bank_account 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/validation_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Server Validation', :live => true do 4 | 5 | def credit_card_valid?(account_number) 6 | digits = account_number.scan(/./).map(&:to_i) 7 | check = digits.pop 8 | 9 | sum = digits.reverse.each_slice(2).map do |x, y| 10 | [(x * 2).divmod(10), y || 0] 11 | end.flatten.inject(:+) 12 | 13 | (10 - sum % 10) == check 14 | end 15 | 16 | it "runs a luhn check for charges" do 17 | Stripe::Charge.new :with => "4242424242424241" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/stripe_mock/api/global_id_prefix.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | def self.global_id_prefix 4 | if StripeMock.client 5 | StripeMock.client.server_global_id_prefix 6 | else 7 | case @global_id_prefix 8 | when false then "" 9 | when nil then "test_" 10 | else @global_id_prefix 11 | end 12 | end 13 | end 14 | 15 | def self.global_id_prefix=(value) 16 | if StripeMock.client 17 | StripeMock.client.set_server_global_id_prefix(value) 18 | else 19 | @global_id_prefix = value 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/coupon.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "coupon.created", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "25OFF_00000000000000", 10 | "percent_off": 25, 11 | "amount_off": null, 12 | "currency": "usd", 13 | "object": "coupon", 14 | "livemode": false, 15 | "duration": "repeating", 16 | "redeem_by": null, 17 | "max_redemptions": null, 18 | "times_redeemed": 0, 19 | "duration_in_months": 3, 20 | "valid": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/stripe_mock/error_queue.rb: -------------------------------------------------------------------------------- 1 | require 'drb/drb' 2 | 3 | module StripeMock 4 | class ErrorQueue 5 | include DRb::DRbUndumped 6 | extend DRb::DRbUndumped 7 | 8 | def initialize 9 | @queue = [] 10 | end 11 | 12 | def queue(error, handler_names) 13 | @queue << handler_names.map {|n| [n, error]} 14 | end 15 | 16 | def error_for_handler_name(handler_name) 17 | return nil if @queue.count == 0 18 | triggers = @queue.first 19 | (triggers.assoc(:all) || triggers.assoc(handler_name) || [])[1] 20 | end 21 | 22 | def dequeue 23 | @queue.shift 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/coupon.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "coupon.deleted", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "25OFF_00000000000000", 10 | "percent_off": 25, 11 | "amount_off": null, 12 | "currency": "usd", 13 | "object": "coupon", 14 | "livemode": false, 15 | "duration": "repeating", 16 | "redeem_by": null, 17 | "max_redemptions": null, 18 | "times_redeemed": 0, 19 | "duration_in_months": 3, 20 | "valid": false 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/invoiceitem.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "invoiceitem.deleted", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "object": "invoiceitem", 10 | "id": "ii_00000000000000", 11 | "date": 1372126711, 12 | "amount": 2500, 13 | "livemode": false, 14 | "proration": false, 15 | "currency": "usd", 16 | "customer": "cus_00000000000000", 17 | "description": "Plan: Veteran's Club Signup Fee Plan Id: 4", 18 | "invoice": "in_00000000000000" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/charge.dispute.closed.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.dispute.closed", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "charge": "ch_00000000000000", 10 | "amount": 1000, 11 | "created": 1381080229, 12 | "status": "won", 13 | "livemode": false, 14 | "currency": "usd", 15 | "object": "dispute", 16 | "reason": "general", 17 | "balance_transaction": "txn_00000000000000", 18 | "evidence_due_by": 1382745599, 19 | "evidence": "Here is some evidence" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/country_specs_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Country Spec API' do 4 | context 'retrieve country', live: true do 5 | it 'retrieves a stripe country spec' do 6 | country = Stripe::CountrySpec.retrieve('US') 7 | 8 | expect(country).to be_a Stripe::CountrySpec 9 | expect(country.id).to match /US/ 10 | end 11 | 12 | it "cannot retrieve a stripe country that doesn't exist" do 13 | expect { Stripe::CountrySpec.retrieve('nope') } 14 | .to raise_error(Stripe::InvalidRequestError, /(nope is not currently supported by Stripe)|(Country 'nope' is unknown)/) 15 | 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/stripe_mock/api/test_helpers.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | def self.create_test_helper(strategy=nil) 4 | if strategy 5 | get_test_helper_strategy(strategy).new 6 | elsif @__test_strat 7 | @__test_strat.new 8 | else 9 | TestStrategies::Mock.new 10 | end 11 | end 12 | 13 | def self.set_default_test_helper_strategy(strategy) 14 | @__test_strat = get_test_helper_strategy(strategy) 15 | end 16 | 17 | def self.get_test_helper_strategy(strategy) 18 | case strategy.to_sym 19 | when :mock then TestStrategies::Mock 20 | when :live then TestStrategies::Live 21 | else raise "Invalid test helper strategy: #{strategy.inspect}" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/account.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "account.updated", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "acct_00000000000000", 10 | "email": "test@stripe.com", 11 | "statement_descriptor": "TEST", 12 | "details_submitted": true, 13 | "charge_enabled": false, 14 | "payouts_enabled": false, 15 | "currencies_supported": [ 16 | "USD" 17 | ], 18 | "default_currency": "USD", 19 | "country": "US", 20 | "object": "account" 21 | }, 22 | "previous_attributes": { 23 | "details_submitted": false 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/charge.dispute.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.dispute.updated", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "charge": "ch_00000000000000", 10 | "amount": 1000, 11 | "created": 1381080226, 12 | "status": "under_review", 13 | "livemode": false, 14 | "currency": "usd", 15 | "object": "dispute", 16 | "reason": "general", 17 | "balance_transaction": "txn_00000000000000", 18 | "evidence_due_by": 1382745599, 19 | "evidence": "Here is some evidence" 20 | }, 21 | "previous_attributes": { 22 | "evidence": null 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bin/stripe-mock-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib = File.expand_path(File.dirname(__FILE__) + '/../lib') 4 | $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib) 5 | 6 | require 'trollop' 7 | 8 | opts = Trollop::options do 9 | opt :port, "Listening port", :type => :int, :default => 4999 10 | opt :host, "Host to listen on", :type => :string, :default => '0.0.0.0' 11 | opt :server, "Server to use", :type => :string, :default => 'thin' 12 | opt :debug, "Request and response output", :default => true 13 | opt :pid_path, "Location to put server pid file", :type => :string, :default => './stripe-mock-server.pid' 14 | end 15 | 16 | require 'stripe_mock' 17 | require 'stripe_mock/server' 18 | 19 | StripeMock::Server.start_new(opts) 20 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/country_spec.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module CountrySpec 4 | 5 | def CountrySpec.included(klass) 6 | klass.add_handler 'get /v1/country_specs/(.*)', :retrieve_country_spec 7 | end 8 | 9 | def retrieve_country_spec(route, method_url, params, headers) 10 | route =~ method_url 11 | 12 | unless ["AT", "AU", "BE", "CA", "DE", "DK", "ES", "FI", "FR", "GB", "IE", "IT", "JP", "LU", "NL", "NO", "SE", "SG", "US"].include?($1) 13 | raise Stripe::InvalidRequestError.new("#{$1} is not currently supported by Stripe.", $1.to_s) 14 | end 15 | 16 | country_spec[$1] ||= Data.mock_country_spec($1) 17 | 18 | assert_existence :country_spec, $1, country_spec[$1] 19 | end 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.discount.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.discount.created", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "coupon": { 10 | "id": "25OFF_00000000000000", 11 | "percent_off": 25, 12 | "amount_off": null, 13 | "currency": "usd", 14 | "object": "coupon", 15 | "livemode": false, 16 | "duration": "repeating", 17 | "redeem_by": null, 18 | "max_redemptions": null, 19 | "times_redeemed": 0, 20 | "duration_in_months": 3 21 | }, 22 | "start": 1381080505, 23 | "object": "discount", 24 | "customer": "cus_00000000000000", 25 | "end": 1389029305 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.discount.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.discount.deleted", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "coupon": { 10 | "id": "25OFF_00000000000000", 11 | "percent_off": 25, 12 | "amount_off": null, 13 | "currency": "usd", 14 | "object": "coupon", 15 | "livemode": false, 16 | "duration": "repeating", 17 | "redeem_by": null, 18 | "max_redemptions": null, 19 | "times_redeemed": 0, 20 | "duration_in_months": 3 21 | }, 22 | "start": 1381080512, 23 | "object": "discount", 24 | "customer": "cus_00000000000000", 25 | "end": 1389029312 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Helpers 4 | def add_coupon_to_object(object, coupon) 5 | discount_attrs = {}.tap do |attrs| 6 | attrs[object[:object]] = object[:id] 7 | attrs[:coupon] = coupon 8 | attrs[:start] = Time.now.to_i 9 | attrs[:end] = (DateTime.now >> coupon[:duration_in_months].to_i).to_time.to_i if coupon[:duration] == 'repeating' 10 | end 11 | 12 | object[:discount] = Stripe::Discount.construct_from(discount_attrs) 13 | object 14 | end 15 | 16 | def delete_coupon_from_object(object) 17 | object[:discount] = nil 18 | object 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/account.external_account.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created":1326853478, 3 | "livemode":false, 4 | "id":"evt_00000000000000", 5 | "type":"account.external_account.created", 6 | "object":"event", 7 | "data":{ 8 | "object":{ 9 | "id":"ba_00000000000000", 10 | "object":"bank_account", 11 | "account":"acct_00000000000000", 12 | "account_holder_name":"Jane Austen", 13 | "account_holder_type":"individual", 14 | "bank_name":"STRIPE TEST BANK", 15 | "country":"US", 16 | "currency":"eur", 17 | "default_for_currency":false, 18 | "fingerprint":"efGCBmiwp56O1lsN", 19 | "last4":"6789", 20 | "metadata":{ 21 | 22 | }, 23 | "routing_number":"110000000", 24 | "status":"new" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/account.external_account.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "created":1326853478, 3 | "livemode":false, 4 | "id":"evt_00000000000000", 5 | "type":"account.external_account.deleted", 6 | "object":"event", 7 | "data":{ 8 | "object":{ 9 | "id":"ba_00000000000000", 10 | "object":"bank_account", 11 | "account":"acct_00000000000000", 12 | "account_holder_name":"Jane Austen", 13 | "account_holder_type":"individual", 14 | "bank_name":"STRIPE TEST BANK", 15 | "country":"US", 16 | "currency":"eur", 17 | "default_for_currency":false, 18 | "fingerprint":"efGCBmiwp56O1lsN", 19 | "last4":"6789", 20 | "metadata":{ 21 | 22 | }, 23 | "routing_number":"110000000", 24 | "status":"new" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/account.external_account.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created":1326853478, 3 | "livemode":false, 4 | "id":"evt_00000000000000", 5 | "type":"account.external_account.updated", 6 | "object":"event", 7 | "data":{ 8 | "object":{ 9 | "id":"ba_00000000000000", 10 | "object":"bank_account", 11 | "account":"acct_00000000000000", 12 | "account_holder_name":"Jane Austen", 13 | "account_holder_type":"individual", 14 | "bank_name":"STRIPE TEST BANK", 15 | "country":"US", 16 | "currency":"eur", 17 | "default_for_currency":false, 18 | "fingerprint":"efGCBmiwp56O1lsN", 19 | "last4":"6789", 20 | "metadata":{ 21 | 22 | }, 23 | "routing_number":"110000000", 24 | "status":"new" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/balance.available.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648319937, 6 | "data": { 7 | "object": { 8 | "object": "balance", 9 | "available": [ 10 | { 11 | "amount": 7648, 12 | "currency": "usd", 13 | "source_types": { 14 | "card": 7648 15 | } 16 | } 17 | ], 18 | "livemode": false, 19 | "pending": [ 20 | { 21 | "amount": 37734, 22 | "currency": "usd", 23 | "source_types": { 24 | "card": 37734 25 | } 26 | } 27 | ] 28 | } 29 | }, 30 | "livemode": false, 31 | "pending_webhooks": 2, 32 | "request": { 33 | "id": null, 34 | "idempotency_key": null 35 | }, 36 | "type": "balance.available" 37 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.source.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.source.deleted", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "card_VALID", 10 | "object": "card", 11 | "last4": "4242", 12 | "type": "Visa", 13 | "brand": "Visa", 14 | "funding": "credit", 15 | "exp_month": 3, 16 | "exp_year": 2020, 17 | "fingerprint": "wXWJT135mEK107G8", 18 | "customer": "cus_VALID", 19 | "country": "US", 20 | "name": "Testy Tester", 21 | "address_line1": null, 22 | "address_line2": null, 23 | "address_city": null, 24 | "address_state": null, 25 | "address_zip": null, 26 | "address_country": null, 27 | "cvc_check": "pass", 28 | "address_line1_check": null, 29 | "address_zip_check": null 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spec/api/instance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe StripeMock do 4 | describe ".mock" do 5 | it "yields the given block between starting and stopping StripeMock" do 6 | expect(StripeMock.instance).to be_nil 7 | expect(StripeMock.state).to eq "ready" 8 | 9 | StripeMock.mock do 10 | expect(StripeMock.instance).to be_instance_of StripeMock::Instance 11 | expect(StripeMock.state).to eq "local" 12 | end 13 | 14 | expect(StripeMock.instance).to be_nil 15 | expect(StripeMock.state).to eq "ready" 16 | end 17 | 18 | it "stops StripeMock if the given block raises an exception" do 19 | expect(StripeMock.instance).to be_nil 20 | begin 21 | StripeMock.mock do 22 | raise "Uh-oh..." 23 | end 24 | rescue 25 | expect(StripeMock.instance).to be_nil 26 | expect(StripeMock.state).to eq "ready" 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/mandate.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "mandate.updated", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "mandate_000000000000000000000000", 10 | "object": "mandate", 11 | "customer_acceptance": { 12 | "accepted_at": 1326853478, 13 | "online": { 14 | "ip_address": "0.0.0.0", 15 | "user_agent": "UserAgent" 16 | }, 17 | "type": "online" 18 | }, 19 | "livemode": false, 20 | "multi_use": {}, 21 | "payment_method": "pm_000000000000000000000000", 22 | "payment_method_details": {}, 23 | "status": "active", 24 | "type": "multi_use" 25 | }, 26 | "previous_attributes": { 27 | "payment_method_details": { 28 | "bacs_debit": { 29 | "network_status": "pending" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: ruby 3 | rvm: 4 | - 2.4.6 5 | - 2.5.5 6 | - 2.6.3 7 | - 2.7.0 8 | before_install: 9 | - gem install bundler -v '< 2' 10 | before_script: 11 | - "sudo touch /var/log/stripe-mock-server.log" 12 | - "sudo chown travis /var/log/stripe-mock-server.log" 13 | script: "bundle exec rspec && bundle exec rspec -t live" 14 | 15 | env: 16 | global: 17 | - IS_TRAVIS=true STRIPE_TEST_SECRET_KEY_A=sk_test_BsztzqQjzd7lqkgo1LjEG5DF00KzH7tWKF STRIPE_TEST_SECRET_KEY_B=sk_test_rKCEu0x8jzg6cKPqoey8kUPQ00usQO3KYE STRIPE_TEST_SECRET_KEY_C=sk_test_qeaB7R6Ywp8sC9pzd1ZIABH700YLC7nhmZ STRIPE_TEST_SECRET_KEY_D=sk_test_r1NwHkUW7UyoozyP4aEBD6cs00CI5uDiGq 18 | 19 | notifications: 20 | webhooks: 21 | urls: 22 | - https://webhooks.gitter.im/e/44a1f4718ae2efb67eac 23 | on_success: change # options: [always|never|change] default: always 24 | on_failure: always # options: [always|never|change] default: always 25 | on_start: false # default: false 26 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/product.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320062, 6 | "data": { 7 | "object": { 8 | "id": "prod_00000000000000", 9 | "object": "product", 10 | "active": false, 11 | "attributes": [ 12 | 13 | ], 14 | "created": 1648320060, 15 | "description": "(created by Stripe CLI)t", 16 | "images": [ 17 | 18 | ], 19 | "livemode": false, 20 | "metadata": { 21 | }, 22 | "name": "myproduct", 23 | "package_dimensions": null, 24 | "shippable": null, 25 | "statement_descriptor": null, 26 | "tax_code": null, 27 | "type": "service", 28 | "unit_label": null, 29 | "updated": 1648320062, 30 | "url": null 31 | } 32 | }, 33 | "livemode": false, 34 | "pending_webhooks": 2, 35 | "request": { 36 | "id": "req_00000000000000", 37 | "idempotency_key": null 38 | }, 39 | "type": "product.deleted" 40 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/plan.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320048, 6 | "data": { 7 | "object": { 8 | "id": "plan_00000000000000", 9 | "object": "plan", 10 | "active": false, 11 | "aggregate_usage": null, 12 | "amount": 2000, 13 | "amount_decimal": "2000", 14 | "billing_scheme": "per_unit", 15 | "created": 1648320047, 16 | "currency": "usd", 17 | "interval": "month", 18 | "interval_count": 1, 19 | "livemode": false, 20 | "metadata": { 21 | }, 22 | "nickname": null, 23 | "product": "prod_00000000000000", 24 | "tiers_mode": null, 25 | "transform_usage": null, 26 | "trial_period_days": null, 27 | "usage_type": "licensed" 28 | } 29 | }, 30 | "livemode": false, 31 | "pending_webhooks": 2, 32 | "request": { 33 | "id": "req_00000000000000", 34 | "idempotency_key": null 35 | }, 36 | "type": "plan.deleted" 37 | } -------------------------------------------------------------------------------- /lib/stripe_mock/test_strategies/mock.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module TestStrategies 3 | class Mock < Base 4 | 5 | def delete_product(product_id) 6 | if StripeMock.state == 'remote' 7 | StripeMock.client.destroy_resource('products', product_id) 8 | elsif StripeMock.state == 'local' 9 | StripeMock.instance.products.delete(product_id) 10 | end 11 | end 12 | 13 | def delete_plan(plan_id) 14 | if StripeMock.state == 'remote' 15 | StripeMock.client.destroy_resource('plans', plan_id) 16 | elsif StripeMock.state == 'local' 17 | StripeMock.instance.plans.delete(plan_id) 18 | end 19 | end 20 | 21 | def upsert_stripe_object(object, attributes = {}) 22 | if StripeMock.state == 'remote' 23 | StripeMock.client.upsert_stripe_object(object, attributes) 24 | elsif StripeMock.state == 'local' 25 | StripeMock.instance.upsert_stripe_object(object, attributes) 26 | end 27 | end 28 | 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/charge.refund.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648319945, 6 | "data": { 7 | "object": { 8 | "id": "re_000000000000000000000000", 9 | "object": "refund", 10 | "amount": 100, 11 | "balance_transaction": "txn_000000000000000000000000", 12 | "charge": "ch_000000000000000000000000", 13 | "created": 1648319945, 14 | "currency": "usd", 15 | "metadata": { 16 | "order_id": "6735" 17 | }, 18 | "payment_intent": null, 19 | "reason": null, 20 | "receipt_number": null, 21 | "source_transfer_reversal": null, 22 | "status": "succeeded", 23 | "transfer_reversal": null 24 | }, 25 | "previous_attributes": { 26 | } 27 | }, 28 | "livemode": false, 29 | "pending_webhooks": 2, 30 | "request": { 31 | "id": "req_00000000000000", 32 | "idempotency_key": "38ee50ca-b664-4611-a846-9eaed51bec07" 33 | }, 34 | "type": "charge.refund.updated" 35 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/product.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320108, 6 | "data": { 7 | "object": { 8 | "id": "prod_00000000000000", 9 | "object": "product", 10 | "active": true, 11 | "attributes": [ 12 | 13 | ], 14 | "created": 1648320108, 15 | "description": "(created by Stripe CLI)", 16 | "images": [ 17 | 18 | ], 19 | "livemode": false, 20 | "metadata": { 21 | }, 22 | "name": "myproduct", 23 | "package_dimensions": null, 24 | "shippable": null, 25 | "statement_descriptor": null, 26 | "tax_code": null, 27 | "type": "service", 28 | "unit_label": null, 29 | "updated": 1648320108, 30 | "url": null 31 | } 32 | }, 33 | "livemode": false, 34 | "pending_webhooks": 2, 35 | "request": { 36 | "id": "req_00000000000000", 37 | "idempotency_key": "7f0e85cc-cf96-42be-a33a-07339f9fbe50" 38 | }, 39 | "type": "product.created" 40 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/plan.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320109, 6 | "data": { 7 | "object": { 8 | "id": "price_000000000000000000000000", 9 | "object": "plan", 10 | "active": true, 11 | "aggregate_usage": null, 12 | "amount": 1500, 13 | "amount_decimal": "1500", 14 | "billing_scheme": "per_unit", 15 | "created": 1648320109, 16 | "currency": "usd", 17 | "interval": "month", 18 | "interval_count": 1, 19 | "livemode": false, 20 | "metadata": { 21 | }, 22 | "nickname": null, 23 | "product": "prod_00000000000000", 24 | "tiers_mode": null, 25 | "transform_usage": null, 26 | "trial_period_days": null, 27 | "usage_type": "licensed" 28 | } 29 | }, 30 | "livemode": false, 31 | "pending_webhooks": 2, 32 | "request": { 33 | "id": "req_00000000000000", 34 | "idempotency_key": "64c22132-7234-47d6-a803-4e34ac0e883b" 35 | }, 36 | "type": "plan.created" 37 | } -------------------------------------------------------------------------------- /lib/stripe_mock/api/instance.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | @state = 'ready' 4 | @instance = nil 5 | @original_execute_request_method = Stripe::StripeClient.instance_method(:execute_request) 6 | 7 | def self.start 8 | return false if @state == 'live' 9 | @instance = instance = Instance.new 10 | Stripe::StripeClient.send(:define_method, :execute_request) { |*args, **keyword_args| instance.mock_request(*args, **keyword_args) } 11 | @state = 'local' 12 | end 13 | 14 | def self.stop 15 | return unless @state == 'local' 16 | restore_stripe_execute_request_method 17 | @instance = nil 18 | @state = 'ready' 19 | end 20 | 21 | # Yield the given block between StripeMock.start and StripeMock.stop 22 | def self.mock(&block) 23 | begin 24 | self.start 25 | yield 26 | ensure 27 | self.stop 28 | end 29 | end 30 | 31 | def self.restore_stripe_execute_request_method 32 | Stripe::StripeClient.send(:define_method, :execute_request, @original_execute_request_method) 33 | end 34 | 35 | def self.instance; @instance; end 36 | def self.state; @state; end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Gilbert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/price.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320048, 6 | "data": { 7 | "object": { 8 | "id": "plan_00000000000000", 9 | "object": "price", 10 | "active": false, 11 | "billing_scheme": "per_unit", 12 | "created": 1648320047, 13 | "currency": "usd", 14 | "livemode": false, 15 | "lookup_key": null, 16 | "metadata": { 17 | }, 18 | "nickname": null, 19 | "product": "prod_00000000000000", 20 | "recurring": { 21 | "aggregate_usage": null, 22 | "interval": "month", 23 | "interval_count": 1, 24 | "trial_period_days": null, 25 | "usage_type": "licensed" 26 | }, 27 | "tax_behavior": "unspecified", 28 | "tiers_mode": null, 29 | "transform_quantity": null, 30 | "type": "recurring", 31 | "unit_amount": 2000, 32 | "unit_amount_decimal": "2000" 33 | } 34 | }, 35 | "livemode": false, 36 | "pending_webhooks": 2, 37 | "request": { 38 | "id": "req_00000000000000", 39 | "idempotency_key": null 40 | }, 41 | "type": "price.deleted" 42 | } -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/payouts.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Payouts 4 | 5 | def Payouts.included(klass) 6 | klass.add_handler 'post /v1/payouts', :new_payout 7 | klass.add_handler 'get /v1/payouts', :list_payouts 8 | klass.add_handler 'get /v1/payouts/(.*)', :get_payout 9 | end 10 | 11 | def new_payout(route, method_url, params, headers) 12 | id = new_id('po') 13 | 14 | unless params[:amount].is_a?(Integer) || (params[:amount].is_a?(String) && /^\d+$/.match(params[:amount])) 15 | raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400) 16 | end 17 | 18 | payouts[id] = Data.mock_payout(params.merge :id => id) 19 | end 20 | 21 | def list_payouts(route, method_url, params, headers) 22 | Data.mock_list_object(payouts.clone.values, params) 23 | end 24 | 25 | def get_payout(route, method_url, params, headers) 26 | route =~ method_url 27 | assert_existence :payout, $1, payouts[$1] 28 | payouts[$1] ||= Data.mock_payout(:id => $1) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/plan.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320056, 6 | "data": { 7 | "object": { 8 | "id": "price_000000000000000000000000", 9 | "object": "plan", 10 | "active": true, 11 | "aggregate_usage": null, 12 | "amount": 1500, 13 | "amount_decimal": "1500", 14 | "billing_scheme": "per_unit", 15 | "created": 1648320055, 16 | "currency": "usd", 17 | "interval": "month", 18 | "interval_count": 1, 19 | "livemode": false, 20 | "metadata": { 21 | "foo": "bar" 22 | }, 23 | "nickname": null, 24 | "product": "prod_00000000000000", 25 | "tiers_mode": null, 26 | "transform_usage": null, 27 | "trial_period_days": null, 28 | "usage_type": "licensed" 29 | }, 30 | "previous_attributes": { 31 | "metadata": { 32 | "foo": null 33 | } 34 | } 35 | }, 36 | "livemode": false, 37 | "pending_webhooks": 2, 38 | "request": { 39 | "id": "req_00000000000000", 40 | "idempotency_key": "113cda68-08d8-4118-aff7-ac98c4dc745b" 41 | }, 42 | "type": "plan.updated" 43 | } -------------------------------------------------------------------------------- /spec/shared_stripe_examples/extra_features_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Extra Features' do 4 | 5 | it "can set the global id prefix" do 6 | StripeMock.global_id_prefix = "custom_prefix_" 7 | expect(StripeMock.global_id_prefix).to eq("custom_prefix_") 8 | 9 | customer = Stripe::Customer.create 10 | expect(customer.id).to match /^custom_prefix_cus/ 11 | StripeMock.global_id_prefix = nil 12 | end 13 | 14 | it "can set the global id prefix to nothing" do 15 | StripeMock.global_id_prefix = "" 16 | expect(StripeMock.global_id_prefix).to eq("") 17 | 18 | customer = Stripe::Customer.create 19 | expect(customer.id).to match /^cus/ 20 | 21 | # Support false 22 | StripeMock.global_id_prefix = false 23 | expect(StripeMock.global_id_prefix).to eq("") 24 | 25 | customer = Stripe::Customer.create 26 | expect(customer.id).to match /^cus/ 27 | StripeMock.global_id_prefix = nil 28 | end 29 | 30 | it "has a default global id prefix" do 31 | StripeMock.global_id_prefix = nil 32 | expect(StripeMock.global_id_prefix).to eq("test_") 33 | customer = Stripe::Customer.create 34 | expect(customer.id).to match /^test_cus/ 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.source.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320107, 6 | "data": { 7 | "object": { 8 | "id": "card_000000000000000000000000", 9 | "object": "card", 10 | "address_city": null, 11 | "address_country": null, 12 | "address_line1": null, 13 | "address_line1_check": null, 14 | "address_line2": null, 15 | "address_state": null, 16 | "address_zip": null, 17 | "address_zip_check": null, 18 | "brand": "Visa", 19 | "country": "US", 20 | "customer": "cus_00000000000000", 21 | "cvc_check": null, 22 | "dynamic_last4": null, 23 | "exp_month": 3, 24 | "exp_year": 2023, 25 | "fingerprint": "ZoVSX2dK5igWt2SB", 26 | "funding": "credit", 27 | "last4": "4242", 28 | "metadata": { 29 | }, 30 | "name": null, 31 | "tokenization_method": null 32 | } 33 | }, 34 | "livemode": false, 35 | "pending_webhooks": 2, 36 | "request": { 37 | "id": "req_00000000000000", 38 | "idempotency_key": "381c7773-97ac-4c18-8fbe-e8bff5e6bbad" 39 | }, 40 | "type": "customer.source.created" 41 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/product.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320064, 6 | "data": { 7 | "object": { 8 | "id": "prod_00000000000000", 9 | "object": "product", 10 | "active": true, 11 | "attributes": [ 12 | 13 | ], 14 | "created": 1648320063, 15 | "description": "(created by Stripe CLI)", 16 | "images": [ 17 | 18 | ], 19 | "livemode": false, 20 | "metadata": { 21 | "foo": "bar" 22 | }, 23 | "name": "myproduct", 24 | "package_dimensions": null, 25 | "shippable": null, 26 | "statement_descriptor": null, 27 | "tax_code": null, 28 | "type": "service", 29 | "unit_label": null, 30 | "updated": 1648320064, 31 | "url": null 32 | }, 33 | "previous_attributes": { 34 | "metadata": { 35 | "foo": null 36 | }, 37 | "updated": 1648320063 38 | } 39 | }, 40 | "livemode": false, 41 | "pending_webhooks": 1, 42 | "request": { 43 | "id": "req_00000000000000", 44 | "idempotency_key": "bf930f8a-e972-4b61-9ee0-492f139aefa3" 45 | }, 46 | "type": "product.updated" 47 | } -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/disputes.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Disputes 4 | 5 | def Disputes.included(klass) 6 | klass.add_handler 'get /v1/disputes/(.*)', :get_dispute 7 | klass.add_handler 'post /v1/disputes/(.*)/close', :close_dispute 8 | klass.add_handler 'post /v1/disputes/(.*)', :update_dispute 9 | klass.add_handler 'get /v1/disputes', :list_disputes 10 | end 11 | 12 | def get_dispute(route, method_url, params, headers) 13 | route =~ method_url 14 | assert_existence :dispute, $1, disputes[$1] 15 | end 16 | 17 | def update_dispute(route, method_url, params, headers) 18 | dispute = get_dispute(route, method_url, params, headers) 19 | dispute.merge!(params) 20 | dispute 21 | end 22 | 23 | def close_dispute(route, method_url, params, headers) 24 | dispute = get_dispute(route, method_url, params, headers) 25 | dispute.merge!({:status => 'lost'}) 26 | dispute 27 | end 28 | 29 | def list_disputes(route, method_url, params, headers) 30 | Data.mock_list_object(disputes.values, params) 31 | end 32 | 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/price.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320109, 6 | "data": { 7 | "object": { 8 | "id": "price_000000000000000000000000", 9 | "object": "price", 10 | "active": true, 11 | "billing_scheme": "per_unit", 12 | "created": 1648320109, 13 | "currency": "usd", 14 | "livemode": false, 15 | "lookup_key": null, 16 | "metadata": { 17 | }, 18 | "nickname": null, 19 | "product": "prod_00000000000000", 20 | "recurring": { 21 | "aggregate_usage": null, 22 | "interval": "month", 23 | "interval_count": 1, 24 | "trial_period_days": null, 25 | "usage_type": "licensed" 26 | }, 27 | "tax_behavior": "unspecified", 28 | "tiers_mode": null, 29 | "transform_quantity": null, 30 | "type": "recurring", 31 | "unit_amount": 1500, 32 | "unit_amount_decimal": "1500" 33 | } 34 | }, 35 | "livemode": false, 36 | "pending_webhooks": 2, 37 | "request": { 38 | "id": "req_00000000000000", 39 | "idempotency_key": "64c22132-7234-47d6-a803-4e34ac0e883b" 40 | }, 41 | "type": "price.created" 42 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648319961, 6 | "data": { 7 | "object": { 8 | "id": "cus_00000000000000", 9 | "object": "customer", 10 | "address": null, 11 | "balance": 0, 12 | "created": 1648319961, 13 | "currency": null, 14 | "default_source": null, 15 | "delinquent": false, 16 | "description": "(created by Stripe CLI)", 17 | "discount": null, 18 | "email": null, 19 | "invoice_prefix": "D343B0B7", 20 | "invoice_settings": { 21 | "custom_fields": null, 22 | "default_payment_method": null, 23 | "footer": null 24 | }, 25 | "livemode": false, 26 | "metadata": { 27 | }, 28 | "name": null, 29 | "next_invoice_sequence": 1, 30 | "phone": null, 31 | "preferred_locales": [ 32 | 33 | ], 34 | "shipping": null, 35 | "tax_exempt": "none", 36 | "test_clock": null 37 | } 38 | }, 39 | "livemode": false, 40 | "pending_webhooks": 2, 41 | "request": { 42 | "id": "req_00000000000000", 43 | "idempotency_key": null 44 | }, 45 | "type": "customer.deleted" 46 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.discount.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.discount.updated", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "coupon": { 10 | "id": "25OFF_00000000000000", 11 | "percent_off": 25, 12 | "amount_off": null, 13 | "currency": "usd", 14 | "object": "coupon", 15 | "livemode": false, 16 | "duration": "repeating", 17 | "redeem_by": null, 18 | "max_redemptions": null, 19 | "times_redeemed": 0, 20 | "duration_in_months": 3 21 | }, 22 | "start": 1381080509, 23 | "object": "discount", 24 | "customer": "cus_00000000000000", 25 | "end": 1389029309 26 | }, 27 | "previous_attributes": { 28 | "coupon": { 29 | "id": "OLD_COUPON_ID", 30 | "percent_off": 25, 31 | "amount_off": null, 32 | "currency": "usd", 33 | "object": "coupon", 34 | "livemode": false, 35 | "duration": "repeating", 36 | "redeem_by": null, 37 | "max_redemptions": null, 38 | "times_redeemed": 0, 39 | "duration_in_months": 3 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/payout.paid.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320045, 6 | "data": { 7 | "object": { 8 | "id": "po_000000000000000000000000", 9 | "object": "payout", 10 | "amount": 1100, 11 | "arrival_date": 1648425600, 12 | "automatic": false, 13 | "balance_transaction": "txn_000000000000000000000000", 14 | "created": 1648320044, 15 | "currency": "usd", 16 | "description": "(created by Stripe CLI)", 17 | "destination": "ba_000000000000000000000000", 18 | "failure_balance_transaction": null, 19 | "failure_code": null, 20 | "failure_message": null, 21 | "livemode": false, 22 | "metadata": { 23 | }, 24 | "method": "standard", 25 | "original_payout": null, 26 | "reversed_by": null, 27 | "source_type": "card", 28 | "statement_descriptor": null, 29 | "status": "paid", 30 | "type": "bank_account" 31 | } 32 | }, 33 | "livemode": false, 34 | "pending_webhooks": 2, 35 | "request": { 36 | "id": "req_00000000000000", 37 | "idempotency_key": "ae68e2e7-0edb-4181-a35f-f36dcc9148c9" 38 | }, 39 | "type": "payout.paid" 40 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/payout.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320045, 6 | "data": { 7 | "object": { 8 | "id": "po_000000000000000000000000", 9 | "object": "payout", 10 | "amount": 1100, 11 | "arrival_date": 1648425600, 12 | "automatic": false, 13 | "balance_transaction": "txn_000000000000000000000000", 14 | "created": 1648320044, 15 | "currency": "usd", 16 | "description": "(created by Stripe CLI)", 17 | "destination": "ba_000000000000000000000000", 18 | "failure_balance_transaction": null, 19 | "failure_code": null, 20 | "failure_message": null, 21 | "livemode": false, 22 | "metadata": { 23 | }, 24 | "method": "standard", 25 | "original_payout": null, 26 | "reversed_by": null, 27 | "source_type": "card", 28 | "statement_descriptor": null, 29 | "status": "paid", 30 | "type": "bank_account" 31 | } 32 | }, 33 | "livemode": false, 34 | "pending_webhooks": 2, 35 | "request": { 36 | "id": "req_00000000000000", 37 | "idempotency_key": "ae68e2e7-0edb-4181-a35f-f36dcc9148c9" 38 | }, 39 | "type": "payout.created" 40 | } -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/tax_rates.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module TaxRates 4 | def TaxRates.included(klass) 5 | klass.add_handler 'post /v1/tax_rates', :new_tax_rate 6 | klass.add_handler 'post /v1/tax_rates/([^/]*)', :update_tax_rate 7 | klass.add_handler 'get /v1/tax_rates/([^/]*)', :get_tax_rate 8 | klass.add_handler 'get /v1/tax_rates', :list_tax_rates 9 | end 10 | 11 | def update_tax_rate(route, method_url, params, headers) 12 | route =~ method_url 13 | rate = assert_existence :tax_rate, $1, tax_rates[$1] 14 | rate.merge!(params) 15 | rate 16 | end 17 | 18 | def new_tax_rate(route, method_url, params, headers) 19 | params[:id] ||= new_id('txr') 20 | tax_rates[ params[:id] ] = Data.mock_tax_rate(params) 21 | tax_rates[ params[:id] ] 22 | end 23 | 24 | def list_tax_rates(route, method_url, params, headers) 25 | Data.mock_list_object(tax_rates.values, params) 26 | end 27 | 28 | def get_tax_rate(route, method_url, params, headers) 29 | route =~ method_url 30 | tax_rate = assert_existence :tax_rate, $1, tax_rates[$1] 31 | tax_rate.clone 32 | end 33 | end 34 | end 35 | end 36 | 37 | -------------------------------------------------------------------------------- /lib/stripe_mock/api/client.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | def self.client 4 | @client 5 | end 6 | 7 | def self.start_client(port=4999) 8 | return false if @state == 'live' 9 | return @client unless @client.nil? 10 | 11 | Stripe::StripeClient.send(:define_method, :execute_request) { |*args, **keyword_args| StripeMock.redirect_to_mock_server(*args, **keyword_args) } 12 | @client = StripeMock::Client.new(port) 13 | @state = 'remote' 14 | @client 15 | end 16 | 17 | def self.stop_client(opts={}) 18 | return false unless @state == 'remote' 19 | @state = 'ready' 20 | 21 | restore_stripe_execute_request_method 22 | @client.clear_server_data if opts[:clear_server_data] == true 23 | @client.cleanup 24 | @client = nil 25 | true 26 | end 27 | 28 | private 29 | 30 | def self.redirect_to_mock_server(method, url, api_key: nil, api_base: nil, params: {}, headers: {}) 31 | handler = Instance.handler_for_method_url("#{method} #{url}") 32 | 33 | if mock_error = client.error_queue.error_for_handler_name(handler[:name]) 34 | client.error_queue.dequeue 35 | raise mock_error 36 | end 37 | 38 | Stripe::Util.symbolize_names client.mock_request(method, url, api_key: api_key, params: params, headers: headers) 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /stripe-ruby-mock.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path("lib/stripe_mock/version", __dir__) 2 | 3 | Gem::Specification.new do |gem| 4 | gem.name = "stripe-ruby-mock" 5 | gem.version = StripeMock::VERSION 6 | gem.summary = "TDD with stripe" 7 | gem.description = "A drop-in library to test stripe without hitting their servers" 8 | gem.license = "MIT" 9 | gem.authors = ["Gilbert"] 10 | gem.email = "gilbertbgarza@gmail.com" 11 | gem.homepage = "https://github.com/stripe-ruby-mock/stripe-ruby-mock" 12 | gem.metadata = { 13 | "bug_tracker_uri" => "https://github.com/stripe-ruby-mock/stripe-ruby-mock/issues", 14 | "changelog_uri" => "https://github.com/stripe-ruby-mock/stripe-ruby-mock/blob/master/CHANGELOG.md", 15 | "source_code_uri" => "https://github.com/stripe-ruby-mock/stripe-ruby-mock" 16 | } 17 | 18 | gem.files = `git ls-files`.split($/) 19 | gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } 20 | gem.require_paths = ["lib"] 21 | 22 | gem.add_dependency "stripe", "> 8", "< 9" 23 | gem.add_dependency "multi_json", "~> 1.0" 24 | gem.add_dependency "dante", ">= 0.2.0" 25 | 26 | gem.add_development_dependency "rspec", "~> 3.7.0" 27 | gem.add_development_dependency "rubygems-tasks", "~> 0.2" 28 | gem.add_development_dependency "thin", "~> 1.8.1" 29 | end 30 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320107, 6 | "data": { 7 | "object": { 8 | "id": "cus_00000000000000", 9 | "object": "customer", 10 | "address": null, 11 | "balance": 0, 12 | "created": 1648320107, 13 | "currency": null, 14 | "default_source": "card_000000000000000000000000", 15 | "delinquent": false, 16 | "description": "(created by Stripe CLI)", 17 | "discount": null, 18 | "email": null, 19 | "invoice_prefix": "985BA9CB", 20 | "invoice_settings": { 21 | "custom_fields": null, 22 | "default_payment_method": null, 23 | "footer": null 24 | }, 25 | "livemode": false, 26 | "metadata": { 27 | }, 28 | "name": null, 29 | "next_invoice_sequence": 1, 30 | "phone": null, 31 | "preferred_locales": [ 32 | 33 | ], 34 | "shipping": null, 35 | "tax_exempt": "none", 36 | "test_clock": null 37 | } 38 | }, 39 | "livemode": false, 40 | "pending_webhooks": 2, 41 | "request": { 42 | "id": "req_00000000000000", 43 | "idempotency_key": "381c7773-97ac-4c18-8fbe-e8bff5e6bbad" 44 | }, 45 | "type": "customer.created" 46 | } -------------------------------------------------------------------------------- /lib/stripe_mock/api/server.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | @default_server_pid_path = './stripe-mock-server.pid' 3 | @default_server_log_path = './stripe-mock-server.log' 4 | 5 | class << self 6 | attr_writer :default_server_pid_path, :default_server_log_path 7 | 8 | ["pid", "log"].each do |config_type| 9 | define_method("default_server_#{config_type}_path") do 10 | instance_variable_get("@default_server_#{config_type}_path") || "./stripe-mock-server.#{config_type}" 11 | end 12 | end 13 | 14 | def spawn_server(opts={}) 15 | pid_path = opts[:pid_path] || default_server_pid_path 16 | log_path = opts[:log_path] || default_server_log_path 17 | 18 | Dante::Runner.new('stripe-mock-server').execute( 19 | :daemonize => true, :pid_path => pid_path, :log_path => log_path 20 | ){ 21 | StripeMock::Server.start_new(opts) 22 | } 23 | at_exit { 24 | begin 25 | e = $! # last exception 26 | kill_server(pid_path) 27 | ensure 28 | raise e if $! != e 29 | end 30 | } 31 | end 32 | 33 | def kill_server(pid_path=nil) 34 | puts "Killing server at #{pid_path}" 35 | path = pid_path || default_server_pid_path 36 | Dante::Runner.new('stripe-mock-server').execute(:kill => true, :pid_path => path) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.source.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648319966, 6 | "data": { 7 | "object": { 8 | "id": "card_000000000000000000000000", 9 | "object": "card", 10 | "address_city": null, 11 | "address_country": null, 12 | "address_line1": null, 13 | "address_line1_check": null, 14 | "address_line2": null, 15 | "address_state": null, 16 | "address_zip": null, 17 | "address_zip_check": null, 18 | "brand": "Visa", 19 | "country": "US", 20 | "customer": "cus_00000000000000", 21 | "cvc_check": null, 22 | "dynamic_last4": null, 23 | "exp_month": 3, 24 | "exp_year": 2023, 25 | "fingerprint": "ZoVSX2dK5igWt2SB", 26 | "funding": "credit", 27 | "last4": "4242", 28 | "metadata": { 29 | "foo": "bar" 30 | }, 31 | "name": null, 32 | "tokenization_method": null 33 | }, 34 | "previous_attributes": { 35 | "metadata": { 36 | "foo": null 37 | } 38 | } 39 | }, 40 | "livemode": false, 41 | "pending_webhooks": 2, 42 | "request": { 43 | "id": "req_00000000000000", 44 | "idempotency_key": "0b5b3636-5097-40ba-8839-7013d6090ab7" 45 | }, 46 | "type": "customer.source.updated" 47 | } -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/cards.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Cards 4 | 5 | def Cards.included(klass) 6 | klass.add_handler 'get /v1/recipients/(.*)/cards', :retrieve_recipient_cards 7 | klass.add_handler 'get /v1/recipients/(.*)/cards/(.*)', :retrieve_recipient_card 8 | klass.add_handler 'post /v1/recipients/(.*)/cards', :create_recipient_card 9 | klass.add_handler 'delete /v1/recipients/(.*)/cards/(.*)', :delete_recipient_card 10 | end 11 | 12 | def create_recipient_card(route, method_url, params, headers) 13 | route =~ method_url 14 | add_card_to(:recipient, $1, params, recipients) 15 | end 16 | 17 | def retrieve_recipient_cards(route, method_url, params, headers) 18 | route =~ method_url 19 | retrieve_object_cards(:recipient, $1, recipients) 20 | end 21 | 22 | def retrieve_recipient_card(route, method_url, params, headers) 23 | route =~ method_url 24 | recipient = assert_existence :recipient, $1, recipients[$1] 25 | 26 | assert_existence :card, $2, get_card(recipient, $2, "Recipient") 27 | end 28 | 29 | def delete_recipient_card(route, method_url, params, headers) 30 | route =~ method_url 31 | delete_card_from(:recipient, $1, $2, recipients) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/price.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320056, 6 | "data": { 7 | "object": { 8 | "id": "price_000000000000000000000000", 9 | "object": "price", 10 | "active": true, 11 | "billing_scheme": "per_unit", 12 | "created": 1648320055, 13 | "currency": "usd", 14 | "livemode": false, 15 | "lookup_key": null, 16 | "metadata": { 17 | "foo": "bar" 18 | }, 19 | "nickname": null, 20 | "product": "prod_00000000000000", 21 | "recurring": { 22 | "aggregate_usage": null, 23 | "interval": "month", 24 | "interval_count": 1, 25 | "trial_period_days": null, 26 | "usage_type": "licensed" 27 | }, 28 | "tax_behavior": "unspecified", 29 | "tiers_mode": null, 30 | "transform_quantity": null, 31 | "type": "recurring", 32 | "unit_amount": 1500, 33 | "unit_amount_decimal": "1500" 34 | }, 35 | "previous_attributes": { 36 | "metadata": { 37 | "foo": null 38 | } 39 | } 40 | }, 41 | "livemode": false, 42 | "pending_webhooks": 2, 43 | "request": { 44 | "id": "req_00000000000000", 45 | "idempotency_key": "113cda68-08d8-4118-aff7-ac98c4dc745b" 46 | }, 47 | "type": "price.updated" 48 | } -------------------------------------------------------------------------------- /spec/integration_examples/prepare_error_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Card Error Prep' do 4 | 5 | # it "prepares a card error" do 6 | # StripeMock.prepare_card_error(:card_declined, :new_charge) 7 | # cus = Stripe::Customer.create :email => 'alice@bob.com', 8 | # :card => stripe_helper.generate_card_token({ :number => '4242424242424242', :brand => 'Visa' }) 9 | 10 | # expect { 11 | # charge = Stripe::Charge.create({ 12 | # :amount => 999, :currency => 'usd', 13 | # :customer => cus, :card => cus.cards.first, 14 | # :description => 'hello' 15 | # }) 16 | # }.to raise_error Stripe::CardError 17 | # end 18 | 19 | it 'is a valid card error', live: true do 20 | stripe_helper.prepare_card_error 21 | 22 | begin 23 | Stripe::Customer.create( 24 | email: 'alice@bob.com', 25 | source: stripe_helper.generate_card_token(number: '123') 26 | ) 27 | rescue Stripe::CardError => e 28 | body = e.json_body 29 | expect(body).to be_a(Hash) 30 | error = body[:error] 31 | 32 | expect(error[:type]).to eq 'card_error' 33 | expect(error[:param]).to eq 'number' 34 | expect(error[:code]).to eq 'invalid_number' 35 | expect(error[:message]).to eq 'The card number is not a valid credit card number.' 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320110, 6 | "data": { 7 | "object": { 8 | "id": "cus_00000000000000", 9 | "object": "customer", 10 | "address": null, 11 | "balance": 0, 12 | "created": 1648320107, 13 | "currency": "usd", 14 | "default_source": "card_000000000000000000000000", 15 | "delinquent": false, 16 | "description": "(created by Stripe CLI)", 17 | "discount": null, 18 | "email": null, 19 | "invoice_prefix": "985BA9CB", 20 | "invoice_settings": { 21 | "custom_fields": null, 22 | "default_payment_method": null, 23 | "footer": null 24 | }, 25 | "livemode": false, 26 | "metadata": { 27 | }, 28 | "name": null, 29 | "next_invoice_sequence": 1, 30 | "phone": null, 31 | "preferred_locales": [ 32 | 33 | ], 34 | "shipping": null, 35 | "tax_exempt": "none", 36 | "test_clock": null 37 | }, 38 | "previous_attributes": { 39 | "currency": null 40 | } 41 | }, 42 | "livemode": false, 43 | "pending_webhooks": 2, 44 | "request": { 45 | "id": "req_00000000000000", 46 | "idempotency_key": "af06a7d9-5408-4f65-8379-59a93874d04c" 47 | }, 48 | "type": "customer.updated" 49 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/payout.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320046, 6 | "data": { 7 | "object": { 8 | "id": "po_000000000000000000000000", 9 | "object": "payout", 10 | "amount": 1100, 11 | "arrival_date": 1648425600, 12 | "automatic": false, 13 | "balance_transaction": "txn_000000000000000000000000", 14 | "created": 1648320044, 15 | "currency": "usd", 16 | "description": "(created by Stripe CLI)", 17 | "destination": "ba_000000000000000000000000", 18 | "failure_balance_transaction": null, 19 | "failure_code": null, 20 | "failure_message": null, 21 | "livemode": false, 22 | "metadata": { 23 | "foo": "bar" 24 | }, 25 | "method": "standard", 26 | "original_payout": null, 27 | "reversed_by": null, 28 | "source_type": "card", 29 | "statement_descriptor": null, 30 | "status": "paid", 31 | "type": "bank_account" 32 | }, 33 | "previous_attributes": { 34 | "metadata": { 35 | "foo": null 36 | } 37 | } 38 | }, 39 | "livemode": false, 40 | "pending_webhooks": 2, 41 | "request": { 42 | "id": "req_00000000000000", 43 | "idempotency_key": "0d30bd53-9777-49de-8893-882fd2914757" 44 | }, 45 | "type": "payout.updated" 46 | } -------------------------------------------------------------------------------- /lib/stripe_mock/util.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module Util 3 | 4 | def self.rmerge(desh_hash, source_hash) 5 | return source_hash if desh_hash.nil? 6 | return nil if source_hash.nil? 7 | 8 | desh_hash.merge(source_hash) do |key, oldval, newval| 9 | if oldval.is_a?(Array) && newval.is_a?(Array) 10 | oldval.fill(nil, oldval.length...newval.length) 11 | oldval.zip(newval).map {|elems| 12 | if elems[1].nil? 13 | elems[0] 14 | elsif elems[1].is_a?(Hash) && elems[1].is_a?(Hash) 15 | rmerge(elems[0], elems[1]) 16 | else 17 | [elems[0], elems[1]].compact 18 | end 19 | }.flatten 20 | elsif oldval.is_a?(Hash) && newval.is_a?(Hash) 21 | rmerge(oldval, newval) 22 | else 23 | newval 24 | end 25 | end 26 | end 27 | 28 | def self.fingerprint(source) 29 | Digest::SHA1.base64digest(source).gsub(/[^a-z]/i, '')[0..15] 30 | end 31 | 32 | def self.card_merge(old_param, new_param) 33 | if new_param[:number] ||= old_param[:number] 34 | if new_param[:last4] 35 | new_param[:number] = new_param[:number][0..-5] + new_param[:last4] 36 | else 37 | new_param[:last4] = new_param[:number][-4..-1] 38 | end 39 | end 40 | old_param.merge(new_param) 41 | end 42 | 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/checkout.session.completed.setup_mode.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "checkout.session.completed", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "cs_00000000000000", 10 | "object": "checkout.session", 11 | "allow_promotion_codes": null, 12 | "amount_subtotal": null, 13 | "amount_total": null, 14 | "automatic_tax": { 15 | "enabled": false, 16 | "status": null 17 | }, 18 | "billing_address_collection": null, 19 | "cancel_url": "https://example.com/cancel", 20 | "client_reference_id": null, 21 | "currency": null, 22 | "customer": null, 23 | "customer_details": null, 24 | "customer_email": null, 25 | "livemode": false, 26 | "locale": null, 27 | "metadata": {}, 28 | "mode": "setup", 29 | "payment_intent": null, 30 | "payment_method_options": {}, 31 | "payment_method_types": [ 32 | "card" 33 | ], 34 | "payment_status": "no_payment_required", 35 | "setup_intent": "seti_00000000000000", 36 | "shipping": null, 37 | "shipping_address_collection": null, 38 | "submit_type": null, 39 | "subscription": null, 40 | "success_url": "https://example.com/success", 41 | "total_details": null, 42 | "url": null 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/payment_link.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320037, 6 | "data": { 7 | "object": { 8 | "id": "plink_000000000000000000000000", 9 | "object": "payment_link", 10 | "active": true, 11 | "after_completion": { 12 | "hosted_confirmation": { 13 | "custom_message": null 14 | }, 15 | "type": "hosted_confirmation" 16 | }, 17 | "allow_promotion_codes": false, 18 | "application_fee_amount": null, 19 | "application_fee_percent": null, 20 | "automatic_tax": { 21 | "enabled": false 22 | }, 23 | "billing_address_collection": "auto", 24 | "livemode": false, 25 | "metadata": { 26 | }, 27 | "on_behalf_of": null, 28 | "payment_method_types": null, 29 | "phone_number_collection": { 30 | "enabled": false 31 | }, 32 | "shipping_address_collection": null, 33 | "subscription_data": { 34 | "trial_period_days": null 35 | }, 36 | "transfer_data": null, 37 | "url": "https://buy.stripe.com/test_000000000000000000" 38 | } 39 | }, 40 | "livemode": false, 41 | "pending_webhooks": 2, 42 | "request": { 43 | "id": "req_00000000000000", 44 | "idempotency_key": "753b9128-4591-4a12-9392-86732e352266" 45 | }, 46 | "type": "payment_link.created" 47 | } -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/subscription_items.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module SubscriptionItems 4 | 5 | def SubscriptionItems.included(klass) 6 | klass.add_handler 'get /v1/subscription_items', :retrieve_subscription_items 7 | klass.add_handler 'post /v1/subscription_items/([^/]*)', :update_subscription_item 8 | klass.add_handler 'post /v1/subscription_items', :create_subscription_items 9 | end 10 | 11 | def retrieve_subscription_items(route, method_url, params, headers) 12 | route =~ method_url 13 | 14 | require_param(:subscription) unless params[:subscription] 15 | 16 | Data.mock_list_object(subscriptions_items, params) 17 | end 18 | 19 | def create_subscription_items(route, method_url, params, headers) 20 | params[:id] ||= new_id('si') 21 | 22 | require_param(:subscription) unless params[:subscription] 23 | require_param(:plan) unless params[:plan] 24 | 25 | subscriptions_items[params[:id]] = Data.mock_subscription_item(params.merge(plan: plans[params[:plan]])) 26 | end 27 | 28 | def update_subscription_item(route, method_url, params, headers) 29 | route =~ method_url 30 | 31 | subscription_item = assert_existence :subscription_item, $1, subscriptions_items[$1] 32 | subscription_item.merge!(params.merge(plan: plans[params[:plan]])) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/setup_intent.canceled.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320088, 6 | "data": { 7 | "object": { 8 | "id": "seti_000000000000000000000000", 9 | "object": "setup_intent", 10 | "application": null, 11 | "cancellation_reason": "requested_by_customer", 12 | "client_secret": "seti_000000000000000000000000_secret_0000000000000000000000000000000", 13 | "created": 1648320088, 14 | "customer": null, 15 | "description": "(created by Stripe CLI)", 16 | "last_setup_error": null, 17 | "latest_attempt": null, 18 | "livemode": false, 19 | "mandate": null, 20 | "metadata": { 21 | }, 22 | "next_action": null, 23 | "on_behalf_of": null, 24 | "payment_method": null, 25 | "payment_method_options": { 26 | "card": { 27 | "mandate_options": null, 28 | "request_three_d_secure": "automatic" 29 | } 30 | }, 31 | "payment_method_types": [ 32 | "card" 33 | ], 34 | "single_use_mandate": null, 35 | "status": "canceled", 36 | "usage": "off_session" 37 | } 38 | }, 39 | "livemode": false, 40 | "pending_webhooks": 2, 41 | "request": { 42 | "id": "req_00000000000000", 43 | "idempotency_key": "6bc9d964-51d3-47d2-82b2-127b3c5c27cd" 44 | }, 45 | "type": "setup_intent.canceled" 46 | } -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/coupons.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Coupons 4 | 5 | def Coupons.included(klass) 6 | klass.add_handler 'post /v1/coupons', :new_coupon 7 | klass.add_handler 'get /v1/coupons/(.*)', :get_coupon 8 | klass.add_handler 'delete /v1/coupons/(.*)', :delete_coupon 9 | klass.add_handler 'get /v1/coupons', :list_coupons 10 | end 11 | 12 | def new_coupon(route, method_url, params, headers) 13 | params[:id] ||= new_id('coupon') 14 | raise Stripe::InvalidRequestError.new('Missing required param: duration', 'coupon', http_status: 400) unless params[:duration] 15 | raise Stripe::InvalidRequestError.new('You must pass currency when passing amount_off', 'coupon', http_status: 400) if params[:amount_off] && !params[:currency] 16 | coupons[ params[:id] ] = Data.mock_coupon({amount_off: nil, percent_off:nil}.merge(params)) 17 | end 18 | 19 | def get_coupon(route, method_url, params, headers) 20 | route =~ method_url 21 | assert_existence :coupon, $1, coupons[$1] 22 | end 23 | 24 | def delete_coupon(route, method_url, params, headers) 25 | route =~ method_url 26 | assert_existence :coupon, $1, coupons.delete($1) 27 | end 28 | 29 | def list_coupons(route, method_url, params, headers) 30 | Data.mock_list_object(coupons.values, params) 31 | end 32 | 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/helpers/token_helpers.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Helpers 4 | 5 | def generate_bank_token(bank_params = {}) 6 | token = new_id 'btok' 7 | bank_params[:id] = new_id 'bank_account' 8 | @bank_tokens[token] = Data.mock_bank_account bank_params 9 | token 10 | end 11 | 12 | def generate_card_token(card_params = {}) 13 | token = new_id 'tok' 14 | card_params[:id] = new_id 'cc' 15 | @card_tokens[token] = Data.mock_card symbolize_names(card_params) 16 | token 17 | end 18 | 19 | def get_bank_by_token(token) 20 | if token.nil? || @bank_tokens[token].nil? 21 | Data.mock_bank_account 22 | else 23 | @bank_tokens.delete(token) 24 | end 25 | end 26 | 27 | def get_card_by_token(token) 28 | if token.nil? || @card_tokens[token].nil? 29 | # TODO: Make this strict 30 | msg = "Invalid token id: #{token}" 31 | raise Stripe::InvalidRequestError.new(msg, 'tok', http_status: 404) 32 | else 33 | @card_tokens.delete(token) 34 | end 35 | end 36 | 37 | def get_card_or_bank_by_token(token) 38 | token_id = token['id'] || token 39 | @card_tokens[token_id] || @bank_tokens[token_id] || raise(Stripe::InvalidRequestError.new("Invalid token id: #{token_id}", 'tok', http_status: 404)) 40 | end 41 | 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/setup_intent.succeeded.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320093, 6 | "data": { 7 | "object": { 8 | "id": "seti_000000000000000000000000", 9 | "object": "setup_intent", 10 | "application": null, 11 | "cancellation_reason": null, 12 | "client_secret": "seti_000000000000000000000000_secret_0000000000000000000000000000000", 13 | "created": 1648320092, 14 | "customer": null, 15 | "description": "(created by Stripe CLI)", 16 | "last_setup_error": null, 17 | "latest_attempt": "setatt_000000000000000000000000", 18 | "livemode": false, 19 | "mandate": null, 20 | "metadata": { 21 | }, 22 | "next_action": null, 23 | "on_behalf_of": null, 24 | "payment_method": "pm_000000000000000000000000", 25 | "payment_method_options": { 26 | "card": { 27 | "mandate_options": null, 28 | "request_three_d_secure": "automatic" 29 | } 30 | }, 31 | "payment_method_types": [ 32 | "card" 33 | ], 34 | "single_use_mandate": null, 35 | "status": "succeeded", 36 | "usage": "off_session" 37 | } 38 | }, 39 | "livemode": false, 40 | "pending_webhooks": 2, 41 | "request": { 42 | "id": "req_00000000000000", 43 | "idempotency_key": "229fd8b7-d019-4b8b-91a9-920460d48a61" 44 | }, 45 | "type": "setup_intent.succeeded" 46 | } -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/plans.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Plans 4 | 5 | def Plans.included(klass) 6 | klass.add_handler 'post /v1/plans', :new_plan 7 | klass.add_handler 'post /v1/plans/(.*)', :update_plan 8 | klass.add_handler 'get /v1/plans/(.*)', :get_plan 9 | klass.add_handler 'delete /v1/plans/(.*)', :delete_plan 10 | klass.add_handler 'get /v1/plans', :list_plans 11 | end 12 | 13 | def new_plan(route, method_url, params, headers) 14 | params[:id] ||= new_id('plan') 15 | validate_create_plan_params(params) 16 | plans[ params[:id] ] = Data.mock_plan(params) 17 | end 18 | 19 | def update_plan(route, method_url, params, headers) 20 | route =~ method_url 21 | assert_existence :plan, $1, plans[$1] 22 | plans[$1].merge!(params) 23 | end 24 | 25 | def get_plan(route, method_url, params, headers) 26 | route =~ method_url 27 | assert_existence :plan, $1, plans[$1] 28 | end 29 | 30 | def delete_plan(route, method_url, params, headers) 31 | route =~ method_url 32 | assert_existence :plan, $1, plans.delete($1) 33 | end 34 | 35 | def list_plans(route, method_url, params, headers) 36 | limit = params[:limit] ? params[:limit] : 10 37 | Data.mock_list_object(plans.values.first(limit), params.merge!(limit: limit)) 38 | end 39 | 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/tax_rate_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'TaxRate API' do 4 | context 'with created tax rate' do 5 | let!(:rate) { Stripe::TaxRate.create } 6 | 7 | it 'returns list of tax rates' do 8 | rates = Stripe::TaxRate.list 9 | expect(rates.count).to eq(1) 10 | end 11 | 12 | it 'retrieves tax rate' do 13 | ret_rate = Stripe::TaxRate.retrieve(rate.id) 14 | expect(ret_rate.id).not_to be_nil 15 | expect(ret_rate.object).to eq('tax_rate') 16 | expect(ret_rate.display_name).to eq('VAT') 17 | expect(ret_rate.percentage).to eq(21.0) 18 | expect(ret_rate.jurisdiction).to eq('EU') 19 | expect(ret_rate.inclusive).to eq(false) 20 | end 21 | 22 | it 'updates tax rate' do 23 | ret_rate = Stripe::TaxRate.update(rate.id, percentage: 30.5) 24 | expect(ret_rate.id).not_to be_nil 25 | expect(ret_rate.object).to eq('tax_rate') 26 | expect(ret_rate.display_name).to eq('VAT') 27 | expect(ret_rate.percentage).to eq(30.5) 28 | expect(ret_rate.jurisdiction).to eq('EU') 29 | expect(ret_rate.inclusive).to eq(false) 30 | end 31 | end 32 | 33 | it 'creates tax rate' do 34 | rate = Stripe::TaxRate.create 35 | expect(rate.id).not_to be_nil 36 | expect(rate.object).to eq('tax_rate') 37 | expect(rate.display_name).to eq('VAT') 38 | expect(rate.percentage).to eq(21.0) 39 | expect(rate.jurisdiction).to eq('EU') 40 | expect(rate.inclusive).to eq(false) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/payment_link.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320038, 6 | "data": { 7 | "object": { 8 | "id": "plink_000000000000000000000000", 9 | "object": "payment_link", 10 | "active": true, 11 | "after_completion": { 12 | "hosted_confirmation": { 13 | "custom_message": null 14 | }, 15 | "type": "hosted_confirmation" 16 | }, 17 | "allow_promotion_codes": true, 18 | "application_fee_amount": null, 19 | "application_fee_percent": null, 20 | "automatic_tax": { 21 | "enabled": false 22 | }, 23 | "billing_address_collection": "auto", 24 | "livemode": false, 25 | "metadata": { 26 | }, 27 | "on_behalf_of": null, 28 | "payment_method_types": null, 29 | "phone_number_collection": { 30 | "enabled": false 31 | }, 32 | "shipping_address_collection": null, 33 | "subscription_data": { 34 | "trial_period_days": null 35 | }, 36 | "transfer_data": null, 37 | "url": "https://buy.stripe.com/test_000000000000000000" 38 | }, 39 | "previous_attributes": { 40 | "allow_promotion_codes": false 41 | } 42 | }, 43 | "livemode": false, 44 | "pending_webhooks": 2, 45 | "request": { 46 | "id": "req_00000000000000", 47 | "idempotency_key": "81dafec6-4d31-4bc8-b45b-0ab21772c793" 48 | }, 49 | "type": "payment_link.updated" 50 | } -------------------------------------------------------------------------------- /spec/integration_examples/completing_checkout_sessions_example.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples "Completing Checkout Sessions" do 4 | let(:test_helper) { StripeMock.create_test_helper } 5 | before { StripeMock.start } 6 | after { StripeMock.stop } 7 | 8 | it "can complete payment checkout sessions" do 9 | session = test_helper.create_checkout_session(mode: "payment") 10 | payment_method = Stripe::PaymentMethod.create(type: "card") 11 | 12 | payment_intent = test_helper.complete_checkout_session(session, payment_method) 13 | 14 | expect(payment_intent.id).to eq(session.payment_intent) 15 | expect(payment_intent.payment_method).to eq(payment_method.id) 16 | expect(payment_intent.status).to eq("succeeded") 17 | end 18 | 19 | it "can complete setup checkout sessions" do 20 | session = test_helper.create_checkout_session(mode: "setup") 21 | payment_method = Stripe::PaymentMethod.create(type: "card") 22 | 23 | setup_intent = test_helper.complete_checkout_session(session, payment_method) 24 | 25 | expect(setup_intent.id).to eq(session.setup_intent) 26 | expect(setup_intent.payment_method).to eq(payment_method.id) 27 | end 28 | 29 | it "can complete subscription checkout sessions" do 30 | session = test_helper.create_checkout_session(mode: "subscription") 31 | payment_method = Stripe::PaymentMethod.create(type: "card") 32 | 33 | subscription = test_helper.complete_checkout_session(session, payment_method) 34 | 35 | expect(subscription.default_payment_method).to eq(payment_method.id) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/setup_intent.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648321932, 6 | "data": { 7 | "object": { 8 | "id": "seti_000000000000000000000000", 9 | "object": "setup_intent", 10 | "application": null, 11 | "cancellation_reason": null, 12 | "client_secret": "seti_000000000000000000000000_secret_0000000000000000000000000000000", 13 | "created": 1648321931, 14 | "customer": null, 15 | "description": null, 16 | "last_setup_error": null, 17 | "latest_attempt": null, 18 | "livemode": false, 19 | "mandate": null, 20 | "metadata": { 21 | }, 22 | "next_action": null, 23 | "on_behalf_of": null, 24 | "payment_method": "pm_000000000000000000000000", 25 | "payment_method_options": { 26 | "acss_debit": { 27 | "currency": "cad", 28 | "mandate_options": { 29 | "interval_description": "First day of every month", 30 | "payment_schedule": "interval", 31 | "transaction_type": "personal" 32 | }, 33 | "verification_method": "automatic" 34 | } 35 | }, 36 | "payment_method_types": [ 37 | "acss_debit" 38 | ], 39 | "single_use_mandate": null, 40 | "status": "requires_confirmation", 41 | "usage": "off_session" 42 | } 43 | }, 44 | "livemode": false, 45 | "pending_webhooks": 1, 46 | "request": { 47 | "id": null, 48 | "idempotency_key": null 49 | }, 50 | "type": "setup_intent.created" 51 | } -------------------------------------------------------------------------------- /lib/stripe_mock/test_strategies/live.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module TestStrategies 3 | class Live < Base 4 | 5 | def create_product(params={}) 6 | params = create_product_params(params) 7 | raise "create_product requires an :id" if params[:id].nil? 8 | delete_product(params[:id]) 9 | Stripe::Product.create params 10 | end 11 | 12 | def delete_product(product_id) 13 | product = Stripe::Product.retrieve(product_id) 14 | Stripe::Plan.list(product: product_id).each(&:delete) if product.type == 'service' 15 | product.delete 16 | rescue Stripe::StripeError => e 17 | # do nothing 18 | end 19 | 20 | def create_plan(params={}) 21 | raise "create_plan requires an :id" if params[:id].nil? 22 | delete_plan(params[:id]) 23 | Stripe::Plan.create create_plan_params(params) 24 | end 25 | 26 | def delete_plan(plan_id) 27 | plan = Stripe::Plan.retrieve(plan_id) 28 | plan.delete 29 | rescue Stripe::StripeError => e 30 | # do nothing 31 | end 32 | 33 | def create_coupon(params={}) 34 | delete_coupon create_coupon_params(params)[:id] 35 | super 36 | end 37 | 38 | def delete_coupon(id) 39 | coupon = Stripe::Coupon.retrieve(id) 40 | coupon.delete 41 | rescue Stripe::StripeError 42 | # do nothing 43 | end 44 | 45 | def upsert_stripe_object(object, attributes) 46 | raise UnsupportedRequestError.new "Updating or inserting Stripe objects in Live mode not supported" 47 | end 48 | 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/balance_transactions.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module BalanceTransactions 4 | 5 | def BalanceTransactions.included(klass) 6 | klass.add_handler 'get /v1/balance_transactions/(.*)', :get_balance_transaction 7 | klass.add_handler 'get /v1/balance_transactions', :list_balance_transactions 8 | end 9 | 10 | def get_balance_transaction(route, method_url, params, headers) 11 | route =~ method_url 12 | assert_existence :balance_transaction, $1, hide_additional_attributes(balance_transactions[$1]) 13 | end 14 | 15 | def list_balance_transactions(route, method_url, params, headers) 16 | values = balance_transactions.values 17 | if params.has_key?(:transfer) 18 | # If transfer supplied as params, need to filter the btxns returned to only include those with the specified transfer id 19 | values = values.select{|btxn| btxn[:transfer] == params[:transfer]} 20 | end 21 | Data.mock_list_object(values.map{|btxn| hide_additional_attributes(btxn)}, params) 22 | end 23 | 24 | private 25 | 26 | def hide_additional_attributes(btxn) 27 | # For automatic Stripe transfers, the transfer attribute on balance_transaction stores the transfer which 28 | # included this balance_transaction. However, it is not exposed as a field returned on a balance_transaction. 29 | # Therefore, need to not show this attribute if it exists. 30 | if !btxn.nil? 31 | btxn.reject{|k,v| k == :transfer } 32 | end 33 | end 34 | 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/checkout.session.completed.payment_mode.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "checkout.session.completed", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "cs_00000000000000", 10 | "object": "checkout.session", 11 | "allow_promotion_codes": null, 12 | "amount_subtotal": 25000, 13 | "amount_total": 25000, 14 | "automatic_tax": { 15 | "enabled": false, 16 | "status": null 17 | }, 18 | "billing_address_collection": null, 19 | "cancel_url": "https://example.com/cancel", 20 | "client_reference_id": null, 21 | "currency": "usd", 22 | "customer": "cus_00000000000000", 23 | "customer_details": { 24 | "email": "example@example.com", 25 | "tax_exempt": "none", 26 | "tax_ids": [] 27 | }, 28 | "customer_email": null, 29 | "livemode": false, 30 | "locale": null, 31 | "metadata": {}, 32 | "mode": "payment", 33 | "payment_intent": "pi_00000000000000", 34 | "payment_method_options": {}, 35 | "payment_method_types": [ 36 | "card" 37 | ], 38 | "payment_status": "paid", 39 | "setup_intent": null, 40 | "shipping": null, 41 | "shipping_address_collection": null, 42 | "submit_type": null, 43 | "subscription": null, 44 | "success_url": "https://example.com/success", 45 | "total_details": { 46 | "amount_discount": 0, 47 | "amount_shipping": 0, 48 | "amount_tax": 0 49 | }, 50 | "url": null 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/invoice_items.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module InvoiceItems 4 | 5 | def InvoiceItems.included(klass) 6 | klass.add_handler 'post /v1/invoiceitems', :new_invoice_item 7 | klass.add_handler 'post /v1/invoiceitems/(.*)', :update_invoice_item 8 | klass.add_handler 'get /v1/invoiceitems/(.*)', :get_invoice_item 9 | klass.add_handler 'get /v1/invoiceitems', :list_invoice_items 10 | klass.add_handler 'delete /v1/invoiceitems/(.*)', :delete_invoice_item 11 | end 12 | 13 | def new_invoice_item(route, method_url, params, headers) 14 | params[:id] ||= new_id('ii') 15 | invoice_items[params[:id]] = Data.mock_invoice_item(params) 16 | end 17 | 18 | def update_invoice_item(route, method_url, params, headers) 19 | route =~ method_url 20 | list_item = assert_existence :list_item, $1, invoice_items[$1] 21 | list_item.merge!(params) 22 | end 23 | 24 | def delete_invoice_item(route, method_url, params, headers) 25 | route =~ method_url 26 | assert_existence :list_item, $1, invoice_items[$1] 27 | 28 | invoice_items[$1] = { 29 | id: invoice_items[$1][:id], 30 | deleted: true 31 | } 32 | end 33 | 34 | def list_invoice_items(route, method_url, params, headers) 35 | Data.mock_list_object(invoice_items.values, params) 36 | end 37 | 38 | def get_invoice_item(route, method_url, params, headers) 39 | route =~ method_url 40 | assert_existence :invoice_item, $1, invoice_items[$1] 41 | end 42 | 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/charge.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.updated", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "ch_00000000000000", 10 | "object": "charge", 11 | "created": 1380933505, 12 | "livemode": false, 13 | "paid": true, 14 | "amount": 1000, 15 | "currency": "usd", 16 | "refunded": false, 17 | "source": { 18 | "id": "cc_00000000000000", 19 | "object": "card", 20 | "last4": "4242", 21 | "type": "Visa", 22 | "brand": "Visa", 23 | "exp_month": 12, 24 | "exp_year": 2013, 25 | "fingerprint": "wXWJT135mEK107G8", 26 | "customer": "cus_00000000000000", 27 | "country": "US", 28 | "name": "Actual Nothing", 29 | "address_line1": null, 30 | "address_line2": null, 31 | "address_city": null, 32 | "address_state": null, 33 | "address_zip": null, 34 | "address_country": null, 35 | "cvc_check": null, 36 | "address_line1_check": null, 37 | "address_zip_check": null 38 | }, 39 | "captured": true, 40 | "refunds": { 41 | 42 | }, 43 | "balance_transaction": "txn_00000000000000", 44 | "failure_message": null, 45 | "failure_code": null, 46 | "amount_refunded": 0, 47 | "customer": "cus_00000000000000", 48 | "invoice": "in_00000000000000", 49 | "description": "Sample description" , 50 | "dispute": null, 51 | "metadata": { 52 | } 53 | }, 54 | "previous_attributes": { 55 | "description": null 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/events.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Events 4 | 5 | def Events.included(klass) 6 | klass.add_handler 'get /v1/events/(.*)', :retrieve_event 7 | klass.add_handler 'get /v1/events', :list_events 8 | end 9 | 10 | def retrieve_event(route, method_url, params, headers) 11 | route =~ method_url 12 | assert_existence :event, $1, events[$1] 13 | end 14 | 15 | def list_events(route, method_url, params, headers) 16 | values = filter_by_created(events.values, params: params) 17 | Data.mock_list_object(values, params) 18 | end 19 | 20 | private 21 | 22 | def filter_by_created(event_list, params:) 23 | if params[:created].nil? 24 | return event_list 25 | end 26 | 27 | if params[:created].is_a?(Hash) 28 | if params[:created][:gt] 29 | event_list = event_list.select { |event| event[:created] > params[:created][:gt].to_i } 30 | end 31 | if params[:created][:gte] 32 | event_list = event_list.select { |event| event[:created] >= params[:created][:gte].to_i } 33 | end 34 | if params[:created][:lt] 35 | event_list = event_list.select { |event| event[:created] < params[:created][:lt].to_i } 36 | end 37 | if params[:created][:lte] 38 | event_list = event_list.select { |event| event[:created] <= params[:created][:lte].to_i } 39 | end 40 | else 41 | event_list = event_list.select { |event| event[:created] == params[:created].to_i } 42 | end 43 | event_list 44 | end 45 | 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/products.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Products 4 | def self.included(base) 5 | base.add_handler 'post /v1/products', :create_product 6 | base.add_handler 'get /v1/products/(.*)', :retrieve_product 7 | base.add_handler 'post /v1/products/(.*)', :update_product 8 | base.add_handler 'get /v1/products', :list_products 9 | base.add_handler 'delete /v1/products/(.*)', :destroy_product 10 | end 11 | 12 | def create_product(_route, _method_url, params, _headers) 13 | params[:id] ||= new_id('prod') 14 | validate_create_product_params(params) 15 | products[params[:id]] = Data.mock_product(params) 16 | end 17 | 18 | def retrieve_product(route, method_url, _params, _headers) 19 | id = method_url.match(route).captures.first 20 | assert_existence :product, id, products[id] 21 | end 22 | 23 | def update_product(route, method_url, params, _headers) 24 | id = method_url.match(route).captures.first 25 | product = assert_existence :product, id, products[id] 26 | 27 | product.merge!(params) 28 | end 29 | 30 | def list_products(_route, _method_url, params, _headers) 31 | limit = params[:limit] || 10 32 | Data.mock_list_object(products.values.take(limit), params) 33 | end 34 | 35 | def destroy_product(route, method_url, _params, _headers) 36 | id = method_url.match(route).captures.first 37 | assert_existence :product, id, products[id] 38 | 39 | products.delete(id) 40 | { id: id, object: 'product', deleted: true } 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/helpers/external_account_helpers.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Helpers 4 | 5 | def add_external_account_to(type, type_id, params, objects) 6 | resource = assert_existence type, type_id, objects[type_id] 7 | 8 | source = 9 | if params[:card] 10 | card_from_params(params[:card]) 11 | elsif params[:bank_account] 12 | bank_from_params(params[:bank_account]) 13 | else 14 | begin 15 | get_card_by_token(params[:external_account]) 16 | rescue Stripe::InvalidRequestError 17 | bank_from_params(params[:external_account]) 18 | end 19 | end 20 | add_external_account_to_object(type, source, resource) 21 | end 22 | 23 | def add_external_account_to_object(type, source, object, replace_current=false) 24 | source[type] = object[:id] 25 | accounts = object[:external_accounts] 26 | 27 | if replace_current && accounts[:data] 28 | accounts[:data].delete_if {|source| source[:id] == object[:default_source]} 29 | object[:default_source] = source[:id] 30 | accounts[:data] = [source] 31 | else 32 | accounts[:total_count] = (accounts[:total_count] || 0) + 1 33 | (accounts[:data] ||= []) << source 34 | end 35 | object[:default_source] = source[:id] if object[:default_source].nil? 36 | 37 | source 38 | end 39 | 40 | def bank_from_params(attrs_or_token) 41 | if attrs_or_token.is_a? Hash 42 | attrs_or_token = generate_bank_token(attrs_or_token) 43 | end 44 | get_bank_by_token(attrs_or_token) 45 | end 46 | 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/prices.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Prices 4 | 5 | def Prices.included(klass) 6 | klass.add_handler 'post /v1/prices', :new_price 7 | klass.add_handler 'post /v1/prices/(.*)', :update_price 8 | klass.add_handler 'get /v1/prices/(.*)', :get_price 9 | klass.add_handler 'get /v1/prices', :list_prices 10 | end 11 | 12 | def new_price(route, method_url, params, headers) 13 | params[:id] ||= new_id('price') 14 | 15 | if params[:product_data] 16 | params[:product] = create_product(nil, nil, params[:product_data], nil)[:id] unless params[:product] 17 | params.delete(:product_data) 18 | end 19 | 20 | validate_create_price_params(params) 21 | prices[ params[:id] ] = Data.mock_price(params) 22 | end 23 | 24 | def update_price(route, method_url, params, headers) 25 | route =~ method_url 26 | assert_existence :price, $1, prices[$1] 27 | prices[$1].merge!(params) 28 | end 29 | 30 | def get_price(route, method_url, params, headers) 31 | route =~ method_url 32 | assert_existence :price, $1, prices[$1] 33 | end 34 | 35 | def list_prices(route, method_url, params, headers) 36 | limit = params[:limit] ? params[:limit] : 10 37 | price_data = prices.values 38 | validate_list_prices_params(params) 39 | 40 | if params.key?(:lookup_keys) 41 | price_data.select! do |price| 42 | params[:lookup_keys].include?(price[:lookup_key]) 43 | end 44 | end 45 | 46 | Data.mock_list_object(price_data.first(limit), params.merge!(limit: limit)) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/payment_method.attached.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320107, 6 | "data": { 7 | "object": { 8 | "id": "card_000000000000000000000000", 9 | "object": "payment_method", 10 | "billing_details": { 11 | "address": { 12 | "city": null, 13 | "country": null, 14 | "line1": null, 15 | "line2": null, 16 | "postal_code": null, 17 | "state": null 18 | }, 19 | "email": null, 20 | "name": null, 21 | "phone": null 22 | }, 23 | "card": { 24 | "brand": "visa", 25 | "checks": { 26 | "address_line1_check": null, 27 | "address_postal_code_check": null, 28 | "cvc_check": null 29 | }, 30 | "country": "US", 31 | "exp_month": 3, 32 | "exp_year": 2023, 33 | "fingerprint": "ZoVSX2dK5igWt2SB", 34 | "funding": "credit", 35 | "generated_from": null, 36 | "last4": "4242", 37 | "networks": { 38 | "available": [ 39 | "visa" 40 | ], 41 | "preferred": null 42 | }, 43 | "three_d_secure_usage": { 44 | "supported": true 45 | }, 46 | "wallet": null 47 | }, 48 | "created": 1648320107, 49 | "customer": "cus_00000000000000", 50 | "livemode": false, 51 | "metadata": { 52 | }, 53 | "type": "card" 54 | } 55 | }, 56 | "livemode": false, 57 | "pending_webhooks": 2, 58 | "request": { 59 | "id": "req_00000000000000", 60 | "idempotency_key": "381c7773-97ac-4c18-8fbe-e8bff5e6bbad" 61 | }, 62 | "type": "payment_method.attached" 63 | } -------------------------------------------------------------------------------- /spec/integration_examples/charge_token_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Charging with Tokens' do 4 | 5 | describe "With OAuth", :oauth => true do 6 | 7 | let(:cus) do 8 | Stripe::Customer.create( 9 | :source => stripe_helper.generate_card_token({ :number => '4242424242424242', :brand => 'Visa' }) 10 | ) 11 | end 12 | 13 | let(:card_token) do 14 | Stripe::Token.create({ 15 | :customer => cus.id, 16 | :source => cus.sources.first.id 17 | }, ENV['STRIPE_TEST_OAUTH_ACCESS_TOKEN']) 18 | end 19 | 20 | it "creates with an oauth access token" do 21 | charge = Stripe::Charge.create({ 22 | :amount => 1099, 23 | :currency => 'usd', 24 | :source => card_token.id 25 | }, ENV['STRIPE_TEST_OAUTH_ACCESS_TOKEN']) 26 | 27 | expect(charge.source.id).to_not eq cus.sources.first.id 28 | expect(charge.source.fingerprint).to eq cus.sources.first.fingerprint 29 | expect(charge.source.last4).to eq '4242' 30 | expect(charge.source.brand).to eq 'Visa' 31 | 32 | retrieved_charge = Stripe::Charge.retrieve(charge.id) 33 | 34 | expect(retrieved_charge.source.id).to_not eq cus.sources.first.id 35 | expect(retrieved_charge.source.fingerprint).to eq cus.sources.first.fingerprint 36 | expect(retrieved_charge.source.last4).to eq '4242' 37 | expect(retrieved_charge.source.brand).to eq 'Visa' 38 | end 39 | 40 | it "throws an error when the card is not an id" do 41 | expect { 42 | charge = Stripe::Charge.create({ 43 | :amount => 1099, 44 | :currency => 'usd', 45 | :source => card_token 46 | }, ENV['STRIPE_TEST_OAUTH_ACCESS_TOKEN']) 47 | }.to raise_error(Stripe::InvalidRequestError, /Invalid token id/) 48 | end 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /spec/integration_examples/customer_card_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples "Multiple Customer Cards" do 4 | it "handles multiple cards", :live => true do 5 | tok1 = Stripe::Token.retrieve stripe_helper.generate_card_token :number => "4242424242424242" 6 | tok2 = Stripe::Token.retrieve stripe_helper.generate_card_token :number => "4012888888881881" 7 | 8 | cus = Stripe::Customer.create(:email => 'alice@bob.com', :source => tok1.id) 9 | default_card = cus.sources.first 10 | cus.sources.create(:source => tok2.id) 11 | 12 | cus = Stripe::Customer.retrieve(cus.id) 13 | expect(cus.sources.count).to eq(2) 14 | expect(cus.default_source).to eq default_card.id 15 | end 16 | 17 | it "gives the same two card numbers the same fingerprints", :live => true do 18 | tok1 = Stripe::Token.retrieve stripe_helper.generate_card_token :number => "4242424242424242" 19 | tok2 = Stripe::Token.retrieve stripe_helper.generate_card_token :number => "4242424242424242" 20 | 21 | cus = Stripe::Customer.create(:email => 'alice@bob.com', :source => tok1.id) 22 | 23 | cus = Stripe::Customer.retrieve(cus.id) 24 | card = cus.sources.find do |existing_card| 25 | existing_card.fingerprint == tok2.card.fingerprint 26 | end 27 | expect(card).to_not be_nil 28 | end 29 | 30 | it "gives different card numbers different fingerprints", :live => true do 31 | tok1 = Stripe::Token.retrieve stripe_helper.generate_card_token :number => "4242424242424242" 32 | tok2 = Stripe::Token.retrieve stripe_helper.generate_card_token :number => "4012888888881881" 33 | 34 | cus = Stripe::Customer.create(:email => 'alice@bob.com', :source => tok1.id) 35 | 36 | cus = Stripe::Customer.retrieve(cus.id) 37 | source = cus.sources.find do |existing_card| 38 | existing_card.fingerprint == tok2.card.fingerprint 39 | end 40 | expect(source).to be_nil 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | gem 'rspec', '~> 3.1' 4 | require 'rspec' 5 | require 'stripe' 6 | require 'stripe_mock' 7 | require 'stripe_mock/server' 8 | require 'dotenv' 9 | 10 | Dotenv.load('.env') 11 | 12 | # Requires supporting ruby files with custom matchers and macros, etc, 13 | # in spec/support/ and its subdirectories. 14 | Dir["./spec/support/**/*.rb"].each {|f| require f} 15 | 16 | RSpec.configure do |c| 17 | tags = c.filter_manager.inclusions.rules 18 | 19 | if tags.include?(:live) || tags.include?(:oauth) 20 | puts "Running **live** tests against Stripe..." 21 | StripeMock.set_default_test_helper_strategy(:live) 22 | 23 | if tags.include?(:oauth) 24 | oauth_token = ENV['STRIPE_TEST_OAUTH_ACCESS_TOKEN'] 25 | if oauth_token.nil? || oauth_token == '' 26 | raise "Please set your STRIPE_TEST_OAUTH_ACCESS_TOKEN environment variable." 27 | end 28 | c.filter_run_excluding :mock_server => true, :live => true 29 | else 30 | c.filter_run_excluding :mock_server => true, :oauth => true 31 | end 32 | 33 | if ENV['IS_TRAVIS'] 34 | puts "Travis ruby version: #{RUBY_VERSION}" 35 | api_key = case RUBY_VERSION 36 | when '2.4.6' then ENV['STRIPE_TEST_SECRET_KEY_A'] 37 | when '2.5.5' then ENV['STRIPE_TEST_SECRET_KEY_B'] 38 | when '2.6.3' then ENV['STRIPE_TEST_SECRET_KEY_C'] 39 | when '2.7.0' then ENV['STRIPE_TEST_SECRET_KEY_D'] 40 | end 41 | else 42 | api_key = ENV['STRIPE_TEST_SECRET_KEY'] 43 | if api_key.nil? || api_key == '' 44 | raise "Please set your STRIPE_TEST_SECRET_KEY environment variable." 45 | end 46 | end 47 | 48 | c.before(:each) do 49 | allow(StripeMock).to receive(:start).and_return(nil) 50 | allow(StripeMock).to receive(:stop).and_return(nil) 51 | Stripe.api_key = api_key 52 | end 53 | c.after(:each) { sleep 0.01 } 54 | else 55 | c.filter_run_excluding :oauth => true 56 | Stripe.api_key ||= '' 57 | end 58 | 59 | c.filter_run focus: true 60 | c.run_all_when_everything_filtered = true 61 | end 62 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/bank_token_examples.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | shared_examples "Bank Account Token Mocking" do 4 | it "generates a bank token with default values" do 5 | bank_token = StripeMock.generate_bank_token 6 | tokens = test_data_source(:bank_tokens) 7 | expect(tokens[bank_token]).to_not be_nil 8 | expect(tokens[bank_token][:bank_name]).to eq("STRIPEMOCK TEST BANK") 9 | expect(tokens[bank_token][:last4]).to eq("6789") 10 | end 11 | 12 | it "generates a bank token with an associated account in memory" do 13 | bank_token = StripeMock.generate_bank_token( 14 | bank_name: "Memory Bank", 15 | last4: "7171" 16 | ) 17 | tokens = test_data_source(:bank_tokens) 18 | expect(tokens[bank_token]).to_not be_nil 19 | expect(tokens[bank_token][:bank_name]).to eq("Memory Bank") 20 | expect(tokens[bank_token][:last4]).to eq("7171") 21 | end 22 | 23 | it "creates a token whose id begins with test_btok" do 24 | bank_token = StripeMock.generate_bank_token({ 25 | last4: "1212" 26 | }) 27 | expect(bank_token).to match(/^test_btok/) 28 | end 29 | 30 | it "assigns the generated bank account to a new recipient", skip: "Stripe has deprecated Recipients" do 31 | bank_token = StripeMock.generate_bank_token( 32 | bank_name: "Bank Token Mocking", 33 | last4: "7777" 34 | ) 35 | 36 | recipient = Stripe::Recipient.create({ 37 | name: "Fred Flinstone", 38 | type: "individual", 39 | email: "blah@domain.co", 40 | bank_account: bank_token 41 | }) 42 | expect(recipient.active_account.last4).to eq("7777") 43 | expect(recipient.active_account.bank_name).to eq("Bank Token Mocking") 44 | end 45 | 46 | it "retrieves a created token" do 47 | bank_token = StripeMock.generate_bank_token( 48 | bank_name: "Cha-ching Banking", 49 | last4: "3939" 50 | ) 51 | token = Stripe::Token.retrieve(bank_token) 52 | 53 | expect(token.id).to eq(bank_token) 54 | expect(token.bank_account.last4).to eq("3939") 55 | expect(token.bank_account.bank_name).to eq("Cha-ching Banking") 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.subscription.trial_will_end.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.subscription.trial_will_end", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "su_00000000000000", 10 | "items": { 11 | "object": "list", 12 | "data": [{ 13 | "id": "si_00000000000000", 14 | "object": "subscription_item", 15 | "created": 1497881783, 16 | "plan": { 17 | "interval": "month", 18 | "product": "pr_00000000000000", 19 | "amount": 100, 20 | "currency": "usd", 21 | "id": "fkx0AFo_00000000000000", 22 | "object": "plan", 23 | "livemode": false, 24 | "interval_count": 1, 25 | "trial_period_days": null, 26 | "metadata": {} 27 | }, 28 | "quantity": 1 29 | }, 30 | { 31 | "id": "si_00000000000001", 32 | "object": "subscription_item", 33 | "created": 1497881788, 34 | "plan": { 35 | "interval": "month", 36 | "product": "pr_00000000000001", 37 | "amount": 200, 38 | "currency": "eur", 39 | "id": "fkx0AFo_00000000000001", 40 | "object": "plan", 41 | "livemode": false, 42 | "interval_count": 1, 43 | "trial_period_days": null, 44 | "metadata": {} 45 | }, 46 | "quantity": 5 47 | } 48 | ] 49 | }, 50 | "object": "subscription", 51 | "start": 1381080623, 52 | "status": "trialing", 53 | "customer": "cus_00000000000000", 54 | "cancel_at_period_end": false, 55 | "current_period_start": 1381080623, 56 | "current_period_end": 1383759023, 57 | "ended_at": null, 58 | "trial_start": 1381021530, 59 | "trial_end": 1381280730, 60 | "canceled_at": null, 61 | "quantity": 1, 62 | "application_fee_percent": null 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/invoice.upcoming.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "invoice.upcoming", 6 | "status": "draft", 7 | "object": "event", 8 | "data": { 9 | "object": { 10 | "created": 1380674206, 11 | "period_start": 1378082075, 12 | "period_end": 1380674075, 13 | "lines": { 14 | "count": 1, 15 | "object": "list", 16 | "url": "/v1/invoices/upcoming/lines?customer=cus_00000000000000", 17 | "data": [ 18 | { 19 | "id": "su_2hksGtIPylSBg2", 20 | "object": "line_item", 21 | "type": "subscription", 22 | "livemode": true, 23 | "amount": 100, 24 | "currency": "usd", 25 | "proration": false, 26 | "period": { 27 | "start": 1383759042, 28 | "end": 1386351042 29 | }, 30 | "quantity": 1, 31 | "plan": { 32 | "interval": "month", 33 | "amount": 100, 34 | "currency": "usd", 35 | "id": "fkx0AFo", 36 | "object": "plan", 37 | "livemode": false, 38 | "interval_count": 1, 39 | "trial_period_days": null, 40 | "product": "pr_00000000000000", 41 | "metadata": {} 42 | }, 43 | "description": null, 44 | "metadata": null 45 | } 46 | ] 47 | }, 48 | "subtotal": 1000, 49 | "total": 1000, 50 | "customer": "cus_00000000000000", 51 | "object": "invoice", 52 | "attempted": false, 53 | "closed": false, 54 | "paid": false, 55 | "livemode": false, 56 | "attempt_count": 0, 57 | "amount_due": 1000, 58 | "currency": "usd", 59 | "starting_balance": 0, 60 | "ending_balance": 0, 61 | "next_payment_attempt": null, 62 | "charge": null, 63 | "discount": null, 64 | "application_fee": null, 65 | "subscription": "sub_00000000000000", 66 | "metadata": {}, 67 | "description": null 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/recipients.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Recipients 4 | 5 | def Recipients.included(klass) 6 | klass.add_handler 'post /v1/recipients', :new_recipient 7 | klass.add_handler 'post /v1/recipients/(.*)', :update_recipient 8 | klass.add_handler 'get /v1/recipients/(.*)', :get_recipient 9 | end 10 | 11 | def new_recipient(route, method_url, params, headers) 12 | params[:id] ||= new_id('rp') 13 | cards = [] 14 | 15 | if params[:name].nil? 16 | raise StripeMock::StripeMockError.new("Missing required parameter name for recipients.") 17 | end 18 | 19 | if params[:type].nil? 20 | raise StripeMock::StripeMockError.new("Missing required parameter type for recipients.") 21 | end 22 | 23 | unless %w(individual corporation).include?(params[:type]) 24 | raise StripeMock::StripeMockError.new("Type must be either individual or corporation..") 25 | end 26 | 27 | if params[:bank_account] 28 | params[:active_account] = get_bank_by_token(params.delete(:bank_account)) 29 | end 30 | 31 | if params[:card] 32 | cards << get_card_by_token(params.delete(:card)) 33 | params[:default_card] = cards.first[:id] 34 | end 35 | 36 | recipients[ params[:id] ] = Data.mock_recipient(cards, params) 37 | recipients[ params[:id] ] 38 | end 39 | 40 | def update_recipient(route, method_url, params, headers) 41 | route =~ method_url 42 | recipient = assert_existence :recipient, $1, recipients[$1] 43 | recipient.merge!(params) 44 | 45 | if params[:card] 46 | new_card = get_card_by_token(params.delete(:card)) 47 | add_card_to_object(:recipient, new_card, recipient, true) 48 | recipient[:default_card] = new_card[:id] 49 | end 50 | 51 | recipient 52 | end 53 | 54 | def get_recipient(route, method_url, params, headers) 55 | route =~ method_url 56 | assert_existence :recipient, $1, recipients[$1] 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/support/stripe_examples.rb: -------------------------------------------------------------------------------- 1 | def require_stripe_examples 2 | Dir["./spec/shared_stripe_examples/**/*.rb"].each {|f| require f} 3 | Dir["./spec/integration_examples/**/*.rb"].each {|f| require f} 4 | end 5 | 6 | def it_behaves_like_stripe(&block) 7 | it_behaves_like 'Account API', &block 8 | it_behaves_like 'Account Link API', &block 9 | it_behaves_like 'Balance API', &block 10 | it_behaves_like 'Balance Transaction API', &block 11 | it_behaves_like 'Bank Account Token Mocking', &block 12 | it_behaves_like 'Card Token Mocking', &block 13 | it_behaves_like 'Card API', &block 14 | it_behaves_like 'Charge API', &block 15 | it_behaves_like 'Bank API', &block 16 | it_behaves_like 'Express Login Link API', &block 17 | it_behaves_like 'External Account API', &block 18 | it_behaves_like 'Coupon API', &block 19 | it_behaves_like 'Customer API', &block 20 | it_behaves_like 'Dispute API', &block 21 | it_behaves_like 'Extra Features', &block 22 | it_behaves_like 'Invoice API', &block 23 | it_behaves_like 'Invoice Item API', &block 24 | it_behaves_like 'Plan API', &block 25 | it_behaves_like 'Price API', &block 26 | it_behaves_like 'Product API', &block 27 | it_behaves_like 'Recipient API', &block 28 | it_behaves_like 'Refund API', &block 29 | it_behaves_like 'Transfer API', &block 30 | it_behaves_like 'Payout API', &block 31 | it_behaves_like 'PaymentIntent API', &block 32 | it_behaves_like 'PaymentMethod API', &block 33 | it_behaves_like 'SetupIntent API', &block 34 | it_behaves_like 'Stripe Error Mocking', &block 35 | it_behaves_like 'Customer Subscriptions with plans', &block 36 | it_behaves_like 'Customer Subscriptions with prices', &block 37 | it_behaves_like 'Subscription Items API', &block 38 | it_behaves_like 'Webhook Events API', &block 39 | it_behaves_like 'Country Spec API', &block 40 | it_behaves_like 'EphemeralKey API', &block 41 | it_behaves_like 'TaxRate API', &block 42 | it_behaves_like 'Checkout Session API', &block 43 | 44 | # Integration tests 45 | it_behaves_like 'Multiple Customer Cards' 46 | it_behaves_like 'Charging with Tokens' 47 | it_behaves_like 'Card Error Prep' 48 | it_behaves_like 'Completing Checkout Sessions' 49 | end 50 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/external_accounts.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module ExternalAccounts 4 | 5 | def ExternalAccounts.included(klass) 6 | klass.add_handler 'get /v1/accounts/(.*)/external_accounts', :retrieve_external_accounts 7 | klass.add_handler 'post /v1/accounts/(.*)/external_accounts', :create_external_account 8 | klass.add_handler 'post /v1/accounts/(.*)/external_accounts/(.*)/verify', :verify_external_account 9 | klass.add_handler 'get /v1/accounts/(.*)/external_accounts/(.*)', :retrieve_external_account 10 | klass.add_handler 'delete /v1/accounts/(.*)/external_accounts/(.*)', :delete_external_account 11 | klass.add_handler 'post /v1/accounts/(.*)/external_accounts/(.*)', :update_external_account 12 | end 13 | 14 | def create_external_account(route, method_url, params, headers) 15 | route =~ method_url 16 | add_external_account_to(:account, $1, params, accounts) 17 | end 18 | 19 | def retrieve_external_accounts(route, method_url, params, headers) 20 | route =~ method_url 21 | retrieve_object_cards(:account, $1, accounts) 22 | end 23 | 24 | def retrieve_external_account(route, method_url, params, headers) 25 | route =~ method_url 26 | account = assert_existence :account, $1, accounts[$1] 27 | 28 | assert_existence :card, $2, get_card(account, $2) 29 | end 30 | 31 | def delete_external_account(route, method_url, params, headers) 32 | route =~ method_url 33 | delete_card_from(:account, $1, $2, accounts) 34 | end 35 | 36 | def update_external_account(route, method_url, params, headers) 37 | route =~ method_url 38 | account = assert_existence :account, $1, accounts[$1] 39 | 40 | card = assert_existence :card, $2, get_card(account, $2) 41 | card.merge!(params) 42 | card 43 | end 44 | 45 | def verify_external_account(route, method_url, params, headers) 46 | route =~ method_url 47 | account = assert_existence :account, $1, accounts[$1] 48 | 49 | external_account = assert_existence :bank_account, $2, verify_bank_account(account, $2) 50 | external_account 51 | end 52 | 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/stripe_mock/server.rb: -------------------------------------------------------------------------------- 1 | require 'drb/drb' 2 | 3 | module StripeMock 4 | class Server 5 | def self.start_new(opts) 6 | puts "Starting StripeMock server on port #{opts[:port] || 4999}" 7 | 8 | host = opts.fetch :host,'0.0.0.0' 9 | port = opts.fetch :port, 4999 10 | 11 | DRb.start_service "druby://#{host}:#{port}", Server.new 12 | DRb.thread.join 13 | end 14 | 15 | def initialize 16 | self.clear_data 17 | end 18 | 19 | def mock_request(*args, **kwargs) 20 | begin 21 | @instance.mock_request(*args, **kwargs) 22 | rescue Stripe::InvalidRequestError => e 23 | { 24 | :error_raised => 'invalid_request', 25 | :error_params => [ 26 | e.message, e.param, { http_status: e.http_status, http_body: e.http_body, json_body: e.json_body} 27 | ] 28 | } 29 | end 30 | end 31 | 32 | def get_data(key) 33 | @instance.send(key) 34 | end 35 | 36 | def destroy_resource(type, id) 37 | @instance.send(type).delete(id) 38 | end 39 | 40 | def clear_data 41 | @instance = Instance.new 42 | end 43 | 44 | def set_debug(toggle) 45 | @instance.debug = toggle 46 | end 47 | 48 | def set_global_id_prefix(value) 49 | StripeMock.global_id_prefix = value 50 | end 51 | 52 | def global_id_prefix 53 | StripeMock.global_id_prefix 54 | end 55 | 56 | def generate_card_token(card_params) 57 | @instance.generate_card_token(card_params) 58 | end 59 | 60 | def generate_bank_token(recipient_params) 61 | @instance.generate_bank_token(recipient_params) 62 | end 63 | 64 | def generate_webhook_event(event_data) 65 | @instance.generate_webhook_event(event_data) 66 | end 67 | 68 | def set_conversion_rate(value) 69 | @instance.conversion_rate = value 70 | end 71 | 72 | def set_account_balance(value) 73 | @instance.account_balance = value 74 | end 75 | 76 | def error_queue 77 | @instance.error_queue 78 | end 79 | 80 | def debug? 81 | @instance.debug 82 | end 83 | 84 | def ping 85 | true 86 | end 87 | 88 | def upsert_stripe_object(object, attributes) 89 | @instance.upsert_stripe_object(object, attributes) 90 | end 91 | 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/charge.dispute.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648319940, 6 | "data": { 7 | "object": { 8 | "id": "dp_000000000000000000000000", 9 | "object": "dispute", 10 | "amount": 100, 11 | "balance_transaction": null, 12 | "balance_transactions": [ 13 | 14 | ], 15 | "charge": "ch_000000000000000000000000", 16 | "created": 1648319940, 17 | "currency": "usd", 18 | "evidence": { 19 | "access_activity_log": null, 20 | "billing_address": null, 21 | "cancellation_policy": null, 22 | "cancellation_policy_disclosure": null, 23 | "cancellation_rebuttal": null, 24 | "customer_communication": null, 25 | "customer_email_address": null, 26 | "customer_name": null, 27 | "customer_purchase_ip": null, 28 | "customer_signature": null, 29 | "duplicate_charge_documentation": null, 30 | "duplicate_charge_explanation": null, 31 | "duplicate_charge_id": null, 32 | "product_description": null, 33 | "receipt": null, 34 | "refund_policy": null, 35 | "refund_policy_disclosure": null, 36 | "refund_refusal_explanation": null, 37 | "service_date": null, 38 | "service_documentation": null, 39 | "shipping_address": null, 40 | "shipping_carrier": null, 41 | "shipping_date": null, 42 | "shipping_documentation": null, 43 | "shipping_tracking_number": null, 44 | "uncategorized_file": null, 45 | "uncategorized_text": null 46 | }, 47 | "evidence_details": { 48 | "due_by": 1649116799, 49 | "has_evidence": false, 50 | "past_due": false, 51 | "submission_count": 0 52 | }, 53 | "is_charge_refundable": true, 54 | "livemode": false, 55 | "metadata": { 56 | }, 57 | "payment_intent": null, 58 | "reason": "fraudulent", 59 | "status": "warning_needs_response" 60 | } 61 | }, 62 | "livemode": false, 63 | "pending_webhooks": 2, 64 | "request": { 65 | "id": "req_00000000000000", 66 | "idempotency_key": "fa04076c-aa1f-40b8-9191-95465afc576e" 67 | }, 68 | "type": "charge.dispute.created" 69 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/payment_intent.canceled.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320024, 6 | "data": { 7 | "object": { 8 | "id": "pi_000000000000000000000000", 9 | "object": "payment_intent", 10 | "amount": 2000, 11 | "amount_capturable": 0, 12 | "amount_received": 0, 13 | "application": null, 14 | "application_fee_amount": null, 15 | "automatic_payment_methods": null, 16 | "canceled_at": 1648320024, 17 | "cancellation_reason": "requested_by_customer", 18 | "capture_method": "automatic", 19 | "charges": { 20 | "object": "list", 21 | "data": [ 22 | 23 | ], 24 | "has_more": false, 25 | "total_count": 0, 26 | "url": "/v1/charges?payment_intent=pi_000000000000000000000000" 27 | }, 28 | "client_secret": "pi_000000000000000000000000_secret_0000000000000000000000000", 29 | "confirmation_method": "automatic", 30 | "created": 1648320024, 31 | "currency": "usd", 32 | "customer": null, 33 | "description": "(created by Stripe CLI)", 34 | "invoice": null, 35 | "last_payment_error": null, 36 | "livemode": false, 37 | "metadata": { 38 | }, 39 | "next_action": null, 40 | "on_behalf_of": null, 41 | "payment_method": null, 42 | "payment_method_options": { 43 | "card": { 44 | "installments": null, 45 | "mandate_options": null, 46 | "network": null, 47 | "request_three_d_secure": "automatic" 48 | } 49 | }, 50 | "payment_method_types": [ 51 | "card" 52 | ], 53 | "processing": null, 54 | "receipt_email": null, 55 | "review": null, 56 | "setup_future_usage": null, 57 | "shipping": null, 58 | "source": null, 59 | "statement_descriptor": null, 60 | "statement_descriptor_suffix": null, 61 | "status": "canceled", 62 | "transfer_data": null, 63 | "transfer_group": null 64 | } 65 | }, 66 | "livemode": false, 67 | "pending_webhooks": 2, 68 | "request": { 69 | "id": "req_00000000000000", 70 | "idempotency_key": "696d09f4-dbea-44e6-8c70-39430b961361" 71 | }, 72 | "type": "payment_intent.canceled" 73 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/checkout.session.completed.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648319959, 6 | "data": { 7 | "object": { 8 | "id": "cs_test_0000000000000000000000000000000000000000000000000000000000", 9 | "object": "checkout.session", 10 | "after_expiration": null, 11 | "allow_promotion_codes": null, 12 | "amount_subtotal": 3000, 13 | "amount_total": 3000, 14 | "automatic_tax": { 15 | "enabled": false, 16 | "status": null 17 | }, 18 | "billing_address_collection": null, 19 | "cancel_url": "https://httpbin.org/post", 20 | "client_reference_id": null, 21 | "consent": null, 22 | "consent_collection": null, 23 | "currency": "usd", 24 | "customer": "cus_00000000000000", 25 | "customer_creation": "always", 26 | "customer_details": { 27 | "email": "stripe@example.com", 28 | "phone": null, 29 | "tax_exempt": "none", 30 | "tax_ids": [ 31 | 32 | ] 33 | }, 34 | "customer_email": null, 35 | "expires_at": 1648406355, 36 | "livemode": false, 37 | "locale": null, 38 | "metadata": { 39 | }, 40 | "mode": "payment", 41 | "payment_intent": "pi_000000000000000000000000", 42 | "payment_link": null, 43 | "payment_method_options": { 44 | }, 45 | "payment_method_types": [ 46 | "card" 47 | ], 48 | "payment_status": "paid", 49 | "phone_number_collection": { 50 | "enabled": false 51 | }, 52 | "recovered_from": null, 53 | "setup_intent": null, 54 | "shipping": null, 55 | "shipping_address_collection": null, 56 | "shipping_options": [ 57 | 58 | ], 59 | "shipping_rate": null, 60 | "status": "complete", 61 | "submit_type": null, 62 | "subscription": null, 63 | "success_url": "https://httpbin.org/post", 64 | "total_details": { 65 | "amount_discount": 0, 66 | "amount_shipping": 0, 67 | "amount_tax": 0 68 | }, 69 | "url": null 70 | } 71 | }, 72 | "livemode": false, 73 | "pending_webhooks": 2, 74 | "request": { 75 | "id": null, 76 | "idempotency_key": null 77 | }, 78 | "type": "checkout.session.completed" 79 | } -------------------------------------------------------------------------------- /spec/shared_stripe_examples/invoice_item_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Invoice Item API' do 4 | 5 | context "creating a new invoice item" do 6 | it "creates a stripe invoice item" do 7 | invoice_item = Stripe::InvoiceItem.create({ 8 | amount: 1099, 9 | customer: 1234, 10 | currency: 'USD', 11 | description: "invoice item desc" 12 | }, 'abcde') 13 | 14 | expect(invoice_item.id).to match(/^test_ii/) 15 | expect(invoice_item.amount).to eq(1099) 16 | expect(invoice_item.description).to eq('invoice item desc') 17 | end 18 | 19 | it "stores a created stripe invoice item in memory" do 20 | invoice_item = Stripe::InvoiceItem.create 21 | data = test_data_source(:invoice_items) 22 | expect(data[invoice_item.id]).to_not be_nil 23 | expect(data[invoice_item.id][:id]).to eq(invoice_item.id) 24 | end 25 | end 26 | 27 | context "retrieving an invoice item" do 28 | it "retrieves a stripe invoice item" do 29 | original = Stripe::InvoiceItem.create 30 | invoice_item = Stripe::InvoiceItem.retrieve(original.id) 31 | expect(invoice_item.id).to eq(original.id) 32 | end 33 | end 34 | 35 | context "retrieving a list of invoice items" do 36 | before do 37 | Stripe::InvoiceItem.create({ amount: 1075 }) 38 | Stripe::InvoiceItem.create({ amount: 1540 }) 39 | end 40 | 41 | it "retrieves all invoice items" do 42 | all = Stripe::InvoiceItem.list 43 | expect(all.count).to eq(2) 44 | expect(all.map &:amount).to include(1075, 1540) 45 | end 46 | end 47 | 48 | it "updates a stripe invoice_item" do 49 | original = Stripe::InvoiceItem.create(id: 'test_invoice_item_update') 50 | amount = original.amount 51 | 52 | original.description = 'new desc' 53 | original.save 54 | 55 | expect(original.amount).to eq(amount) 56 | expect(original.description).to eq('new desc') 57 | 58 | invoice_item = Stripe::InvoiceItem.retrieve("test_invoice_item_update") 59 | expect(invoice_item.amount).to eq(original.amount) 60 | expect(invoice_item.description).to eq('new desc') 61 | end 62 | 63 | it "deletes a invoice_item" do 64 | invoice_item = Stripe::InvoiceItem.create(id: 'test_invoice_item_sub') 65 | invoice_item = invoice_item.delete 66 | expect(invoice_item.deleted).to eq true 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/payout_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Payout API' do 4 | 5 | it "creates a stripe payout" do 6 | payout = Stripe::Payout.create(amount: "100", currency: "usd") 7 | 8 | expect(payout.id).to match(/^test_po/) 9 | expect(payout.amount).to eq('100') 10 | expect(payout.currency).to eq('usd') 11 | expect(payout.metadata.to_hash).to eq({}) 12 | end 13 | 14 | describe "listing payouts" do 15 | before do 16 | 3.times do 17 | Stripe::Payout.create(amount: "100", currency: "usd") 18 | end 19 | end 20 | 21 | it "without params retrieves all tripe payouts" do 22 | expect(Stripe::Payout.list.count).to eq(3) 23 | end 24 | 25 | it "accepts a limit param" do 26 | expect(Stripe::Payout.list(limit: 2).count).to eq(2) 27 | end 28 | end 29 | 30 | it "retrieves a stripe payout" do 31 | original = Stripe::Payout.create(amount: "100", currency: "usd") 32 | payout = Stripe::Payout.retrieve(original.id) 33 | 34 | expect(payout.id).to eq(original.id) 35 | expect(payout.amount).to eq(original.amount) 36 | expect(payout.currency).to eq(original.currency) 37 | expect(payout.metadata.to_hash).to eq(original.metadata.to_hash) 38 | end 39 | 40 | it "cannot retrieve a payout that doesn't exist" do 41 | expect { Stripe::Payout.retrieve('nope') }.to raise_error {|e| 42 | expect(e).to be_a Stripe::InvalidRequestError 43 | expect(e.param).to eq('payout') 44 | expect(e.http_status).to eq(404) 45 | } 46 | end 47 | 48 | it 'when amount is not integer', live: true do 49 | expect { Stripe::Payout.create(amount: '400.2', 50 | currency: 'usd', 51 | description: 'Payout for test@example.com') }.to raise_error { |e| 52 | expect(e).to be_a Stripe::InvalidRequestError 53 | expect(e.param).to eq('amount') 54 | expect(e.http_status).to eq(400) 55 | } 56 | end 57 | 58 | it 'when amount is negative', live: true do 59 | expect { Stripe::Payout.create(amount: '-400', 60 | currency: 'usd', 61 | description: 'Payout for test@example.com') }.to raise_error { |e| 62 | expect(e).to be_a Stripe::InvalidRequestError 63 | expect(e.param).to eq('amount') 64 | expect(e.message).to match(/^Invalid.*integer/) 65 | expect(e.http_status).to eq(400) 66 | } 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/transfer.paid.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "transfer.paid", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "tr_00000000000000", 10 | "object": "transfer", 11 | "date": 1381104000, 12 | "livemode": false, 13 | "amount": 67, 14 | "currency": "usd", 15 | "status": "paid", 16 | "balance_transaction": "txn_00000000000000", 17 | "summary": { 18 | "charge_gross": 100, 19 | "charge_fees": 33, 20 | "charge_fee_details": [ 21 | { 22 | "amount": 33, 23 | "currency": "usd", 24 | "type": "stripe_fee", 25 | "description": null, 26 | "application": null 27 | } 28 | ], 29 | "refund_gross": 0, 30 | "refund_fees": 0, 31 | "refund_fee_details": [ 32 | 33 | ], 34 | "adjustment_gross": 0, 35 | "adjustment_fees": 0, 36 | "adjustment_fee_details": [ 37 | 38 | ], 39 | "validation_fees": 0, 40 | "validation_count": 0, 41 | "charge_count": 1, 42 | "refund_count": 0, 43 | "adjustment_count": 0, 44 | "net": 67, 45 | "currency": "usd", 46 | "collected_fee_gross": 0, 47 | "collected_fee_count": 0, 48 | "collected_fee_refund_gross": 0, 49 | "collected_fee_refund_count": 0 50 | }, 51 | "transactions": { 52 | "object": "list", 53 | "count": 1, 54 | "url": "/v1/transfers/tr_2h8RC13PPvwDZs/transactions", 55 | "has_more": false, 56 | "data": [ 57 | { 58 | "id": "ch_2fb4RERw49oI8s", 59 | "type": "charge", 60 | "amount": 100, 61 | "currency": "usd", 62 | "net": 67, 63 | "created": 1380582860, 64 | "description": null, 65 | "fee": 33, 66 | "fee_details": [ 67 | { 68 | "amount": 33, 69 | "currency": "usd", 70 | "type": "stripe_fee", 71 | "description": "Stripe processing fees", 72 | "application": null 73 | } 74 | ] 75 | } 76 | ] 77 | }, 78 | "other_transfers": [ 79 | "tr_2h8RC13PPvwDZs" 80 | ], 81 | "account": null, 82 | "description": "STRIPE TRANSFER", 83 | "metadata": { 84 | }, 85 | "statement_descriptor": null, 86 | "recipient": null 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/transfer.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "transfer.created", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "tr_00000000000000", 10 | "object": "transfer", 11 | "date": 1381104000, 12 | "livemode": false, 13 | "amount": 67, 14 | "currency": "usd", 15 | "status": "pending", 16 | "balance_transaction": "txn_00000000000000", 17 | "summary": { 18 | "charge_gross": 100, 19 | "charge_fees": 33, 20 | "charge_fee_details": [ 21 | { 22 | "amount": 33, 23 | "currency": "usd", 24 | "type": "stripe_fee", 25 | "description": null, 26 | "application": null 27 | } 28 | ], 29 | "refund_gross": 0, 30 | "refund_fees": 0, 31 | "refund_fee_details": [ 32 | 33 | ], 34 | "adjustment_gross": 0, 35 | "adjustment_fees": 0, 36 | "adjustment_fee_details": [ 37 | 38 | ], 39 | "validation_fees": 0, 40 | "validation_count": 0, 41 | "charge_count": 1, 42 | "refund_count": 0, 43 | "adjustment_count": 0, 44 | "net": 67, 45 | "currency": "usd", 46 | "collected_fee_gross": 0, 47 | "collected_fee_count": 0, 48 | "collected_fee_refund_gross": 0, 49 | "collected_fee_refund_count": 0 50 | }, 51 | "transactions": { 52 | "object": "list", 53 | "count": 1, 54 | "url": "/v1/transfers/tr_2h8RC13PPvwDZs/transactions", 55 | "has_more": false, 56 | "data": [ 57 | { 58 | "id": "ch_2fb4RERw49oI8s", 59 | "type": "charge", 60 | "amount": 100, 61 | "currency": "usd", 62 | "net": 67, 63 | "created": 1380582860, 64 | "description": null, 65 | "fee": 33, 66 | "fee_details": [ 67 | { 68 | "amount": 33, 69 | "currency": "usd", 70 | "type": "stripe_fee", 71 | "description": "Stripe processing fees", 72 | "application": null 73 | } 74 | ] 75 | } 76 | ] 77 | }, 78 | "other_transfers": [ 79 | "tr_2h8RC13PPvwDZs" 80 | ], 81 | "account": null, 82 | "description": "STRIPE TRANSFER", 83 | "metadata": { 84 | }, 85 | "statement_descriptor": null, 86 | "recipient": null 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/transfer.failed.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "transfer.failed", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "tr_00000000000000", 10 | "object": "transfer", 11 | "date": 1381104000, 12 | "livemode": false, 13 | "amount": 67, 14 | "currency": "usd", 15 | "status": "failed", 16 | "balance_transaction": "txn_00000000000000", 17 | "summary": { 18 | "charge_gross": 100, 19 | "charge_fees": 33, 20 | "charge_fee_details": [ 21 | { 22 | "amount": 33, 23 | "currency": "usd", 24 | "type": "stripe_fee", 25 | "description": null, 26 | "application": null 27 | } 28 | ], 29 | "refund_gross": 0, 30 | "refund_fees": 0, 31 | "refund_fee_details": [ 32 | 33 | ], 34 | "adjustment_gross": 0, 35 | "adjustment_fees": 0, 36 | "adjustment_fee_details": [ 37 | 38 | ], 39 | "validation_fees": 0, 40 | "validation_count": 0, 41 | "charge_count": 1, 42 | "refund_count": 0, 43 | "adjustment_count": 0, 44 | "net": 67, 45 | "currency": "usd", 46 | "collected_fee_gross": 0, 47 | "collected_fee_count": 0, 48 | "collected_fee_refund_gross": 0, 49 | "collected_fee_refund_count": 0 50 | }, 51 | "transactions": { 52 | "object": "list", 53 | "count": 1, 54 | "url": "/v1/transfers/tr_2h8RC13PPvwDZs/transactions", 55 | "has_more": false, 56 | "data": [ 57 | { 58 | "id": "ch_2fb4RERw49oI8s", 59 | "type": "charge", 60 | "amount": 100, 61 | "currency": "usd", 62 | "net": 67, 63 | "created": 1380582860, 64 | "description": null, 65 | "fee": 33, 66 | "fee_details": [ 67 | { 68 | "amount": 33, 69 | "currency": "usd", 70 | "type": "stripe_fee", 71 | "description": "Stripe processing fees", 72 | "application": null 73 | } 74 | ] 75 | } 76 | ] 77 | }, 78 | "other_transfers": [ 79 | "tr_2h8RC13PPvwDZs" 80 | ], 81 | "account": null, 82 | "description": "STRIPE TRANSFER", 83 | "metadata": { 84 | }, 85 | "statement_descriptor": null, 86 | "recipient": null 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/transfers.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Transfers 4 | 5 | def Transfers.included(klass) 6 | klass.add_handler 'post /v1/transfers', :new_transfer 7 | klass.add_handler 'get /v1/transfers', :get_all_transfers 8 | klass.add_handler 'get /v1/transfers/(.*)', :get_transfer 9 | klass.add_handler 'post /v1/transfers/(.*)/cancel', :cancel_transfer 10 | end 11 | 12 | def get_all_transfers(route, method_url, params, headers) 13 | extra_params = params.keys - [:created, :destination, :ending_before, 14 | :limit, :starting_after, :transfer_group] 15 | unless extra_params.empty? 16 | raise Stripe::InvalidRequestError.new("Received unknown parameter: #{extra_params[0]}", extra_params[0].to_s, http_status: 400) 17 | end 18 | 19 | if destination = params[:destination] 20 | assert_existence :destination, destination, accounts[destination] 21 | end 22 | 23 | _transfers = transfers.each_with_object([]) do |(_, transfer), array| 24 | if destination 25 | array << transfer if transfer[:destination] == destination 26 | else 27 | array << transfer 28 | end 29 | end 30 | 31 | if params[:limit] 32 | _transfers = _transfers.first([params[:limit], _transfers.size].min) 33 | end 34 | 35 | Data.mock_list_object(_transfers, params) 36 | end 37 | 38 | def new_transfer(route, method_url, params, headers) 39 | id = new_id('tr') 40 | if params[:bank_account] 41 | params[:account] = get_bank_by_token(params.delete(:bank_account)) 42 | end 43 | 44 | unless params[:amount].is_a?(Integer) || (params[:amount].is_a?(String) && /^\d+$/.match(params[:amount])) 45 | raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400) 46 | end 47 | 48 | transfers[id] = Data.mock_transfer(params.merge :id => id) 49 | end 50 | 51 | def get_transfer(route, method_url, params, headers) 52 | route =~ method_url 53 | assert_existence :transfer, $1, transfers[$1] 54 | transfers[$1] ||= Data.mock_transfer(:id => $1) 55 | end 56 | 57 | def cancel_transfer(route, method_url, params, headers) 58 | route =~ method_url 59 | assert_existence :transfer, $1, transfers[$1] 60 | t = transfers[$1] ||= Data.mock_transfer(:id => $1) 61 | t.merge!({:status => "canceled"}) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/setup_intent_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'SetupIntent API' do 4 | 5 | it "creates a stripe setup_intent" do 6 | setup_intent = Stripe::SetupIntent.create() 7 | 8 | expect(setup_intent.id).to match(/^test_si/) 9 | expect(setup_intent.metadata.to_hash).to eq({}) 10 | expect(setup_intent.status).to eq('requires_payment_method') 11 | end 12 | 13 | describe "listing setup_intent" do 14 | before do 15 | 3.times do 16 | Stripe::SetupIntent.create() 17 | end 18 | end 19 | 20 | it "without params retrieves all stripe setup_intent" do 21 | expect(Stripe::SetupIntent.list.count).to eq(3) 22 | end 23 | 24 | it "accepts a limit param" do 25 | expect(Stripe::SetupIntent.list(limit: 2).count).to eq(2) 26 | end 27 | end 28 | 29 | it "retrieves a stripe setup_intent" do 30 | original = Stripe::SetupIntent.create() 31 | setup_intent = Stripe::SetupIntent.retrieve(original.id) 32 | 33 | expect(setup_intent.id).to eq(original.id) 34 | expect(setup_intent.metadata.to_hash).to eq(original.metadata.to_hash) 35 | end 36 | 37 | it "cannot retrieve a setup_intent that doesn't exist" do 38 | expect { Stripe::SetupIntent.retrieve('nope') }.to raise_error {|e| 39 | expect(e).to be_a Stripe::InvalidRequestError 40 | expect(e.param).to eq('setup_intent') 41 | expect(e.http_status).to eq(404) 42 | } 43 | end 44 | 45 | it "expands payment_method" do 46 | payment_method = Stripe::PaymentMethod.create(type: "card") 47 | original = Stripe::SetupIntent.create(payment_method: payment_method.id) 48 | 49 | setup_intent = Stripe::SetupIntent.retrieve({id: original.id, expand: ["payment_method"]}) 50 | 51 | expect(setup_intent.payment_method).to eq(payment_method) 52 | end 53 | 54 | it "confirms a stripe setup_intent" do 55 | setup_intent = Stripe::SetupIntent.create() 56 | confirmed_setup_intent = setup_intent.confirm() 57 | expect(confirmed_setup_intent.status).to eq("succeeded") 58 | end 59 | 60 | it "cancels a stripe setup_intent" do 61 | setup_intent = Stripe::SetupIntent.create() 62 | confirmed_setup_intent = setup_intent.cancel() 63 | expect(confirmed_setup_intent.status).to eq("canceled") 64 | end 65 | 66 | it "updates a stripe setup_intent" do 67 | original = Stripe::SetupIntent.create() 68 | setup_intent = Stripe::SetupIntent.retrieve(original.id) 69 | 70 | setup_intent.metadata[:foo] = :bar 71 | setup_intent.save 72 | 73 | updated = Stripe::SetupIntent.retrieve(original.id) 74 | 75 | expect(updated.metadata[:foo]).to eq(:bar) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/quote.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320086, 6 | "data": { 7 | "object": { 8 | "id": "qt_000000000000000000000000", 9 | "object": "quote", 10 | "amount_subtotal": 15000, 11 | "amount_total": 15000, 12 | "application_fee_amount": null, 13 | "application_fee_percent": null, 14 | "automatic_tax": { 15 | "enabled": false, 16 | "status": null 17 | }, 18 | "collection_method": "charge_automatically", 19 | "computed": { 20 | "recurring": { 21 | "amount_subtotal": 15000, 22 | "amount_total": 15000, 23 | "interval": "month", 24 | "interval_count": 1, 25 | "total_details": { 26 | "amount_discount": 0, 27 | "amount_shipping": 0, 28 | "amount_tax": 0 29 | } 30 | }, 31 | "upfront": { 32 | "amount_subtotal": 15000, 33 | "amount_total": 15000, 34 | "total_details": { 35 | "amount_discount": 0, 36 | "amount_shipping": 0, 37 | "amount_tax": 0 38 | } 39 | } 40 | }, 41 | "created": 1648320085, 42 | "currency": "usd", 43 | "customer": "cus_00000000000000", 44 | "default_tax_rates": [ 45 | 46 | ], 47 | "description": "(created by Stripe CLI)", 48 | "discounts": [ 49 | 50 | ], 51 | "expires_at": 1650912085, 52 | "footer": null, 53 | "from_quote": null, 54 | "header": null, 55 | "invoice": null, 56 | "invoice_settings": { 57 | "days_until_due": null 58 | }, 59 | "livemode": false, 60 | "metadata": { 61 | }, 62 | "number": null, 63 | "on_behalf_of": null, 64 | "status": "draft", 65 | "status_transitions": { 66 | "accepted_at": null, 67 | "canceled_at": null, 68 | "finalized_at": null 69 | }, 70 | "subscription": null, 71 | "subscription_data": { 72 | "effective_date": null, 73 | "trial_period_days": null 74 | }, 75 | "subscription_schedule": null, 76 | "test_clock": null, 77 | "total_details": { 78 | "amount_discount": 0, 79 | "amount_shipping": 0, 80 | "amount_tax": 0 81 | }, 82 | "transfer_data": null 83 | } 84 | }, 85 | "livemode": false, 86 | "pending_webhooks": 2, 87 | "request": { 88 | "id": "req_00000000000000", 89 | "idempotency_key": "444b105e-dba9-4779-b9aa-80bdc9b9a354" 90 | }, 91 | "type": "quote.created" 92 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/quote.canceled.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320077, 6 | "data": { 7 | "object": { 8 | "id": "qt_000000000000000000000000", 9 | "object": "quote", 10 | "amount_subtotal": 15000, 11 | "amount_total": 15000, 12 | "application_fee_amount": null, 13 | "application_fee_percent": null, 14 | "automatic_tax": { 15 | "enabled": false, 16 | "status": null 17 | }, 18 | "collection_method": "charge_automatically", 19 | "computed": { 20 | "recurring": { 21 | "amount_subtotal": 15000, 22 | "amount_total": 15000, 23 | "interval": "month", 24 | "interval_count": 1, 25 | "total_details": { 26 | "amount_discount": 0, 27 | "amount_shipping": 0, 28 | "amount_tax": 0 29 | } 30 | }, 31 | "upfront": { 32 | "amount_subtotal": 15000, 33 | "amount_total": 15000, 34 | "total_details": { 35 | "amount_discount": 0, 36 | "amount_shipping": 0, 37 | "amount_tax": 0 38 | } 39 | } 40 | }, 41 | "created": 1648320076, 42 | "currency": "usd", 43 | "customer": "cus_00000000000000", 44 | "default_tax_rates": [ 45 | 46 | ], 47 | "description": "(created by Stripe CLI)", 48 | "discounts": [ 49 | 50 | ], 51 | "expires_at": 1650912076, 52 | "footer": null, 53 | "from_quote": null, 54 | "header": null, 55 | "invoice": null, 56 | "invoice_settings": { 57 | "days_until_due": null 58 | }, 59 | "livemode": false, 60 | "metadata": { 61 | }, 62 | "number": null, 63 | "on_behalf_of": null, 64 | "status": "canceled", 65 | "status_transitions": { 66 | "accepted_at": null, 67 | "canceled_at": 1648320077, 68 | "finalized_at": null 69 | }, 70 | "subscription": null, 71 | "subscription_data": { 72 | "effective_date": null, 73 | "trial_period_days": null 74 | }, 75 | "subscription_schedule": null, 76 | "test_clock": null, 77 | "total_details": { 78 | "amount_discount": 0, 79 | "amount_shipping": 0, 80 | "amount_tax": 0 81 | }, 82 | "transfer_data": null 83 | } 84 | }, 85 | "livemode": false, 86 | "pending_webhooks": 2, 87 | "request": { 88 | "id": "req_00000000000000", 89 | "idempotency_key": "81d553e8-c88c-4a3e-9572-24da02fd83c0" 90 | }, 91 | "type": "quote.canceled" 92 | } -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/sources.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Sources 4 | 5 | def Sources.included(klass) 6 | klass.add_handler 'get /v1/customers/(.*)/sources', :retrieve_sources 7 | klass.add_handler 'post /v1/customers/(.*)/sources', :create_source 8 | klass.add_handler 'post /v1/customers/(.*)/sources/(.*)/verify', :verify_source 9 | klass.add_handler 'get /v1/customers/(.*)/sources/(.*)', :retrieve_source 10 | klass.add_handler 'delete /v1/customers/(.*)/sources/(.*)', :delete_source 11 | klass.add_handler 'post /v1/customers/(.*)/sources/(.*)', :update_source 12 | end 13 | 14 | def create_source(route, method_url, params, headers) 15 | stripe_account = headers && headers[:stripe_account] || Stripe.api_key 16 | route =~ method_url 17 | add_source_to(:customer, $1, params, customers[stripe_account]) 18 | end 19 | 20 | def retrieve_sources(route, method_url, params, headers) 21 | stripe_account = headers && headers[:stripe_account] || Stripe.api_key 22 | route =~ method_url 23 | retrieve_object_cards(:customer, $1, customers[stripe_account]) 24 | end 25 | 26 | def retrieve_source(route, method_url, params, headers) 27 | stripe_account = headers && headers[:stripe_account] || Stripe.api_key 28 | route =~ method_url 29 | customer = assert_existence :customer, $1, customers[stripe_account][$1] 30 | 31 | assert_existence :card, $2, get_card(customer, $2) 32 | end 33 | 34 | def delete_source(route, method_url, params, headers) 35 | stripe_account = headers && headers[:stripe_account] || Stripe.api_key 36 | route =~ method_url 37 | delete_card_from(:customer, $1, $2, customers[stripe_account]) 38 | end 39 | 40 | def update_source(route, method_url, params, headers) 41 | stripe_account = headers && headers[:stripe_account] || Stripe.api_key 42 | route =~ method_url 43 | customer = assert_existence :customer, $1, customers[stripe_account][$1] 44 | 45 | card = assert_existence :card, $2, get_card(customer, $2) 46 | card.merge!(params) 47 | card 48 | end 49 | 50 | def verify_source(route, method_url, params, headers) 51 | stripe_account = headers && headers[:stripe_account] || Stripe.api_key 52 | route =~ method_url 53 | customer = assert_existence :customer, $1, customers[stripe_account][$1] 54 | 55 | bank_account = assert_existence :bank_account, $2, verify_bank_account(customer, $2) 56 | bank_account 57 | end 58 | 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/transfer.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "transfer.updated", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "tr_00000000000000", 10 | "object": "transfer", 11 | "date": 1381104000, 12 | "livemode": false, 13 | "amount": 67, 14 | "currency": "usd", 15 | "status": "pending", 16 | "balance_transaction": "txn_00000000000000", 17 | "summary": { 18 | "charge_gross": 100, 19 | "charge_fees": 33, 20 | "charge_fee_details": [ 21 | { 22 | "amount": 33, 23 | "currency": "usd", 24 | "type": "stripe_fee", 25 | "description": null, 26 | "application": null 27 | } 28 | ], 29 | "refund_gross": 0, 30 | "refund_fees": 0, 31 | "refund_fee_details": [ 32 | 33 | ], 34 | "adjustment_gross": 0, 35 | "adjustment_fees": 0, 36 | "adjustment_fee_details": [ 37 | 38 | ], 39 | "validation_fees": 0, 40 | "validation_count": 0, 41 | "charge_count": 1, 42 | "refund_count": 0, 43 | "adjustment_count": 0, 44 | "net": 67, 45 | "currency": "usd", 46 | "collected_fee_gross": 0, 47 | "collected_fee_count": 0, 48 | "collected_fee_refund_gross": 0, 49 | "collected_fee_refund_count": 0 50 | }, 51 | "transactions": { 52 | "object": "list", 53 | "count": 1, 54 | "url": "/v1/transfers/tr_2h8RC13PPvwDZs/transactions", 55 | "has_more": false, 56 | "data": [ 57 | { 58 | "id": "ch_2fb4RERw49oI8s", 59 | "type": "charge", 60 | "amount": 100, 61 | "currency": "usd", 62 | "net": 67, 63 | "created": 1380582860, 64 | "description": null, 65 | "fee": 33, 66 | "fee_details": [ 67 | { 68 | "amount": 33, 69 | "currency": "usd", 70 | "type": "stripe_fee", 71 | "description": "Stripe processing fees", 72 | "application": null 73 | } 74 | ] 75 | } 76 | ] 77 | }, 78 | "other_transfers": [ 79 | "tr_2h8RC13PPvwDZs" 80 | ], 81 | "account": null, 82 | "description": "STRIPE TRANSFER", 83 | "metadata": { 84 | }, 85 | "statement_descriptor": null, 86 | "recipient": null 87 | }, 88 | "previous_attributes": { 89 | "amount": 123 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/quote.finalized.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320086, 6 | "data": { 7 | "object": { 8 | "id": "qt_000000000000000000000000", 9 | "object": "quote", 10 | "amount_subtotal": 15000, 11 | "amount_total": 15000, 12 | "application_fee_amount": null, 13 | "application_fee_percent": null, 14 | "automatic_tax": { 15 | "enabled": false, 16 | "status": null 17 | }, 18 | "collection_method": "charge_automatically", 19 | "computed": { 20 | "recurring": { 21 | "amount_subtotal": 15000, 22 | "amount_total": 15000, 23 | "interval": "month", 24 | "interval_count": 1, 25 | "total_details": { 26 | "amount_discount": 0, 27 | "amount_shipping": 0, 28 | "amount_tax": 0 29 | } 30 | }, 31 | "upfront": { 32 | "amount_subtotal": 15000, 33 | "amount_total": 15000, 34 | "total_details": { 35 | "amount_discount": 0, 36 | "amount_shipping": 0, 37 | "amount_tax": 0 38 | } 39 | } 40 | }, 41 | "created": 1648320085, 42 | "currency": "usd", 43 | "customer": "cus_00000000000000", 44 | "default_tax_rates": [ 45 | 46 | ], 47 | "description": "(created by Stripe CLI)", 48 | "discounts": [ 49 | 50 | ], 51 | "expires_at": 1650912085, 52 | "footer": null, 53 | "from_quote": null, 54 | "header": null, 55 | "invoice": null, 56 | "invoice_settings": { 57 | "days_until_due": null 58 | }, 59 | "livemode": false, 60 | "metadata": { 61 | }, 62 | "number": "QT-013268C5-0001-1", 63 | "on_behalf_of": null, 64 | "status": "open", 65 | "status_transitions": { 66 | "accepted_at": null, 67 | "canceled_at": null, 68 | "finalized_at": 1648320086 69 | }, 70 | "subscription": null, 71 | "subscription_data": { 72 | "effective_date": null, 73 | "trial_period_days": null 74 | }, 75 | "subscription_schedule": null, 76 | "test_clock": null, 77 | "total_details": { 78 | "amount_discount": 0, 79 | "amount_shipping": 0, 80 | "amount_tax": 0 81 | }, 82 | "transfer_data": null 83 | } 84 | }, 85 | "livemode": false, 86 | "pending_webhooks": 2, 87 | "request": { 88 | "id": "req_00000000000000", 89 | "idempotency_key": "1aa55aab-a2f3-4b60-9755-bd8ab903d387" 90 | }, 91 | "type": "quote.finalized" 92 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/payment_intent.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320032, 6 | "data": { 7 | "object": { 8 | "id": "pi_000000000000000000000000", 9 | "object": "payment_intent", 10 | "amount": 2000, 11 | "amount_capturable": 0, 12 | "amount_received": 0, 13 | "application": null, 14 | "application_fee_amount": null, 15 | "automatic_payment_methods": null, 16 | "canceled_at": null, 17 | "cancellation_reason": null, 18 | "capture_method": "automatic", 19 | "charges": { 20 | "object": "list", 21 | "data": [ 22 | 23 | ], 24 | "has_more": false, 25 | "total_count": 0, 26 | "url": "/v1/charges?payment_intent=pi_000000000000000000000000" 27 | }, 28 | "client_secret": "pi_000000000000000000000000_secret_0000000000000000000000000", 29 | "confirmation_method": "automatic", 30 | "created": 1648320032, 31 | "currency": "usd", 32 | "customer": null, 33 | "description": "(created by Stripe CLI)", 34 | "invoice": null, 35 | "last_payment_error": null, 36 | "livemode": false, 37 | "metadata": { 38 | }, 39 | "next_action": null, 40 | "on_behalf_of": null, 41 | "payment_method": null, 42 | "payment_method_options": { 43 | "card": { 44 | "installments": null, 45 | "mandate_options": null, 46 | "network": null, 47 | "request_three_d_secure": "automatic" 48 | } 49 | }, 50 | "payment_method_types": [ 51 | "card" 52 | ], 53 | "processing": null, 54 | "receipt_email": null, 55 | "review": null, 56 | "setup_future_usage": null, 57 | "shipping": { 58 | "address": { 59 | "city": "San Francisco", 60 | "country": "US", 61 | "line1": "510 Townsend St", 62 | "line2": null, 63 | "postal_code": "94103", 64 | "state": "CA" 65 | }, 66 | "carrier": null, 67 | "name": "Jenny Rosen", 68 | "phone": null, 69 | "tracking_number": null 70 | }, 71 | "source": null, 72 | "statement_descriptor": null, 73 | "statement_descriptor_suffix": null, 74 | "status": "requires_payment_method", 75 | "transfer_data": null, 76 | "transfer_group": null 77 | } 78 | }, 79 | "livemode": false, 80 | "pending_webhooks": 2, 81 | "request": { 82 | "id": "req_00000000000000", 83 | "idempotency_key": "7143a956-2f40-407a-b312-43038ae76c86" 84 | }, 85 | "type": "payment_intent.created" 86 | } -------------------------------------------------------------------------------- /spec/readme_spec.rb: -------------------------------------------------------------------------------- 1 | require 'stripe_mock' 2 | 3 | describe 'README examples' do 4 | let(:stripe_helper) { StripeMock.create_test_helper } 5 | 6 | before { StripeMock.start } 7 | after { StripeMock.stop } 8 | 9 | it "creates a stripe customer" do 10 | 11 | # This doesn't touch stripe's servers nor the internet! 12 | customer = Stripe::Customer.create({ 13 | email: 'johnny@appleseed.com', 14 | card: stripe_helper.generate_card_token 15 | }) 16 | expect(customer.email).to eq('johnny@appleseed.com') 17 | end 18 | 19 | 20 | it "mocks a declined card error" do 21 | # Prepares an error for the next create charge request 22 | StripeMock.prepare_card_error(:card_declined) 23 | 24 | expect { Stripe::Charge.create(amount: 1, currency: 'usd') }.to raise_error {|e| 25 | expect(e).to be_a Stripe::CardError 26 | expect(e.http_status).to eq(402) 27 | expect(e.code).to eq('card_declined') 28 | expect(e.json_body[:error][:decline_code]).to eq('do_not_honor') 29 | } 30 | end 31 | 32 | it "has built-in card errors" do 33 | StripeMock.prepare_card_error(:incorrect_number) 34 | StripeMock.prepare_card_error(:invalid_number) 35 | StripeMock.prepare_card_error(:invalid_expiry_month) 36 | StripeMock.prepare_card_error(:invalid_expiry_year) 37 | StripeMock.prepare_card_error(:invalid_cvc) 38 | StripeMock.prepare_card_error(:expired_card) 39 | StripeMock.prepare_card_error(:incorrect_cvc) 40 | StripeMock.prepare_card_error(:card_declined) 41 | StripeMock.prepare_card_error(:missing) 42 | StripeMock.prepare_card_error(:processing_error) 43 | end 44 | 45 | it "mocks a stripe webhook" do 46 | event = StripeMock.mock_webhook_event('customer.created') 47 | 48 | customer_object = event.data.object 49 | expect(customer_object.id).to_not be_nil 50 | expect(customer_object.default_source).to_not be_nil 51 | # etc. 52 | end 53 | 54 | it "can override default webhook values" do 55 | event = StripeMock.mock_webhook_event('customer.created', { 56 | :id => 'cus_my_custom_value', 57 | :email => 'joe@example.com' 58 | }) 59 | # Alternatively: 60 | # event.data.object.id = 'cus_my_custom_value' 61 | # event.data.object.email = 'joe@example.com' 62 | expect(event.data.object.id).to eq('cus_my_custom_value') 63 | expect(event.data.object.email).to eq('joe@example.com') 64 | end 65 | 66 | it "generates a stripe card token" do 67 | card_token = StripeMock.generate_card_token(last4: "9191", exp_year: 1984) 68 | 69 | cus = Stripe::Customer.create(source: card_token) 70 | card = cus.sources.data.first 71 | expect(card.last4).to eq("9191") 72 | expect(card.exp_year).to eq(1984) 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/quote.accepted.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320071, 6 | "data": { 7 | "object": { 8 | "id": "qt_000000000000000000000000", 9 | "object": "quote", 10 | "amount_subtotal": 15000, 11 | "amount_total": 15000, 12 | "application_fee_amount": null, 13 | "application_fee_percent": null, 14 | "automatic_tax": { 15 | "enabled": false, 16 | "status": null 17 | }, 18 | "collection_method": "charge_automatically", 19 | "computed": { 20 | "recurring": { 21 | "amount_subtotal": 15000, 22 | "amount_total": 15000, 23 | "interval": "month", 24 | "interval_count": 1, 25 | "total_details": { 26 | "amount_discount": 0, 27 | "amount_shipping": 0, 28 | "amount_tax": 0 29 | } 30 | }, 31 | "upfront": { 32 | "amount_subtotal": 15000, 33 | "amount_total": 15000, 34 | "total_details": { 35 | "amount_discount": 0, 36 | "amount_shipping": 0, 37 | "amount_tax": 0 38 | } 39 | } 40 | }, 41 | "created": 1648320070, 42 | "currency": "usd", 43 | "customer": "cus_00000000000000", 44 | "default_tax_rates": [ 45 | 46 | ], 47 | "description": "(created by Stripe CLI)", 48 | "discounts": [ 49 | 50 | ], 51 | "expires_at": 1650912070, 52 | "footer": null, 53 | "from_quote": null, 54 | "header": null, 55 | "invoice": "in_000000000000000000000000", 56 | "invoice_settings": { 57 | "days_until_due": null 58 | }, 59 | "livemode": false, 60 | "metadata": { 61 | }, 62 | "number": "QT-F5FD0419-0001-1", 63 | "on_behalf_of": null, 64 | "status": "accepted", 65 | "status_transitions": { 66 | "accepted_at": 1648320071, 67 | "canceled_at": null, 68 | "finalized_at": 1648320070 69 | }, 70 | "subscription": "sub_000000000000000000000000", 71 | "subscription_data": { 72 | "effective_date": null, 73 | "trial_period_days": null 74 | }, 75 | "subscription_schedule": null, 76 | "test_clock": null, 77 | "total_details": { 78 | "amount_discount": 0, 79 | "amount_shipping": 0, 80 | "amount_tax": 0 81 | }, 82 | "transfer_data": null 83 | } 84 | }, 85 | "livemode": false, 86 | "pending_webhooks": 2, 87 | "request": { 88 | "id": "req_00000000000000", 89 | "idempotency_key": "456e23c0-aebf-4ec5-b23a-a1097b504557" 90 | }, 91 | "type": "quote.accepted" 92 | } -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/orders.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Orders 4 | 5 | def Orders.included(klass) 6 | klass.add_handler 'post /v1/orders', :new_order 7 | klass.add_handler 'post /v1/orders/(.*)/pay', :pay_order 8 | klass.add_handler 'post /v1/orders/(.*)', :update_order 9 | klass.add_handler 'get /v1/orders/(.*)', :get_order 10 | klass.add_handler 'get /v1/orders', :list_orders 11 | end 12 | 13 | def new_order(route, method_url, params, headers) 14 | params[:id] ||= new_id('or') 15 | order_items = [] 16 | 17 | unless params[:currency].to_s.size == 3 18 | raise Stripe::InvalidRequestError.new('You must supply a currency', nil, http_status: 400) 19 | end 20 | 21 | if params[:items] 22 | unless params[:items].is_a? Array 23 | raise Stripe::InvalidRequestError.new('You must supply a list of items', nil, http_status: 400) 24 | end 25 | 26 | unless params[:items].first.is_a? Hash 27 | raise Stripe::InvalidRequestError.new('You must supply an item', nil, http_status: 400) 28 | end 29 | end 30 | 31 | orders[ params[:id] ] = Data.mock_order(order_items, params) 32 | 33 | orders[ params[:id] ] 34 | end 35 | 36 | def update_order(route, method_url, params, headers) 37 | route =~ method_url 38 | order = assert_existence :order, $1, orders[$1] 39 | 40 | if params[:metadata] 41 | if params[:metadata].empty? 42 | order[:metadata] = {} 43 | else 44 | order[:metadata].merge(params[:metadata]) 45 | end 46 | end 47 | 48 | if %w(created paid canceled fulfilled returned).include? params[:status] 49 | order[:status] = params[:status] 50 | end 51 | order 52 | end 53 | 54 | def get_order(route, method_url, params, headers) 55 | route =~ method_url 56 | assert_existence :order, $1, orders[$1] 57 | end 58 | 59 | def pay_order(route, method_url, params, headers) 60 | route =~ method_url 61 | order = assert_existence :order, $1, orders[$1] 62 | 63 | if params[:source].blank? && params[:customer].blank? 64 | raise Stripe::InvalidRequestError.new('You must supply a source or customer', nil, http_status: 400) 65 | end 66 | 67 | charge_id = new_id('ch') 68 | charges[charge_id] = Data.mock_charge(id: charge_id) 69 | order[:charge] = charge_id 70 | order[:status] = "paid" 71 | order 72 | end 73 | 74 | def list_orders(route, method_url, params, headers) 75 | Data.mock_list_object(orders.values, params) 76 | end 77 | 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/invoiceitem.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320097, 6 | "data": { 7 | "object": { 8 | "id": "ii_000000000000000000000000", 9 | "object": "invoiceitem", 10 | "amount": -1500, 11 | "currency": "usd", 12 | "customer": "cus_00000000000000", 13 | "date": 1648320097, 14 | "description": "Unused time on myproduct after 26 Mar 2022", 15 | "discountable": false, 16 | "discounts": [ 17 | 18 | ], 19 | "invoice": null, 20 | "livemode": false, 21 | "metadata": { 22 | }, 23 | "period": { 24 | "end": 1650998496, 25 | "start": 1648320097 26 | }, 27 | "plan": { 28 | "id": "price_000000000000000000000000", 29 | "object": "plan", 30 | "active": true, 31 | "aggregate_usage": null, 32 | "amount": 1500, 33 | "amount_decimal": "1500", 34 | "billing_scheme": "per_unit", 35 | "created": 1648320095, 36 | "currency": "usd", 37 | "interval": "month", 38 | "interval_count": 1, 39 | "livemode": false, 40 | "metadata": { 41 | }, 42 | "nickname": null, 43 | "product": "prod_00000000000000", 44 | "tiers_mode": null, 45 | "transform_usage": null, 46 | "trial_period_days": null, 47 | "usage_type": "licensed" 48 | }, 49 | "price": { 50 | "id": "price_000000000000000000000000", 51 | "object": "price", 52 | "active": true, 53 | "billing_scheme": "per_unit", 54 | "created": 1648320095, 55 | "currency": "usd", 56 | "livemode": false, 57 | "lookup_key": null, 58 | "metadata": { 59 | }, 60 | "nickname": null, 61 | "product": "prod_00000000000000", 62 | "recurring": { 63 | "aggregate_usage": null, 64 | "interval": "month", 65 | "interval_count": 1, 66 | "trial_period_days": null, 67 | "usage_type": "licensed" 68 | }, 69 | "tax_behavior": "unspecified", 70 | "tiers_mode": null, 71 | "transform_quantity": null, 72 | "type": "recurring", 73 | "unit_amount": 1500, 74 | "unit_amount_decimal": "1500" 75 | }, 76 | "proration": true, 77 | "quantity": 1, 78 | "subscription": "sub_000000000000000000000000", 79 | "subscription_item": "si_00000000000000", 80 | "tax_rates": [ 81 | 82 | ], 83 | "test_clock": null, 84 | "unit_amount": -1500, 85 | "unit_amount_decimal": "-1500" 86 | } 87 | }, 88 | "livemode": false, 89 | "pending_webhooks": 2, 90 | "request": { 91 | "id": "req_00000000000000", 92 | "idempotency_key": "27f56452-b979-42fb-8bc1-219db1e65979" 93 | }, 94 | "type": "invoiceitem.created" 95 | } -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/invoiceitem.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320097, 6 | "data": { 7 | "object": { 8 | "id": "ii_000000000000000000000000", 9 | "object": "invoiceitem", 10 | "amount": -1500, 11 | "currency": "usd", 12 | "customer": "cus_00000000000000", 13 | "date": 1648320097, 14 | "description": "Unused time on myproduct after 26 Mar 2022", 15 | "discountable": false, 16 | "discounts": [ 17 | 18 | ], 19 | "invoice": "in_000000000000000000000000", 20 | "livemode": false, 21 | "metadata": { 22 | }, 23 | "period": { 24 | "end": 1650998496, 25 | "start": 1648320097 26 | }, 27 | "plan": { 28 | "id": "price_000000000000000000000000", 29 | "object": "plan", 30 | "active": true, 31 | "aggregate_usage": null, 32 | "amount": 1500, 33 | "amount_decimal": "1500", 34 | "billing_scheme": "per_unit", 35 | "created": 1648320095, 36 | "currency": "usd", 37 | "interval": "month", 38 | "interval_count": 1, 39 | "livemode": false, 40 | "metadata": { 41 | }, 42 | "nickname": null, 43 | "product": "prod_00000000000000", 44 | "tiers_mode": null, 45 | "transform_usage": null, 46 | "trial_period_days": null, 47 | "usage_type": "licensed" 48 | }, 49 | "price": { 50 | "id": "price_000000000000000000000000", 51 | "object": "price", 52 | "active": true, 53 | "billing_scheme": "per_unit", 54 | "created": 1648320095, 55 | "currency": "usd", 56 | "livemode": false, 57 | "lookup_key": null, 58 | "metadata": { 59 | }, 60 | "nickname": null, 61 | "product": "prod_00000000000000", 62 | "recurring": { 63 | "aggregate_usage": null, 64 | "interval": "month", 65 | "interval_count": 1, 66 | "trial_period_days": null, 67 | "usage_type": "licensed" 68 | }, 69 | "tax_behavior": "unspecified", 70 | "tiers_mode": null, 71 | "transform_quantity": null, 72 | "type": "recurring", 73 | "unit_amount": 1500, 74 | "unit_amount_decimal": "1500" 75 | }, 76 | "proration": true, 77 | "quantity": 1, 78 | "subscription": "sub_000000000000000000000000", 79 | "subscription_item": "si_00000000000000", 80 | "tax_rates": [ 81 | 82 | ], 83 | "test_clock": null, 84 | "unit_amount": -1500, 85 | "unit_amount_decimal": "-1500" 86 | }, 87 | "previous_attributes": { 88 | "invoice": null 89 | } 90 | }, 91 | "livemode": false, 92 | "pending_webhooks": 2, 93 | "request": { 94 | "id": "req_00000000000000", 95 | "idempotency_key": "27f56452-b979-42fb-8bc1-219db1e65979" 96 | }, 97 | "type": "invoiceitem.updated" 98 | } -------------------------------------------------------------------------------- /spec/shared_stripe_examples/balance_transaction_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Balance Transaction API' do 4 | 5 | let(:stripe_helper) { StripeMock.create_test_helper } 6 | 7 | it "returns an error if balance transaction does not exist" do 8 | txn_id = 'txn_xxxxxxxxxxxxxxxxxxxxxxxx' 9 | 10 | expect { 11 | Stripe::BalanceTransaction.retrieve(txn_id) 12 | }.to raise_error { |e| 13 | expect(e).to be_a(Stripe::InvalidRequestError) 14 | expect(e.message).to eq('No such balance_transaction: ' + txn_id) 15 | } 16 | end 17 | 18 | it "retrieves a single balance transaction" do 19 | txn_id = 'txn_05RsQX2eZvKYlo2C0FRTGSSA' 20 | txn = Stripe::BalanceTransaction.retrieve(txn_id) 21 | 22 | expect(txn).to be_a(Stripe::BalanceTransaction) 23 | expect(txn.id).to eq(txn_id) 24 | end 25 | 26 | describe "listing balance transactions" do 27 | 28 | it "retrieves all balance transactions" do 29 | disputes = Stripe::BalanceTransaction.list 30 | 31 | expect(disputes.count).to eq(10) 32 | expect(disputes.map &:id).to include('txn_05RsQX2eZvKYlo2C0FRTGSSA','txn_15RsQX2eZvKYlo2C0ERTYUIA', 'txn_25RsQX2eZvKYlo2C0ZXCVBNM', 'txn_35RsQX2eZvKYlo2C0QAZXSWE', 'txn_45RsQX2eZvKYlo2C0EDCVFRT', 'txn_55RsQX2eZvKYlo2C0OIKLJUY', 'txn_65RsQX2eZvKYlo2C0ASDFGHJ', 'txn_75RsQX2eZvKYlo2C0EDCXSWQ', 'txn_85RsQX2eZvKYlo2C0UJMCDET', 'txn_95RsQX2eZvKYlo2C0EDFRYUI') 33 | end 34 | 35 | end 36 | 37 | it 'retrieves balance transactions for an automated transfer' do 38 | transfer_id = Stripe::Transfer.create({ amount: 2730, currency: "usd" }) 39 | 40 | # verify transfer currently has no balance transactions 41 | transfer_transactions = Stripe::BalanceTransaction.list({transfer: transfer_id}) 42 | expect(transfer_transactions.count).to eq(0) 43 | 44 | # verify we can create a new balance transaction associated with the transfer 45 | new_txn_id = stripe_helper.upsert_stripe_object(:balance_transaction, {amount: 12300, transfer: transfer_id}) 46 | new_txn = Stripe::BalanceTransaction.retrieve(new_txn_id) 47 | expect(new_txn).to be_a(Stripe::BalanceTransaction) 48 | expect(new_txn.amount).to eq(12300) 49 | # although transfer was specified as an attribute on the balance_transaction, it should not be returned in the object 50 | expect{new_txn.transfer}.to raise_error(NoMethodError) 51 | 52 | # verify we can update an existing balance transaction to associate with the transfer 53 | existing_txn_id = 'txn_05RsQX2eZvKYlo2C0FRTGSSA' 54 | existing_txn = Stripe::BalanceTransaction.retrieve(existing_txn_id) 55 | stripe_helper.upsert_stripe_object(:balance_transaction, {id: existing_txn_id, transfer: transfer_id}) 56 | 57 | # now verify that only these balance transactions are retrieved with the transfer 58 | transfer_transactions = Stripe::BalanceTransaction.list({transfer: transfer_id}) 59 | expect(transfer_transactions.count).to eq(2) 60 | expect(transfer_transactions.map &:id).to include(new_txn_id, existing_txn_id) 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/charge.dispute.funds_reinstated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.dispute.funds_reinstated", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2017-12-14", 10 | "data": { 11 | "object": { 12 | "id": "dp_00000000000000", 13 | "object": "dispute", 14 | "amount": 25000, 15 | "balance_transaction": "txn_00000000000000", 16 | "balance_transactions": [ 17 | { 18 | "id": "txn_1Bl3mMGWCopOTFn18p8iALq8", 19 | "object": "balance_transaction", 20 | "amount": -25000, 21 | "available_on": 1516233600, 22 | "created": 1516145022, 23 | "currency": "usd", 24 | "description": "Chargeback withdrawal for ch_1Bl3mKGWCopOTFn1LKoN557r", 25 | "exchange_rate": null, 26 | "fee": 1500, 27 | "fee_details": [ 28 | { 29 | "amount": 1500, 30 | "application": null, 31 | "currency": "usd", 32 | "description": "Dispute fee", 33 | "type": "stripe_fee" 34 | } 35 | ], 36 | "net": -26500, 37 | "source": "dp_1Bl3mMGWCopOTFn1jpVaJDrU", 38 | "status": "pending", 39 | "type": "adjustment" 40 | } 41 | ], 42 | "charge": "ch_00000000000000", 43 | "created": 1516145022, 44 | "currency": "usd", 45 | "evidence": { 46 | "access_activity_log": null, 47 | "billing_address": null, 48 | "cancellation_policy": null, 49 | "cancellation_policy_disclosure": null, 50 | "cancellation_rebuttal": null, 51 | "customer_communication": null, 52 | "customer_email_address": "amitree.apu@gmail.com", 53 | "customer_name": "amitree.apu@gmail.com", 54 | "customer_purchase_ip": "157.131.133.10", 55 | "customer_signature": null, 56 | "duplicate_charge_documentation": null, 57 | "duplicate_charge_explanation": null, 58 | "duplicate_charge_id": null, 59 | "product_description": null, 60 | "receipt": null, 61 | "refund_policy": null, 62 | "refund_policy_disclosure": null, 63 | "refund_refusal_explanation": null, 64 | "service_date": null, 65 | "service_documentation": null, 66 | "shipping_address": null, 67 | "shipping_carrier": null, 68 | "shipping_date": null, 69 | "shipping_documentation": null, 70 | "shipping_tracking_number": null, 71 | "uncategorized_file": null, 72 | "uncategorized_text": null 73 | }, 74 | "evidence_details": { 75 | "due_by": 1517529599, 76 | "has_evidence": false, 77 | "past_due": false, 78 | "submission_count": 0 79 | }, 80 | "is_charge_refundable": false, 81 | "livemode": false, 82 | "metadata": { 83 | }, 84 | "reason": "fraudulent", 85 | "status": "needs_response" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/charge.dispute.funds_withdrawn.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.dispute.funds_withdrawn", 6 | "object": "event", 7 | "request": null, 8 | "pending_webhooks": 1, 9 | "api_version": "2017-12-14", 10 | "data": { 11 | "object": { 12 | "id": "dp_00000000000000", 13 | "object": "dispute", 14 | "amount": 25000, 15 | "balance_transaction": "txn_00000000000000", 16 | "balance_transactions": [ 17 | { 18 | "id": "txn_1Bl3mMGWCopOTFn18p8iALq8", 19 | "object": "balance_transaction", 20 | "amount": -25000, 21 | "available_on": 1516233600, 22 | "created": 1516145022, 23 | "currency": "usd", 24 | "description": "Chargeback withdrawal for ch_1Bl3mKGWCopOTFn1LKoN557r", 25 | "exchange_rate": null, 26 | "fee": 1500, 27 | "fee_details": [ 28 | { 29 | "amount": 1500, 30 | "application": null, 31 | "currency": "usd", 32 | "description": "Dispute fee", 33 | "type": "stripe_fee" 34 | } 35 | ], 36 | "net": -26500, 37 | "source": "dp_1Bl3mMGWCopOTFn1jpVaJDrU", 38 | "status": "pending", 39 | "type": "adjustment" 40 | } 41 | ], 42 | "charge": "ch_00000000000000", 43 | "created": 1516145022, 44 | "currency": "usd", 45 | "evidence": { 46 | "access_activity_log": null, 47 | "billing_address": null, 48 | "cancellation_policy": null, 49 | "cancellation_policy_disclosure": null, 50 | "cancellation_rebuttal": null, 51 | "customer_communication": null, 52 | "customer_email_address": "amitree.apu@gmail.com", 53 | "customer_name": "amitree.apu@gmail.com", 54 | "customer_purchase_ip": "157.131.133.10", 55 | "customer_signature": null, 56 | "duplicate_charge_documentation": null, 57 | "duplicate_charge_explanation": null, 58 | "duplicate_charge_id": null, 59 | "product_description": null, 60 | "receipt": null, 61 | "refund_policy": null, 62 | "refund_policy_disclosure": null, 63 | "refund_refusal_explanation": null, 64 | "service_date": null, 65 | "service_documentation": null, 66 | "shipping_address": null, 67 | "shipping_carrier": null, 68 | "shipping_date": null, 69 | "shipping_documentation": null, 70 | "shipping_tracking_number": null, 71 | "uncategorized_file": null, 72 | "uncategorized_text": null 73 | }, 74 | "evidence_details": { 75 | "due_by": 1517529599, 76 | "has_evidence": false, 77 | "past_due": false, 78 | "submission_count": 0 79 | }, 80 | "is_charge_refundable": false, 81 | "livemode": false, 82 | "metadata": { 83 | }, 84 | "reason": "fraudulent", 85 | "status": "needs_response" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/coupon_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Coupon API' do 4 | context 'create coupon' do 5 | let(:coupon) { stripe_helper.create_coupon } 6 | 7 | it 'creates a stripe coupon', live: true do 8 | expect(coupon.id).to eq('10BUCKS') 9 | expect(coupon.amount_off).to eq(1000) 10 | 11 | expect(coupon.currency).to eq('usd') 12 | expect(coupon.max_redemptions).to eq(100) 13 | expect(coupon.metadata.to_hash).to eq( { :created_by => 'admin_acct_1' } ) 14 | expect(coupon.duration).to eq('once') 15 | end 16 | it 'stores a created stripe coupon in memory' do 17 | coupon 18 | 19 | data = test_data_source(:coupons) 20 | 21 | expect(data[coupon.id]).to_not be_nil 22 | expect(data[coupon.id][:amount_off]).to eq(1000) 23 | end 24 | it 'fails when a coupon is created without a duration' do 25 | expect { Stripe::Coupon.create(id: '10PERCENT') }.to raise_error {|e| 26 | expect(e).to be_a(Stripe::InvalidRequestError) 27 | expect(e.message).to match /duration/ 28 | } 29 | end 30 | it 'fails when a coupon is created without a currency when amount_off is specified' do 31 | expect { Stripe::Coupon.create(id: '10OFF', duration: 'once', amount_off: 1000) }.to raise_error {|e| 32 | expect(e).to be_a(Stripe::InvalidRequestError) 33 | expect(e.message).to match /You must pass currency when passing amount_off/ 34 | } 35 | end 36 | end 37 | 38 | context 'retrieve coupon', live: true do 39 | let(:coupon1) { stripe_helper.create_coupon } 40 | let(:coupon2) { stripe_helper.create_coupon(id: '11BUCKS', amount_off: 3000) } 41 | 42 | it 'retrieves a stripe coupon' do 43 | coupon1 44 | 45 | coupon = Stripe::Coupon.retrieve(coupon1.id) 46 | 47 | expect(coupon.id).to eq(coupon1.id) 48 | expect(coupon.amount_off).to eq(coupon1.amount_off) 49 | end 50 | it 'retrieves all coupons' do 51 | stripe_helper.delete_all_coupons 52 | 53 | coupon1 54 | coupon2 55 | 56 | all = Stripe::Coupon.list 57 | 58 | expect(all.count).to eq(2) 59 | expect(all.map &:id).to include('10BUCKS', '11BUCKS') 60 | expect(all.map &:amount_off).to include(1000, 3000) 61 | end 62 | it "cannot retrieve a stripe coupon that doesn't exist" do 63 | expect { Stripe::Coupon.retrieve('nope') }.to raise_error {|e| 64 | expect(e).to be_a Stripe::InvalidRequestError 65 | expect(e.param).to eq('coupon') 66 | expect(e.http_status).to eq(404) 67 | } 68 | end 69 | end 70 | 71 | context 'Delete coupon', live: true do 72 | it 'deletes a stripe coupon' do 73 | original = stripe_helper.create_coupon 74 | coupon = Stripe::Coupon.retrieve(original.id) 75 | 76 | coupon.delete 77 | 78 | expect { Stripe::Coupon.retrieve(coupon.id) }.to raise_error {|e| 79 | expect(e).to be_a Stripe::InvalidRequestError 80 | expect(e.param).to eq('coupon') 81 | expect(e.http_status).to eq(404) 82 | } 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/accounts.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Accounts 4 | VALID_START_YEAR = 2009 5 | 6 | def Accounts.included(klass) 7 | klass.add_handler 'post /v1/accounts', :new_account 8 | klass.add_handler 'get /v1/account', :get_account 9 | klass.add_handler 'get /v1/accounts/(.*)', :get_account 10 | klass.add_handler 'post /v1/accounts/(.*)', :update_account 11 | klass.add_handler 'get /v1/accounts', :list_accounts 12 | klass.add_handler 'post /oauth/deauthorize',:deauthorize 13 | end 14 | 15 | def new_account(route, method_url, params, headers) 16 | params[:id] ||= new_id('acct') 17 | route =~ method_url 18 | accounts[params[:id]] ||= Data.mock_account(params) 19 | end 20 | 21 | def get_account(route, method_url, params, headers) 22 | route =~ method_url 23 | init_account 24 | id = $1 || accounts.keys[0] 25 | assert_existence :account, id, accounts[id] 26 | end 27 | 28 | def update_account(route, method_url, params, headers) 29 | route =~ method_url 30 | account = assert_existence :account, $1, accounts[$1] 31 | account.merge!(params) 32 | if blank_value?(params[:tos_acceptance], :date) 33 | raise Stripe::InvalidRequestError.new("Invalid integer: ", "tos_acceptance[date]", http_status: 400) 34 | elsif params[:tos_acceptance] && params[:tos_acceptance][:date] 35 | validate_acceptance_date(params[:tos_acceptance][:date]) 36 | end 37 | account 38 | end 39 | 40 | def list_accounts(route, method_url, params, headers) 41 | init_account 42 | Data.mock_list_object(accounts.values, params) 43 | end 44 | 45 | def deauthorize(route, method_url, params, headers) 46 | init_account 47 | route =~ method_url 48 | Stripe::StripeObject.construct_from(:stripe_user_id => params[:stripe_user_id]) 49 | end 50 | 51 | private 52 | 53 | def init_account 54 | if accounts == {} 55 | acc = Data.mock_account 56 | accounts[acc[:id]] = acc 57 | end 58 | end 59 | 60 | # Checks if setting a blank value 61 | # 62 | # returns true if the key is included in the hash 63 | # and its value is empty or nil 64 | def blank_value?(hash, key) 65 | if hash.key?(key) 66 | value = hash[key] 67 | return true if value.nil? || "" == value 68 | end 69 | false 70 | end 71 | 72 | def validate_acceptance_date(unix_date) 73 | unix_now = Time.now.strftime("%s").to_i 74 | formatted_date = Time.at(unix_date) 75 | 76 | return if formatted_date.year >= VALID_START_YEAR && unix_now >= unix_date 77 | 78 | raise Stripe::InvalidRequestError.new( 79 | "ToS acceptance date is not valid. Dates are expected to be integers, measured in seconds, not in the future, and after 2009", 80 | "tos_acceptance[date]", 81 | http_status: 400 82 | ) 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/instance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_stripe_examples 3 | 4 | describe StripeMock::Instance do 5 | 6 | let(:stripe_helper) { StripeMock.create_test_helper } 7 | 8 | it_behaves_like_stripe do 9 | def test_data_source(type) 10 | StripeMock.instance.send(type) 11 | end 12 | end 13 | 14 | before { StripeMock.start } 15 | after { StripeMock.stop } 16 | 17 | it "handles both string and symbol hash keys" do 18 | symbol_params = stripe_helper.create_product_params( 19 | :name => "Symbol Product", 20 | "type" => "service" 21 | ) 22 | res, api_key = StripeMock.instance.mock_request('post', '/v1/products', api_key: 'api_key', params: symbol_params) 23 | expect(res.data[:name]).to eq('Symbol Product') 24 | expect(res.data[:type]).to eq('service') 25 | end 26 | 27 | it "exits gracefully on an unrecognized handler url" do 28 | dummy_params = { 29 | "id" => "str_12345", 30 | "name" => "PLAN" 31 | } 32 | 33 | expect { res, api_key = StripeMock.instance.mock_request('post', '/v1/unrecongnized_method', api_key: 'api_key', params: dummy_params) }.to_not raise_error 34 | end 35 | 36 | it "can toggle debug" do 37 | StripeMock.toggle_debug(true) 38 | expect(StripeMock.instance.debug).to eq(true) 39 | StripeMock.toggle_debug(false) 40 | expect(StripeMock.instance.debug).to eq(false) 41 | end 42 | 43 | it "should toggle off debug when mock session ends" do 44 | StripeMock.toggle_debug(true) 45 | 46 | StripeMock.stop 47 | expect(StripeMock.instance).to be_nil 48 | 49 | StripeMock.start 50 | expect(StripeMock.instance.debug).to eq(false) 51 | end 52 | 53 | it "can set a conversion rate" do 54 | StripeMock.set_conversion_rate(1.25) 55 | expect(StripeMock.instance.conversion_rate).to eq(1.25) 56 | end 57 | 58 | it "allows non-usd default currency" do 59 | pending("Stripe::Plan requires currency param - how can we test this?") 60 | old_default_currency = StripeMock.default_currency 61 | plan = begin 62 | StripeMock.default_currency = "jpy" 63 | Stripe::Plan.create(interval: 'month') 64 | ensure 65 | StripeMock.default_currency = old_default_currency 66 | end 67 | expect(plan.currency).to eq("jpy") 68 | end 69 | 70 | context 'when creating sources with metadata' do 71 | let(:customer) { Stripe::Customer.create(email: 'test@email.com') } 72 | let(:metadata) { { test_key: 'test_value' } } 73 | 74 | context 'for credit card' do 75 | let(:credit_card) do 76 | customer.sources.create( 77 | source: stripe_helper.generate_card_token, 78 | metadata: metadata 79 | ) 80 | end 81 | 82 | it('should save metadata') do 83 | expect(credit_card.metadata.test_key).to eq metadata[:test_key] 84 | end 85 | end 86 | 87 | context 'for bank account' do 88 | let(:bank_account) do 89 | customer.sources.create( 90 | source: stripe_helper.generate_bank_token, 91 | metadata: metadata 92 | ) 93 | end 94 | 95 | it('should save metadata') do 96 | expect(bank_account.metadata.test_key).to eq metadata[:test_key] 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/subscription_items_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Subscription Items API' do 4 | let(:stripe_helper) { StripeMock.create_test_helper } 5 | let(:product) { stripe_helper.create_product(name: 'Silver Product') } 6 | let(:plan) { stripe_helper.create_plan(product: product.id, id: 'silver_plan') } 7 | let(:plan2) { stripe_helper.create_plan(amount: 100, id: 'one_more_1_plan', product: product.id) } 8 | let(:customer) { Stripe::Customer.create(source: stripe_helper.generate_card_token) } 9 | let(:subscription) { Stripe::Subscription.create(customer: customer.id, items: [{ plan: plan.id }]) } 10 | 11 | context 'creates an item' do 12 | it 'when required params only' do 13 | item = Stripe::SubscriptionItem.create(plan: plan.id, subscription: subscription.id) 14 | 15 | expect(item.id).to match(/^test_si/) 16 | expect(item.plan.id).to eq(plan.id) 17 | expect(item.subscription).to eq(subscription.id) 18 | end 19 | it 'when no subscription params' do 20 | expect { Stripe::SubscriptionItem.create(plan: plan.id) }.to raise_error { |e| 21 | expect(e).to be_a(Stripe::InvalidRequestError) 22 | expect(e.param).to eq('subscription') 23 | expect(e.message).to eq('Missing required param: subscription.') 24 | } 25 | end 26 | it 'when no plan params' do 27 | expect { Stripe::SubscriptionItem.create(subscription: subscription.id) }.to raise_error { |e| 28 | expect(e).to be_a(Stripe::InvalidRequestError) 29 | expect(e.param).to eq('plan') 30 | expect(e.message).to eq('Missing required param: plan.') 31 | } 32 | end 33 | end 34 | 35 | context 'updates an item' do 36 | let(:item) { Stripe::SubscriptionItem.create(plan: plan.id, subscription: subscription.id, quantity: 2 ) } 37 | 38 | it 'updates plan' do 39 | updated_item = Stripe::SubscriptionItem.update(item.id, plan: plan2.id) 40 | 41 | expect(updated_item.plan.id).to eq(plan2.id) 42 | end 43 | it 'updates quantity' do 44 | updated_item = Stripe::SubscriptionItem.update(item.id, quantity: 23) 45 | 46 | expect(updated_item.quantity).to eq(23) 47 | end 48 | it 'when no existing item' do 49 | expect { Stripe::SubscriptionItem.update('some_id') }.to raise_error { |e| 50 | expect(e).to be_a(Stripe::InvalidRequestError) 51 | expect(e.param).to eq('subscription_item') 52 | expect(e.message).to eq('No such subscription_item: some_id') 53 | } 54 | end 55 | end 56 | 57 | context 'retrieves a list of items' do 58 | before do 59 | Stripe::SubscriptionItem.create(plan: plan.id, subscription: subscription.id, quantity: 2 ) 60 | Stripe::SubscriptionItem.create(plan: plan2.id, subscription: subscription.id, quantity: 20) 61 | end 62 | 63 | it 'retrieves all subscription items' do 64 | all = Stripe::SubscriptionItem.list(subscription: subscription.id) 65 | 66 | expect(all.count).to eq(2) 67 | end 68 | it 'when no subscription param' do 69 | expect { Stripe::SubscriptionItem.list }.to raise_error { |e| 70 | expect(e).to be_a(Stripe::InvalidRequestError) 71 | expect(e.param).to eq('subscription') 72 | expect(e.message).to eq('Missing required param: subscription.') 73 | } 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/setup_intent.setup_failed.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_000000000000000000000000", 3 | "object": "event", 4 | "api_version": "2020-08-27", 5 | "created": 1648320091, 6 | "data": { 7 | "object": { 8 | "id": "seti_000000000000000000000000", 9 | "object": "setup_intent", 10 | "application": null, 11 | "cancellation_reason": null, 12 | "client_secret": "seti_000000000000000000000000_secret_0000000000000000000000000000000", 13 | "created": 1648320090, 14 | "customer": null, 15 | "description": "(created by Stripe CLI)", 16 | "last_setup_error": { 17 | "code": "card_declined", 18 | "decline_code": "generic_decline", 19 | "doc_url": "https://stripe.com/docs/error-codes/card-declined", 20 | "message": "Your card was declined.", 21 | "payment_method": { 22 | "id": "pm_000000000000000000000000", 23 | "object": "payment_method", 24 | "billing_details": { 25 | "address": { 26 | "city": null, 27 | "country": null, 28 | "line1": null, 29 | "line2": null, 30 | "postal_code": null, 31 | "state": null 32 | }, 33 | "email": null, 34 | "name": null, 35 | "phone": null 36 | }, 37 | "card": { 38 | "brand": "visa", 39 | "checks": { 40 | "address_line1_check": null, 41 | "address_postal_code_check": null, 42 | "cvc_check": null 43 | }, 44 | "country": "US", 45 | "exp_month": 3, 46 | "exp_year": 2023, 47 | "fingerprint": "kMzSwhaalD1uR96R", 48 | "funding": "credit", 49 | "generated_from": null, 50 | "last4": "0002", 51 | "networks": { 52 | "available": [ 53 | "visa" 54 | ], 55 | "preferred": null 56 | }, 57 | "three_d_secure_usage": { 58 | "supported": true 59 | }, 60 | "wallet": null 61 | }, 62 | "created": 1648320090, 63 | "customer": null, 64 | "livemode": false, 65 | "metadata": { 66 | }, 67 | "type": "card" 68 | }, 69 | "type": "card_error" 70 | }, 71 | "latest_attempt": "setatt_000000000000000000000000", 72 | "livemode": false, 73 | "mandate": null, 74 | "metadata": { 75 | }, 76 | "next_action": null, 77 | "on_behalf_of": null, 78 | "payment_method": null, 79 | "payment_method_options": { 80 | "card": { 81 | "mandate_options": null, 82 | "request_three_d_secure": "automatic" 83 | } 84 | }, 85 | "payment_method_types": [ 86 | "card" 87 | ], 88 | "single_use_mandate": null, 89 | "status": "requires_payment_method", 90 | "usage": "off_session" 91 | } 92 | }, 93 | "livemode": false, 94 | "pending_webhooks": 2, 95 | "request": { 96 | "id": "req_00000000000000", 97 | "idempotency_key": "bc94842b-7374-410e-8aa2-935b8cc1250c" 98 | }, 99 | "type": "setup_intent.setup_failed" 100 | } -------------------------------------------------------------------------------- /spec/shared_stripe_examples/account_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'Account API' do 4 | describe 'retrive accounts' do 5 | it 'retrieves a stripe account', live: true do 6 | account = Stripe::Account.retrieve 7 | 8 | expect(account).to be_a Stripe::Account 9 | expect(account.id).to match /acct\_/ 10 | end 11 | it 'retrieves a specific stripe account' do 12 | account = Stripe::Account.retrieve('acct_103ED82ePvKYlo2C') 13 | 14 | expect(account).to be_a Stripe::Account 15 | expect(account.id).to match /acct\_/ 16 | end 17 | it 'retrieves all' do 18 | accounts = Stripe::Account.list 19 | 20 | expect(accounts).to be_a Stripe::ListObject 21 | expect(accounts.data.count).to satisfy { |n| n >= 1 } 22 | end 23 | end 24 | describe 'create account' do 25 | it 'creates one more account' do 26 | account = Stripe::Account.create(email: 'lol@what.com') 27 | 28 | expect(account).to be_a Stripe::Account 29 | end 30 | it 'create managed account' do 31 | account = Stripe::Account.create(managed: true, country: 'CA') 32 | 33 | # expect(account).to include(:keys) 34 | expect(account.keys).not_to be_nil 35 | expect(account.keys.secret).to match /sk_(live|test)_[\d\w]+/ 36 | expect(account.keys.publishable).to match /pk_(live|test)_[\d\w]+/ 37 | expect(account.external_accounts).not_to be_nil 38 | expect(account.external_accounts.data).to be_an Array 39 | expect(account.external_accounts.url).to match /\/v1\/accounts\/.*\/external_accounts/ 40 | end 41 | end 42 | describe 'updates account' do 43 | it 'updates account' do 44 | account = Stripe::Account.retrieve 45 | account.support_phone = '1234567' 46 | account.save 47 | 48 | account = Stripe::Account.retrieve 49 | 50 | expect(account.support_phone).to eq '1234567' 51 | end 52 | 53 | it 'raises when sending an empty tos date' do 54 | account = Stripe::Account.retrieve 55 | account.tos_acceptance.date = nil 56 | expect { 57 | account.save 58 | }.to raise_error 59 | end 60 | 61 | context 'with tos acceptance date' do 62 | let(:error_message) { "ToS acceptance date is not valid. Dates are expected to be integers, measured in seconds, not in the future, and after 2009" } 63 | 64 | it 'raises error when tos date is before 2009' do 65 | date = Date.new(2008,1,1).strftime("%s").to_i 66 | 67 | account = Stripe::Account.retrieve 68 | account.tos_acceptance.date = date 69 | 70 | expect { 71 | account.save 72 | }.to raise_error Stripe::InvalidRequestError, error_message 73 | end 74 | 75 | it 'raises error when tos date is in the future' do 76 | year = Time.now.year + 5 77 | date = Date.new(year,1,1).strftime("%s").to_i 78 | 79 | account = Stripe::Account.retrieve 80 | account.tos_acceptance.date = date 81 | 82 | expect { 83 | account.save 84 | }.to raise_error Stripe::InvalidRequestError, error_message 85 | end 86 | end 87 | end 88 | 89 | it 'deauthorizes the stripe account', live: false do 90 | account = Stripe::Account.retrieve 91 | result = account.deauthorize('CLIENT_ID') 92 | 93 | expect(result).to be_a Stripe::StripeObject 94 | expect(result[:stripe_user_id]).to eq account[:id] 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/setup_intents.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module SetupIntents 4 | ALLOWED_PARAMS = [ 5 | :confirm, 6 | :customer, 7 | :description, 8 | :metadata, 9 | :on_behalf_of, 10 | :payment_method, 11 | :payment_method_options, 12 | :payment_method_types, 13 | :return_url, 14 | :usage 15 | ] 16 | 17 | def SetupIntents.included(klass) 18 | klass.add_handler 'post /v1/setup_intents', :new_setup_intent 19 | klass.add_handler 'get /v1/setup_intents', :get_setup_intents 20 | klass.add_handler 'get /v1/setup_intents/(.*)', :get_setup_intent 21 | klass.add_handler 'post /v1/setup_intents/(.*)/confirm', :confirm_setup_intent 22 | klass.add_handler 'post /v1/setup_intents/(.*)/cancel', :cancel_setup_intent 23 | klass.add_handler 'post /v1/setup_intents/(.*)', :update_setup_intent 24 | end 25 | 26 | def new_setup_intent(route, method_url, params, headers) 27 | id = new_id('si') 28 | 29 | setup_intents[id] = Data.mock_setup_intent( 30 | params.merge( 31 | id: id 32 | ) 33 | ) 34 | 35 | setup_intents[id].clone 36 | end 37 | 38 | def update_setup_intent(route, method_url, params, headers) 39 | route =~ method_url 40 | id = $1 41 | 42 | setup_intent = assert_existence :setup_intent, id, setup_intents[id] 43 | setup_intents[id] = Util.rmerge(setup_intent, params.select { |k, v| ALLOWED_PARAMS.include?(k) }) 44 | end 45 | 46 | def get_setup_intents(route, method_url, params, headers) 47 | params[:offset] ||= 0 48 | params[:limit] ||= 10 49 | 50 | clone = setup_intents.clone 51 | 52 | if params[:customer] 53 | clone.delete_if { |k, v| v[:customer] != params[:customer] } 54 | end 55 | 56 | Data.mock_list_object(clone.values, params) 57 | end 58 | 59 | def get_setup_intent(route, method_url, params, headers) 60 | route =~ method_url 61 | setup_intent_id = $1 || params[:setup_intent] 62 | setup_intent = assert_existence :setup_intent, setup_intent_id, setup_intents[setup_intent_id] 63 | 64 | setup_intent = setup_intent.clone 65 | 66 | if params[:expand]&.include?("payment_method") 67 | setup_intent[:payment_method] = assert_existence :payment_method, setup_intent[:payment_method], payment_methods[setup_intent[:payment_method]] 68 | end 69 | 70 | setup_intent 71 | end 72 | 73 | def capture_setup_intent(route, method_url, params, headers) 74 | route =~ method_url 75 | setup_intent = assert_existence :setup_intent, $1, setup_intents[$1] 76 | 77 | setup_intent[:status] = 'succeeded' 78 | setup_intent 79 | end 80 | 81 | def confirm_setup_intent(route, method_url, params, headers) 82 | route =~ method_url 83 | setup_intent = assert_existence :setup_intent, $1, setup_intents[$1] 84 | 85 | setup_intent[:status] = 'succeeded' 86 | setup_intent 87 | end 88 | 89 | def cancel_setup_intent(route, method_url, params, headers) 90 | route =~ method_url 91 | setup_intent = assert_existence :setup_intent, $1, setup_intents[$1] 92 | 93 | setup_intent[:status] = 'canceled' 94 | setup_intent 95 | end 96 | end 97 | end 98 | end 99 | --------------------------------------------------------------------------------