├── .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 │ ├── webhooks.rb │ └── errors.rb │ ├── webhook_fixtures │ ├── account.application.deauthorized.json │ ├── plan.created.json │ ├── plan.deleted.json │ ├── plan.updated.json │ ├── coupon.created.json │ ├── coupon.deleted.json │ ├── invoiceitem.created.json │ ├── invoiceitem.deleted.json │ ├── charge.dispute.created.json │ ├── charge.dispute.closed.json │ ├── invoiceitem.updated.json │ ├── balance.available.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 │ ├── product.created.json │ ├── product.deleted.json │ ├── customer.source.created.json │ ├── customer.source.deleted.json │ ├── product.updated.json │ ├── customer.source.updated.json │ ├── customer.deleted.json │ ├── customer.discount.updated.json │ ├── checkout.session.completed.setup_mode.json │ ├── charge.succeeded.json │ ├── checkout.session.completed.payment_mode.json │ ├── charge.updated.json │ ├── customer.created.json │ ├── customer.updated.json │ ├── charge.refunded.json │ ├── customer.subscription.deleted.json │ ├── customer.subscription.created.json │ ├── customer.subscription.trial_will_end.json │ ├── invoice.created.json │ ├── invoice.updated.json │ ├── customer.subscription.updated.json │ ├── transfer.paid.json │ ├── transfer.created.json │ ├── transfer.failed.json │ ├── transfer.updated.json │ ├── charge.dispute.funds_reinstated.json │ ├── charge.dispute.funds_withdrawn.json │ ├── invoice.payment_failed.json │ └── invoice.payment_succeeded.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 │ ├── events.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 │ ├── products.rb │ ├── prices.rb │ ├── recipients.rb │ ├── external_accounts.rb │ ├── transfers.rb │ ├── sources.rb │ ├── orders.rb │ ├── accounts.rb │ ├── setup_intents.rb │ ├── tokens.rb │ └── refunds.rb │ ├── error_queue.rb │ ├── test_strategies │ ├── mock.rb │ └── live.rb │ ├── util.rb │ ├── server.rb │ ├── data │ └── list.rb │ └── client.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 │ ├── checkout_session_examples.rb │ └── dispute_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 --format documentation 2 | -------------------------------------------------------------------------------- /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 | platforms :ruby_19 do 4 | gem 'mime-types', '~> 2.6' 5 | gem 'rest-client', '~> 1.8' 6 | end 7 | 8 | group :test do 9 | gem 'rake' 10 | gem 'dotenv' 11 | end 12 | 13 | gemspec 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/plan.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "plan.created", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "interval": "month", 10 | "product": "pr_00000000000000", 11 | "amount": 100, 12 | "currency": "usd", 13 | "id": "fkx0AFo_00000000000000", 14 | "object": "plan", 15 | "livemode": false, 16 | "interval_count": 1, 17 | "trial_period_days": null 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/plan.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "plan.deleted", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "interval": "month", 10 | "product": "pr_00000000000000", 11 | "amount": 100, 12 | "currency": "usd", 13 | "id": "fkx0AFo_00000000000000", 14 | "object": "plan", 15 | "livemode": false, 16 | "interval_count": 1, 17 | "trial_period_days": null 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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/plan.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "plan.updated", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "interval": "month", 10 | "product": "pr_00000000000000", 11 | "amount": 100, 12 | "currency": "usd", 13 | "id": "fkx0AFo_00000000000000", 14 | "object": "plan", 15 | "livemode": false, 16 | "interval_count": 1, 17 | "trial_period_days": null 18 | }, 19 | "previous_attributes": { 20 | "name": "Old name" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /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.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "invoiceitem.created", 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/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/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 | Data.mock_list_object(events.values, params) 17 | end 18 | 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/charge.dispute.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.dispute.created", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "charge": "ch_00000000000000", 10 | "amount": 1000, 11 | "created": 1381080223, 12 | "status": "needs_response", 13 | "livemode": false, 14 | "currency": "usd", 15 | "object": "dispute", 16 | "reason": "general", 17 | "balance_transaction": "txn_00000000000000", 18 | "evidence_due_by": 1382745599, 19 | "evidence": null 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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/invoiceitem.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "invoiceitem.updated", 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 | "previous_attributes": { 21 | "amount": 2121 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/balance.available.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "balance.available", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "pending": [ 10 | { 11 | "amount": 2217, 12 | "currency": "usd" 13 | } 14 | ], 15 | "available": [ 16 | { 17 | "amount": 0, 18 | "currency": "usd" 19 | } 20 | ], 21 | "instant_available": [ 22 | { 23 | "amount": 0, 24 | "currency": "usd" 25 | } 26 | ], 27 | "livemode": false, 28 | "object": "balance" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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/product.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "product.created", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "prod_00000000000000", 10 | "object": "product", 11 | "active": true, 12 | "attributes": [ 13 | ], 14 | "caption": null, 15 | "created": 1558795883, 16 | "deactivate_on": [ 17 | ], 18 | "description": null, 19 | "images": [ 20 | ], 21 | "livemode": false, 22 | "metadata": { 23 | }, 24 | "name": "Test Product", 25 | "package_dimensions": null, 26 | "shippable": null, 27 | "statement_descriptor": null, 28 | "type": "service", 29 | "unit_label": null, 30 | "updated": 1558795883, 31 | "url": null 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/product.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "product.deleted", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "prod_00000000000000", 10 | "object": "product", 11 | "active": true, 12 | "attributes": [ 13 | ], 14 | "caption": null, 15 | "created": 1558795883, 16 | "deactivate_on": [ 17 | ], 18 | "description": null, 19 | "images": [ 20 | ], 21 | "livemode": false, 22 | "metadata": { 23 | }, 24 | "name": "Test Product", 25 | "package_dimensions": null, 26 | "shippable": null, 27 | "statement_descriptor": null, 28 | "type": "service", 29 | "unit_label": null, 30 | "updated": 1558795883, 31 | "url": null 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.source.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.source.created", 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "product.updated", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "prod_00000000000000", 10 | "object": "product", 11 | "active": true, 12 | "attributes": [ 13 | ], 14 | "caption": null, 15 | "created": 1558795883, 16 | "deactivate_on": [ 17 | ], 18 | "description": null, 19 | "images": [ 20 | ], 21 | "livemode": false, 22 | "metadata": { 23 | }, 24 | "name": "Test Product", 25 | "package_dimensions": null, 26 | "shippable": null, 27 | "statement_descriptor": null, 28 | "type": "service", 29 | "unit_label": null, 30 | "updated": 1558795883, 31 | "url": null 32 | }, 33 | "previous_attributes": { 34 | "name": "Product Test", 35 | "updated": 1558873981 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.source.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.source.updated", 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 | "previous_attributes": 32 | { 33 | "name": "Testy Tester Jr." 34 | } 35 | } 36 | } 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/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/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 | -------------------------------------------------------------------------------- /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/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/customer.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "data": { 4 | "object": { 5 | "account_balance": 0, 6 | "active_card": { 7 | "address_city": null, 8 | "address_country": null, 9 | "address_line1": null, 10 | "address_line1_check": null, 11 | "address_line2": null, 12 | "address_state": null, 13 | "address_zip": null, 14 | "address_zip_check": null, 15 | "country": "US", 16 | "cvc_check": "pass", 17 | "exp_month": 12, 18 | "exp_year": 2013, 19 | "fingerprint": "wXWJT135mEK107G8", 20 | "last4": "4242", 21 | "name": "1231", 22 | "object": "card", 23 | "type": "Visa", 24 | "brand": "Visa", 25 | "funding": "credit" 26 | }, 27 | "created": 1359947599, 28 | "delinquent": false, 29 | "description": null, 30 | "discount": null, 31 | "email": "ajoe@mailinator.com", 32 | "id": "cus_00000000000000", 33 | "livemode": false, 34 | "object": "customer", 35 | "subscription": null 36 | } 37 | }, 38 | "id": "evt_00000000000000", 39 | "livemode": false, 40 | "object": "event", 41 | "type": "customer.deleted" 42 | } 43 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /stripe-ruby-mock.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.expand_path('../lib/stripe_mock/version', __FILE__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "stripe-ruby-mock" 7 | gem.version = StripeMock::VERSION 8 | gem.summary = %q{TDD with stripe} 9 | gem.description = %q{A drop-in library to test stripe without hitting their servers} 10 | gem.license = "MIT" 11 | gem.authors = ["Gilbert"] 12 | gem.email = "gilbertbgarza@gmail.com" 13 | gem.homepage = "https://github.com/stripe-ruby-mock/stripe-ruby-mock" 14 | gem.metadata = { 15 | "bug_tracker_uri" => "https://github.com/stripe-ruby-mock/stripe-ruby-mock/issues", 16 | "changelog_uri" => "https://github.com/stripe-ruby-mock/stripe-ruby-mock/blob/master/CHANGELOG.md", 17 | "source_code_uri" => "https://github.com/stripe-ruby-mock/stripe-ruby-mock" 18 | } 19 | 20 | gem.files = `git ls-files`.split($/) 21 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 22 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 23 | gem.require_paths = ['lib'] 24 | 25 | gem.add_dependency 'stripe', '> 5', '< 6' 26 | gem.add_dependency 'multi_json', '~> 1.0' 27 | gem.add_dependency 'dante', '>= 0.2.0' 28 | 29 | gem.add_development_dependency 'rspec', '~> 3.7.0' 30 | gem.add_development_dependency 'rubygems-tasks', '~> 0.2' 31 | gem.add_development_dependency 'thin', '~> 1.6.4' 32 | end 33 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/charge.succeeded.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.succeeded", 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": null, 50 | "dispute": null, 51 | "metadata": { 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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/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/webhook_fixtures/customer.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.created", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "object": "customer", 10 | "created": 1375148334, 11 | "id": "cus_00000000000000", 12 | "livemode": false, 13 | "description": null, 14 | "email": "bond@mailinator.com", 15 | "delinquent": true, 16 | "metadata": { 17 | }, 18 | "preferred_locales": [], 19 | "subscription": null, 20 | "discount": null, 21 | "account_balance": 0, 22 | "sources": { 23 | "object": "list", 24 | "count": 1, 25 | "url": "/v1/customers/cus_2I2AhGQOPmEFeu/cards", 26 | "data": [ 27 | { 28 | "id": "cc_2I2akIhmladin5", 29 | "object": "card", 30 | "last4": "0341", 31 | "type": "Visa", 32 | "brand": "Visa", 33 | "funding": "credit", 34 | "exp_month": 12, 35 | "exp_year": 2013, 36 | "fingerprint": "fWvZEzdbEIFF8QrK", 37 | "customer": "cus_2I2AhGQOPmEFeu", 38 | "country": "US", 39 | "name": "Johnny Goodman", 40 | "address_line1": null, 41 | "address_line2": null, 42 | "address_city": null, 43 | "address_state": null, 44 | "address_zip": null, 45 | "address_country": null, 46 | "cvc_check": "pass", 47 | "address_line1_check": null, 48 | "address_zip_check": null 49 | } 50 | ] 51 | }, 52 | "default_card": "cc_2I2akIhmladin5" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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/customer.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.updated", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "object": "customer", 10 | "created": 1375148334, 11 | "id": "cus_00000000000000", 12 | "livemode": false, 13 | "description": null, 14 | "email": "bond@mailinator.com", 15 | "delinquent": true, 16 | "metadata": { 17 | }, 18 | "preferred_locales": [], 19 | "subscription": null, 20 | "discount": null, 21 | "account_balance": 0, 22 | "sources": { 23 | "object": "list", 24 | "count": 1, 25 | "url": "/v1/customers/cus_2I2AhGQOPmEFeu/cards", 26 | "data": [ 27 | { 28 | "id": "cc_2I2akIhmladin5", 29 | "object": "card", 30 | "last4": "0341", 31 | "type": "Visa", 32 | "brand": "Visa", 33 | "funding": "credit", 34 | "exp_month": 12, 35 | "exp_year": 2013, 36 | "fingerprint": "fWvZEzdbEIFF8QrK", 37 | "customer": "cus_2I2AhGQOPmEFeu", 38 | "country": "US", 39 | "name": "Johnny Goodman", 40 | "address_line1": null, 41 | "address_line2": null, 42 | "address_city": null, 43 | "address_state": null, 44 | "address_zip": null, 45 | "address_country": null, 46 | "cvc_check": "pass", 47 | "address_line1_check": null, 48 | "address_zip_check": null 49 | } 50 | ] 51 | }, 52 | "default_source": "cc_2I2akIhmladin5" 53 | }, 54 | "previous_attributes": { 55 | "description": "Old description" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/charge.refunded.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "charge.refunded", 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": true, 17 | "source": { 18 | "id": "cc_00000000000000", 19 | "object": "card", 20 | "last4": "4242", 21 | "type": "Visa", 22 | "brand": "Visa", 23 | "funding": "credit", 24 | "exp_month": 12, 25 | "exp_year": 2013, 26 | "fingerprint": "wXWJT135mEK107G8", 27 | "customer": "cus_00000000000000", 28 | "country": "US", 29 | "name": "Actual Nothing", 30 | "address_line1": null, 31 | "address_line2": null, 32 | "address_city": null, 33 | "address_state": null, 34 | "address_zip": null, 35 | "address_country": null, 36 | "cvc_check": null, 37 | "address_line1_check": null, 38 | "address_zip_check": null 39 | }, 40 | "captured": true, 41 | "refunds": { 42 | "object": "list", 43 | "total_count": 1, 44 | "has_more": false, 45 | "data": [ 46 | { 47 | "amount": 1000, 48 | "currency": "usd", 49 | "created": 1381080103, 50 | "object": "refund", 51 | "balance_transaction": "txn_2hkjgg43ucu7K1", 52 | "id": "re_00000000000000" 53 | } 54 | ] 55 | }, 56 | "balance_transaction": "txn_00000000000000", 57 | "failure_message": null, 58 | "failure_code": null, 59 | "amount_refunded": 1000, 60 | "customer": "cus_00000000000000", 61 | "invoice": "in_00000000000000", 62 | "description": null, 63 | "dispute": null, 64 | "metadata": { 65 | }, 66 | "fee": 0 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /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 | 5 | it "generates a bank token with default values" do 6 | bank_token = StripeMock.generate_bank_token 7 | tokens = test_data_source(:bank_tokens) 8 | expect(tokens[bank_token]).to_not be_nil 9 | expect(tokens[bank_token][:bank_name]).to eq("STRIPEMOCK TEST BANK") 10 | expect(tokens[bank_token][:last4]).to eq("6789") 11 | end 12 | 13 | it "generates a bank token with an associated account in memory" do 14 | bank_token = StripeMock.generate_bank_token( 15 | :bank_name => "Memory Bank", 16 | :last4 => "7171" 17 | ) 18 | tokens = test_data_source(:bank_tokens) 19 | expect(tokens[bank_token]).to_not be_nil 20 | expect(tokens[bank_token][:bank_name]).to eq("Memory Bank") 21 | expect(tokens[bank_token][:last4]).to eq("7171") 22 | end 23 | 24 | it "creates a token whose id begins with test_btok" do 25 | bank_token = StripeMock.generate_bank_token({ 26 | :last4 => "1212" 27 | }) 28 | expect(bank_token).to match /^test_btok/ 29 | end 30 | 31 | it "assigns the generated bank account to a new recipient" do 32 | bank_token = StripeMock.generate_bank_token( 33 | :bank_name => "Bank Token Mocking", 34 | :last4 => "7777" 35 | ) 36 | 37 | recipient = Stripe::Recipient.create({ 38 | name: "Fred Flinstone", 39 | type: "individual", 40 | email: 'blah@domain.co', 41 | bank_account: bank_token 42 | }) 43 | expect(recipient.active_account.last4).to eq("7777") 44 | expect(recipient.active_account.bank_name).to eq("Bank Token Mocking") 45 | end 46 | 47 | it "retrieves a created token" do 48 | bank_token = StripeMock.generate_bank_token( 49 | :bank_name => "Cha-ching Banking", 50 | :last4 => "3939" 51 | ) 52 | token = Stripe::Token.retrieve(bank_token) 53 | 54 | expect(token.id).to eq(bank_token) 55 | expect(token.bank_account.last4).to eq("3939") 56 | expect(token.bank_account.bank_name).to eq("Cha-ching Banking") 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.subscription.deleted.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.subscription.deleted", 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": 1381080564, 52 | "status": "canceled", 53 | "customer": "cus_00000000000000", 54 | "cancel_at_period_end": false, 55 | "current_period_start": 1381080564, 56 | "current_period_end": 1383758964, 57 | "ended_at": 1381021514, 58 | "trial_start": null, 59 | "trial_end": null, 60 | "canceled_at": null, 61 | "quantity": 1, 62 | "application_fee_percent": null 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/customer.subscription.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.subscription.created", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "su_00000000000000", 10 | "items": { 11 | "object": "list", 12 | "data": [ 13 | { 14 | "id": "si_00000000000000", 15 | "object": "subscription_item", 16 | "created": 1497881783, 17 | "plan": { 18 | "interval": "month", 19 | "product": "pr_00000000000000", 20 | "amount": 100, 21 | "currency": "usd", 22 | "id": "fkx0AFo_00000000000000", 23 | "object": "plan", 24 | "livemode": false, 25 | "interval_count": 1, 26 | "trial_period_days": null, 27 | "metadata": {} 28 | }, 29 | "quantity": 1 30 | }, 31 | { 32 | "id": "si_00000000000001", 33 | "object": "subscription_item", 34 | "created": 1497881788, 35 | "plan": { 36 | "interval": "month", 37 | "product": "pr_00000000000001", 38 | "amount": 200, 39 | "currency": "eur", 40 | "id": "fkx0AFo_00000000000001", 41 | "object": "plan", 42 | "livemode": false, 43 | "interval_count": 1, 44 | "trial_period_days": null, 45 | "metadata": {} 46 | }, 47 | "quantity": 5 48 | } 49 | ] 50 | }, 51 | "object": "subscription", 52 | "start": 1381080557, 53 | "status": "active", 54 | "customer": "cus_00000000000000", 55 | "cancel_at_period_end": false, 56 | "current_period_start": 1381080557, 57 | "current_period_end": 1383758957, 58 | "ended_at": null, 59 | "trial_start": null, 60 | "trial_end": null, 61 | "canceled_at": null, 62 | "quantity": 1, 63 | "application_fee_percent": null 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /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.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "invoice.created", 6 | "status": "paid", 7 | "object": "event", 8 | "data": { 9 | "object": { 10 | "created": 1380674206, 11 | "id": "in_00000000000000", 12 | "period_start": 1378082075, 13 | "period_end": 1380674075, 14 | "lines": { 15 | "count": 1, 16 | "object": "list", 17 | "url": "/v1/invoices/in_00000000000000/lines", 18 | "data": [ 19 | { 20 | "id": "su_2hksGtIPylSBg2", 21 | "object": "line_item", 22 | "type": "subscription", 23 | "livemode": true, 24 | "amount": 100, 25 | "currency": "usd", 26 | "proration": false, 27 | "period": { 28 | "start": 1383759042, 29 | "end": 1386351042 30 | }, 31 | "quantity": 1, 32 | "plan": { 33 | "interval": "month", 34 | "amount": 100, 35 | "currency": "usd", 36 | "id": "fkx0AFo", 37 | "object": "plan", 38 | "livemode": false, 39 | "interval_count": 1, 40 | "trial_period_days": null, 41 | "product": "pr_00000000000000", 42 | "metadata": {} 43 | }, 44 | "description": null, 45 | "metadata": null 46 | } 47 | ] 48 | }, 49 | "subtotal": 1000, 50 | "total": 1000, 51 | "customer": "cus_00000000000000", 52 | "object": "invoice", 53 | "attempted": false, 54 | "closed": true, 55 | "paid": true, 56 | "livemode": false, 57 | "attempt_count": 1, 58 | "amount_due": 1000, 59 | "currency": "usd", 60 | "starting_balance": 0, 61 | "ending_balance": 0, 62 | "next_payment_attempt": null, 63 | "charge": "ch_00000000000000", 64 | "discount": null, 65 | "application_fee": null, 66 | "subscription": "sub_00000000000000", 67 | "metadata": {}, 68 | "description": null 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /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/webhook_fixtures/invoice.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "invoice.updated", 6 | "status": "paid", 7 | "object": "event", 8 | "data": { 9 | "object": { 10 | "created": 1380674206, 11 | "id": "in_00000000000000", 12 | "period_start": 1378082075, 13 | "period_end": 1380674075, 14 | "lines": { 15 | "count": 1, 16 | "object": "list", 17 | "url": "/v1/invoices/in_00000000000000/lines", 18 | "data": [ 19 | { 20 | "id": "su_00000000000000", 21 | "object": "line_item", 22 | "type": "subscription", 23 | "livemode": true, 24 | "amount": 100, 25 | "currency": "usd", 26 | "proration": false, 27 | "period": { 28 | "start": 1383759047, 29 | "end": 1386351047 30 | }, 31 | "quantity": 1, 32 | "plan": { 33 | "interval": "month", 34 | "product": "pr_00000000000000", 35 | "amount": 100, 36 | "currency": "usd", 37 | "id": "fkx0AFo", 38 | "object": "plan", 39 | "livemode": false, 40 | "interval_count": 1, 41 | "trial_period_days": null, 42 | "metadata": {} 43 | }, 44 | "description": null, 45 | "metadata": null 46 | } 47 | ] 48 | }, 49 | "subtotal": 1000, 50 | "total": 1000, 51 | "customer": "cus_00000000000000", 52 | "object": "invoice", 53 | "attempted": true, 54 | "closed": true, 55 | "paid": true, 56 | "livemode": false, 57 | "attempt_count": 1, 58 | "amount_due": 1000, 59 | "currency": "usd", 60 | "starting_balance": 0, 61 | "ending_balance": 0, 62 | "next_payment_attempt": null, 63 | "charge": "ch_00000000000000", 64 | "discount": null, 65 | "application_fee": null, 66 | "subscription": "sub_00000000000000", 67 | "metadata": {}, 68 | "description": null 69 | }, 70 | "previous_attributes": { 71 | "lines": [] 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/customer.subscription.updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "customer.subscription.updated", 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": 1381080561, 52 | "status": "active", 53 | "customer": "cus_00000000000000", 54 | "cancel_at_period_end": false, 55 | "current_period_start": 1381080561, 56 | "current_period_end": 1383758961, 57 | "ended_at": null, 58 | "trial_start": null, 59 | "trial_end": null, 60 | "canceled_at": null, 61 | "quantity": 1, 62 | "application_fee_percent": null 63 | }, 64 | "previous_attributes": { 65 | "plan": { 66 | "interval": "month", 67 | "product": "pr_00000000000002", 68 | "amount": 100, 69 | "currency": "usd", 70 | "id": "OLD_PLAN_ID", 71 | "object": "plan", 72 | "livemode": false, 73 | "interval_count": 1, 74 | "trial_period_days": null 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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_card).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/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 | -------------------------------------------------------------------------------- /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); StripeMock.instance.send(type); end 10 | end 11 | 12 | before { StripeMock.start } 13 | after { StripeMock.stop } 14 | 15 | it "handles both string and symbol hash keys" do 16 | symbol_params = stripe_helper.create_product_params( 17 | :name => "Symbol Product", 18 | "type" => "service" 19 | ) 20 | res, api_key = StripeMock.instance.mock_request('post', '/v1/products', api_key: 'api_key', params: symbol_params) 21 | expect(res.data[:name]).to eq('Symbol Product') 22 | expect(res.data[:type]).to eq('service') 23 | end 24 | 25 | it "exits gracefully on an unrecognized handler url" do 26 | dummy_params = { 27 | "id" => "str_12345", 28 | "name" => "PLAN" 29 | } 30 | 31 | expect { res, api_key = StripeMock.instance.mock_request('post', '/v1/unrecongnized_method', api_key: 'api_key', params: dummy_params) }.to_not raise_error 32 | end 33 | 34 | it "can toggle debug" do 35 | StripeMock.toggle_debug(true) 36 | expect(StripeMock.instance.debug).to eq(true) 37 | StripeMock.toggle_debug(false) 38 | expect(StripeMock.instance.debug).to eq(false) 39 | end 40 | 41 | it "should toggle off debug when mock session ends" do 42 | StripeMock.toggle_debug(true) 43 | 44 | StripeMock.stop 45 | expect(StripeMock.instance).to be_nil 46 | 47 | StripeMock.start 48 | expect(StripeMock.instance.debug).to eq(false) 49 | end 50 | 51 | it "can set a conversion rate" do 52 | StripeMock.set_conversion_rate(1.25) 53 | expect(StripeMock.instance.conversion_rate).to eq(1.25) 54 | end 55 | 56 | it "allows non-usd default currency" do 57 | pending("Stripe::Plan requires currency param - how can we test this?") 58 | old_default_currency = StripeMock.default_currency 59 | plan = begin 60 | StripeMock.default_currency = "jpy" 61 | Stripe::Plan.create(interval: 'month') 62 | ensure 63 | StripeMock.default_currency = old_default_currency 64 | end 65 | expect(plan.currency).to eq("jpy") 66 | end 67 | 68 | context 'when creating sources with metadata' do 69 | let(:customer) { Stripe::Customer.create(email: 'test@email.com') } 70 | let(:metadata) { { test_key: 'test_value' } } 71 | 72 | context 'for credit card' do 73 | let(:credit_card) do 74 | customer.sources.create( 75 | source: stripe_helper.generate_card_token, 76 | metadata: metadata 77 | ) 78 | end 79 | 80 | it('should save metadata') do 81 | expect(credit_card.metadata.test_key).to eq metadata[:test_key] 82 | end 83 | end 84 | 85 | context 'for bank account' do 86 | let(:bank_account) do 87 | customer.sources.create( 88 | source: stripe_helper.generate_bank_token, 89 | metadata: metadata 90 | ) 91 | end 92 | 93 | it('should save metadata') do 94 | expect(bank_account.metadata.test_key).to eq metadata[:test_key] 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /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/api/webhooks.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | 3 | def self.mock_webhook_payload(type, params = {}) 4 | 5 | fixture_file = File.join(@webhook_fixture_path, "#{type}.json") 6 | 7 | unless File.exists?(fixture_file) 8 | unless Webhooks.event_list.include?(type) 9 | raise UnsupportedRequestError.new "Unsupported webhook event `#{type}` (Searched in #{@webhook_fixture_path})" 10 | end 11 | fixture_file = File.join(@webhook_fixture_fallback_path, "#{type}.json") 12 | end 13 | 14 | json = MultiJson.load File.read(fixture_file) 15 | 16 | json = Stripe::Util.symbolize_names(json) 17 | params = Stripe::Util.symbolize_names(params) 18 | json[:account] = params.delete(:account) if params.key?(:account) 19 | json[:data][:object] = Util.rmerge(json[:data][:object], params) 20 | json.delete(:id) 21 | json[:created] = params[:created] || Time.now.to_i 22 | 23 | if @state == 'local' 24 | event_data = instance.generate_webhook_event(json) 25 | elsif @state == 'remote' 26 | event_data = client.generate_webhook_event(json) 27 | else 28 | raise UnstartedStateError 29 | end 30 | event_data 31 | end 32 | 33 | def self.mock_webhook_event(type, params={}) 34 | Stripe::Event.construct_from(mock_webhook_payload(type, params)) 35 | end 36 | 37 | module Webhooks 38 | def self.event_list 39 | @__list = [ 40 | 'account.updated', 41 | 'account.application.deauthorized', 42 | 'account.external_account.created', 43 | 'account.external_account.updated', 44 | 'account.external_account.deleted', 45 | 'balance.available', 46 | 'charge.succeeded', 47 | 'charge.updated', 48 | 'charge.failed', 49 | 'charge.refunded', 50 | 'charge.dispute.created', 51 | 'charge.dispute.updated', 52 | 'charge.dispute.closed', 53 | 'charge.dispute.funds_reinstated', 54 | 'charge.dispute.funds_withdrawn', 55 | 'checkout.session.completed.payment_mode', 56 | 'checkout.session.completed.setup_mode', 57 | 'customer.source.created', 58 | 'customer.source.deleted', 59 | 'customer.source.updated', 60 | 'customer.created', 61 | 'customer.updated', 62 | 'customer.deleted', 63 | 'customer.subscription.created', 64 | 'customer.subscription.updated', 65 | 'customer.subscription.deleted', 66 | 'customer.subscription.trial_will_end', 67 | 'customer.discount.created', 68 | 'customer.discount.updated', 69 | 'customer.discount.deleted', 70 | 'invoice.created', 71 | 'invoice.updated', 72 | 'invoice.payment_succeeded', 73 | 'invoice.payment_failed', 74 | 'invoiceitem.created', 75 | 'invoiceitem.updated', 76 | 'invoiceitem.deleted', 77 | 'payment_intent.processing', 78 | 'payment_intent.succeeded', 79 | 'payment_intent.payment_failed', 80 | 'plan.created', 81 | 'plan.updated', 82 | 'plan.deleted', 83 | 'product.created', 84 | 'product.updated', 85 | 'product.deleted', 86 | 'coupon.created', 87 | 'coupon.deleted', 88 | 'transfer.created', 89 | 'transfer.paid', 90 | 'transfer.updated', 91 | 'transfer.failed' 92 | ] 93 | end 94 | end 95 | 96 | end 97 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/stripe_mock/data/list.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module Data 3 | class List 4 | attr_reader :data, :limit, :offset, :starting_after, :ending_before, :active 5 | 6 | def initialize(data, options = {}) 7 | @data = Array(data.clone) 8 | @limit = [[options[:limit] || 10, 100].min, 1].max # restrict @limit to 1..100 9 | @starting_after = options[:starting_after] 10 | @ending_before = options[:ending_before] 11 | @active = options[:active] 12 | if contains_stripe_objects? 13 | prune_deleted_data 14 | sort_data 15 | end 16 | end 17 | 18 | def url 19 | "/v1/#{object_types}" 20 | end 21 | 22 | def to_hash 23 | { object: "list", data: data_page, url: url, has_more: has_more? } 24 | end 25 | alias_method :to_h, :to_hash 26 | 27 | def has_more? 28 | (offset + limit) < data.size 29 | end 30 | 31 | def method_missing(method_name, *args, &block) 32 | hash = to_hash 33 | 34 | if hash.keys.include?(method_name) 35 | hash[method_name] 36 | else 37 | super 38 | end 39 | end 40 | 41 | def respond_to?(method_name, priv = false) 42 | to_hash.keys.include?(method_name) || super 43 | end 44 | 45 | private 46 | 47 | def offset 48 | case 49 | when starting_after 50 | index = data.index { |datum| datum[:id] == starting_after } 51 | (index || raise("No such object id: #{starting_after}")) + 1 52 | when ending_before 53 | index = data.index { |datum| datum[:id] == ending_before } 54 | (index || raise("No such object id: #{ending_before}")) - 1 55 | else 56 | 0 57 | end 58 | end 59 | 60 | def data_page 61 | filtered_data[offset, limit] 62 | end 63 | 64 | def filtered_data 65 | filtered_data = data 66 | filtered_data = filtered_data.select { |d| d[:active] == active } unless active.nil? 67 | 68 | filtered_data 69 | end 70 | 71 | def object_types 72 | if first_object = data[0] 73 | "#{first_object.class.to_s.split('::')[-1].downcase}s" 74 | end 75 | end 76 | 77 | def contains_stripe_objects? 78 | return false if data.empty? 79 | 80 | object = data.first 81 | object.is_a?(Stripe::StripeObject) || ( 82 | object.is_a?(Hash) && [:created, :deleted].any? { |k| object.key?(k) } 83 | ) 84 | end 85 | 86 | def prune_deleted_data 87 | data.reject! do |object| 88 | (object.is_a?(Hash) && object[:deleted]) || 89 | (object.is_a?(Stripe::StripeObject) && object.deleted?) 90 | end 91 | end 92 | 93 | def sort_data 94 | # Reverse must follow sort to preserve existing test dependencies. The 95 | # alternative would be to simply reverse lhs and rhs in the comparison, 96 | # however, being a stable sort this breaks the existing dependency when 97 | # more than one record share the same `created` value. 98 | @data = data.sort { |lhs, rhs| sort_val(lhs) <=> sort_val(rhs) }.reverse 99 | end 100 | 101 | def sort_val(object) 102 | object.is_a?(Stripe::StripeObject) ? object.created : object[:created] 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/invoice.payment_failed.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "invoice.payment_failed", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "date": 1380674206, 10 | "id": "in_00000000000000", 11 | "period_start": 1378082075, 12 | "period_end": 1380674075, 13 | "lines": { 14 | "object": "list", 15 | "count": 3, 16 | "url": "/v1/invoices/in_00000000000000/lines", 17 | "data": [ 18 | { 19 | "id": "ii_00000000000000", 20 | "object": "line_item", 21 | "type": "invoiceitem", 22 | "livemode": false, 23 | "amount": 19000, 24 | "currency": "usd", 25 | "proration": true, 26 | "period": { 27 | "start": 1393765661, 28 | "end": 1393765661 29 | }, 30 | "quantity": null, 31 | "plan": null, 32 | "description": "Remaining time on Platinum after 02 Mar 2014", 33 | "metadata": {} 34 | }, 35 | { 36 | "id": "ii_00000000000001", 37 | "object": "line_item", 38 | "type": "invoiceitem", 39 | "livemode": false, 40 | "amount": -9000, 41 | "currency": "usd", 42 | "proration": true, 43 | "period": { 44 | "start": 1393765661, 45 | "end": 1393765661 46 | }, 47 | "quantity": null, 48 | "plan": null, 49 | "description": "Unused time on Gold after 05 Mar 2014", 50 | "metadata": {} 51 | }, 52 | { 53 | "id": "su_00000000000000", 54 | "object": "line_item", 55 | "type": "subscription", 56 | "livemode": false, 57 | "amount": 20000, 58 | "currency": "usd", 59 | "proration": false, 60 | "period": { 61 | "start": 1383759053, 62 | "end": 1386351053 63 | }, 64 | "quantity": 1, 65 | "plan": { 66 | "interval": "month", 67 | "product": "pr_00000000000000", 68 | "created": 1300000000, 69 | "amount": 20000, 70 | "currency": "usd", 71 | "id": "platinum", 72 | "object": "plan", 73 | "livemode": false, 74 | "interval_count": 1, 75 | "trial_period_days": null, 76 | "metadata": {} 77 | }, 78 | "description": null, 79 | "metadata": null 80 | } 81 | ] 82 | }, 83 | "subtotal": 30000, 84 | "total": 30000, 85 | "customer": "cus_00000000000000", 86 | "object": "invoice", 87 | "attempted": true, 88 | "closed": false, 89 | "paid": false, 90 | "livemode": false, 91 | "attempt_count": 1, 92 | "amount_due": 30000, 93 | "currency": "usd", 94 | "starting_balance": 0, 95 | "ending_balance": 0, 96 | "next_payment_attempt": 1380760475, 97 | "charge": "ch_00000000000000", 98 | "discount": null, 99 | "application_fee": null, 100 | "subscription": "su_00000000000000", 101 | "metadata": {}, 102 | "description": null 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/tokens.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Tokens 4 | 5 | def Tokens.included(klass) 6 | klass.add_handler 'post /v1/tokens', :create_token 7 | klass.add_handler 'get /v1/tokens/(.*)', :get_token 8 | end 9 | 10 | def create_token(route, method_url, params, headers) 11 | stripe_account = headers && headers[:stripe_account] || Stripe.api_key 12 | 13 | if params[:customer].nil? && params[:card].nil? && params[:bank_account].nil? 14 | raise Stripe::InvalidRequestError.new('You must supply either a card, customer, or bank account to create a token.', nil, http_status: 400) 15 | end 16 | 17 | cus_id = params[:customer] 18 | 19 | if cus_id && params[:source] 20 | customer = assert_existence :customer, cus_id, customers[stripe_account][cus_id] 21 | 22 | # params[:card] is an id; grab it from the db 23 | customer_card = get_card(customer, params[:source]) 24 | assert_existence :card, params[:source], customer_card 25 | elsif params[:card].is_a?(String) 26 | customer = assert_existence :customer, cus_id, customers[stripe_account][cus_id] 27 | 28 | # params[:card] is an id; grab it from the db 29 | customer_card = get_card(customer, params[:card]) 30 | assert_existence :card, params[:card], customer_card 31 | elsif params[:card] 32 | # params[:card] is a hash of cc info; "Sanitize" the card number 33 | params[:card][:fingerprint] = StripeMock::Util.fingerprint(params[:card][:number]) 34 | params[:card][:last4] = params[:card][:number][-4,4] 35 | customer_card = params[:card] 36 | elsif params[:bank_account].is_a?(String) 37 | customer = assert_existence :customer, cus_id, customers[stripe_account][cus_id] 38 | 39 | # params[:bank_account] is an id; grab it from the db 40 | bank_account = verify_bank_account(customer, params[:bank_account]) 41 | assert_existence :bank_account, params[:bank_account], bank_account 42 | elsif params[:bank_account] 43 | # params[:card] is a hash of cc info; "Sanitize" the card number 44 | bank_account = params[:bank_account] 45 | else 46 | customer = assert_existence :customer, cus_id, customers[stripe_account][cus_id] || customers[Stripe.api_key][cus_id] 47 | customer_card = get_card(customer, customer[:default_source]) 48 | end 49 | 50 | if bank_account 51 | token_id = generate_bank_token(bank_account.dup) 52 | bank_account = @bank_tokens[token_id] 53 | 54 | Data.mock_bank_account_token(params.merge :id => token_id, :bank_account => bank_account) 55 | else 56 | token_id = generate_card_token(customer_card.dup) 57 | card = @card_tokens[token_id] 58 | 59 | Data.mock_card_token(params.merge :id => token_id, :card => card) 60 | end 61 | end 62 | 63 | def get_token(route, method_url, params, headers) 64 | route =~ method_url 65 | # A Stripe token can be either a bank token or a card token 66 | bank_or_card = @bank_tokens[$1] || @card_tokens[$1] 67 | assert_existence :token, $1, bank_or_card 68 | 69 | if bank_or_card[:object] == 'card' 70 | Data.mock_card_token(:id => $1, :card => bank_or_card) 71 | elsif bank_or_card[:object] == 'bank_account' 72 | Data.mock_bank_account_token(:id => $1, :bank_account => bank_or_card) 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/checkout_session_examples.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | shared_examples "Checkout Session API" do 4 | it "creates PaymentIntent with payment mode" do 5 | line_items = [{ 6 | name: "T-shirt", 7 | quantity: 2, 8 | amount: 500, 9 | currency: "usd", 10 | }] 11 | session = Stripe::Checkout::Session.create( 12 | payment_method_types: ["card"], 13 | line_items: line_items, 14 | cancel_url: "https://example.com/cancel", 15 | success_url: "https://example.com/success" 16 | ) 17 | 18 | expect(session.payment_intent).to_not be_empty 19 | payment_intent = Stripe::PaymentIntent.retrieve(session.payment_intent) 20 | expect(payment_intent.amount).to eq(1000) 21 | expect(payment_intent.currency).to eq("usd") 22 | expect(payment_intent.customer).to eq(session.customer) 23 | end 24 | 25 | context "when creating a payment" do 26 | it "requires line_items" do 27 | expect do 28 | session = Stripe::Checkout::Session.create( 29 | customer: "customer_id", 30 | success_url: "localhost/nada", 31 | cancel_url: "localhost/nada", 32 | payment_method_types: ["card"], 33 | ) 34 | end.to raise_error(Stripe::InvalidRequestError, /line_items/i) 35 | 36 | end 37 | end 38 | 39 | it "creates SetupIntent with setup mode" do 40 | session = Stripe::Checkout::Session.create( 41 | mode: "setup", 42 | payment_method_types: ["card"], 43 | cancel_url: "https://example.com/cancel", 44 | success_url: "https://example.com/success" 45 | ) 46 | 47 | expect(session.setup_intent).to_not be_empty 48 | setup_intent = Stripe::SetupIntent.retrieve(session.setup_intent) 49 | expect(setup_intent.payment_method_types).to eq(["card"]) 50 | end 51 | 52 | context "when creating a subscription" do 53 | it "requires line_items" do 54 | expect do 55 | session = Stripe::Checkout::Session.create( 56 | customer: "customer_id", 57 | success_url: "localhost/nada", 58 | cancel_url: "localhost/nada", 59 | payment_method_types: ["card"], 60 | mode: "subscription", 61 | ) 62 | end.to raise_error(Stripe::InvalidRequestError, /line_items/i) 63 | 64 | end 65 | end 66 | 67 | context "retrieve a checkout session" do 68 | let(:checkout_session1) { stripe_helper.create_checkout_session } 69 | 70 | it "can be retrieved by id" do 71 | checkout_session1 72 | 73 | checkout_session = Stripe::Checkout::Session.retrieve(checkout_session1.id) 74 | 75 | expect(checkout_session.id).to eq(checkout_session1.id) 76 | end 77 | 78 | it "cannot retrieve a checkout session that doesn't exist" do 79 | expect { Stripe::Checkout::Session.retrieve("nope") }.to raise_error { |e| 80 | expect(e).to be_a Stripe::InvalidRequestError 81 | expect(e.param).to eq("checkout_session") 82 | expect(e.http_status).to eq(404) 83 | } 84 | end 85 | 86 | it "can expand setup_intent" do 87 | initial_session = Stripe::Checkout::Session.create( 88 | mode: "setup", 89 | cancel_url: "https://example.com", 90 | success_url: "https://example.com", 91 | payment_method_types: ["card"] 92 | ) 93 | 94 | checkout_session = Stripe::Checkout::Session.retrieve(id: initial_session.id, expand: ["setup_intent"]) 95 | 96 | expect(checkout_session.setup_intent).to be_a_kind_of(Stripe::SetupIntent) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/stripe_mock/webhook_fixtures/invoice.payment_succeeded.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": 1326853478, 3 | "livemode": false, 4 | "id": "evt_00000000000000", 5 | "type": "invoice.payment_succeeded", 6 | "object": "event", 7 | "data": { 8 | "object": { 9 | "id": "in_00000000000000", 10 | "object": "invoice", 11 | "amount_due": 999, 12 | "application_fee": null, 13 | "attempt_count": 1, 14 | "attempted": true, 15 | "charge": "ch_18EcOcLrgDIZ7iq8TaNlErVv", 16 | "closed": true, 17 | "currency": "eur", 18 | "customer": "cus_00000000000000", 19 | "created": 1464084258, 20 | "description": null, 21 | "discount": null, 22 | "ending_balance": 0, 23 | "forgiven": false, 24 | "lines": { 25 | "data": [{ 26 | "id": "sub_00000000000000", 27 | "object": "line_item", 28 | "amount": 50, 29 | "currency": "eur", 30 | "description": null, 31 | "discountable": true, 32 | "livemode": true, 33 | "metadata": {}, 34 | "period": { 35 | "start": 1500637196, 36 | "end": 1532173196 37 | }, 38 | "plan": { 39 | "id": "platinum", 40 | "object": "plan", 41 | "amount": 500, 42 | "created": 1499943145, 43 | "currency": "eur", 44 | "interval": "month", 45 | "interval_count": 1, 46 | "livemode": false, 47 | "metadata": {}, 48 | "name": "New Plan Test", 49 | "statement_descriptor": null, 50 | "trial_period_days": null 51 | }, 52 | "proration": false, 53 | "quantity": 1, 54 | "subscription": null, 55 | "subscription_item": "si_18ZfWyLrgDIZ7iq8fSlSNGIV", 56 | "type": "subscription" 57 | }, 58 | { 59 | "id": "sub_00000000000000", 60 | "object": "line_item", 61 | "amount": 50, 62 | "currency": "eur", 63 | "description": null, 64 | "discountable": true, 65 | "livemode": true, 66 | "metadata": {}, 67 | "period": { 68 | "start": 1500637196, 69 | "end": 1532173196 70 | }, 71 | "plan": { 72 | "id": "gold", 73 | "object": "plan", 74 | "amount": 300, 75 | "created": 1499943155, 76 | "currency": "eur", 77 | "interval": "month", 78 | "interval_count": 1, 79 | "livemode": false, 80 | "metadata": {}, 81 | "name": "New gold Plan Test", 82 | "statement_descriptor": null, 83 | "trial_period_days": null 84 | }, 85 | "proration": false, 86 | "quantity": 1, 87 | "subscription": null, 88 | "subscription_item": "si_18ZfWyLrgDIZ7iq8fSlSNGIV", 89 | "type": "subscription" 90 | }], 91 | "total_count": 1, 92 | "object": "list", 93 | "url": "/v1/invoices/in_18EcOcLrgDIZ7iq8zsDkunZ0/lines" 94 | }, 95 | "livemode": false, 96 | "metadata": {}, 97 | "next_payment_attempt": null, 98 | "paid": true, 99 | "period_end": 1464084258, 100 | "period_start": 1464084258, 101 | "receipt_number": null, 102 | "starting_balance": 0, 103 | "statement_descriptor": null, 104 | "subscription": "sub_00000000000000", 105 | "subtotal": 999, 106 | "tax": null, 107 | "tax_percent": null, 108 | "total": 999, 109 | "webhooks_delivered_at": 1464084258 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /spec/shared_stripe_examples/dispute_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'pp' 3 | 4 | shared_examples 'Dispute API' do 5 | 6 | let(:stripe_helper) { StripeMock.create_test_helper } 7 | 8 | it "returns an error if dispute does not exist" do 9 | dispute_id = 'dp_xxxxxxxxxxxxxxxxxxxxxxxx' 10 | 11 | expect { 12 | Stripe::Dispute.retrieve(dispute_id) 13 | }.to raise_error { |e| 14 | expect(e).to be_a(Stripe::InvalidRequestError) 15 | expect(e.message).to eq('No such dispute: ' + dispute_id) 16 | } 17 | end 18 | 19 | it "retrieves a single dispute" do 20 | dispute_id = 'dp_05RsQX2eZvKYlo2C0FRTGSSA' 21 | dispute = Stripe::Dispute.retrieve(dispute_id) 22 | 23 | expect(dispute).to be_a(Stripe::Dispute) 24 | expect(dispute.id).to eq(dispute_id) 25 | end 26 | 27 | it "updates a dispute" do 28 | dispute_id = 'dp_65RsQX2eZvKYlo2C0ASDFGHJ' 29 | dispute = Stripe::Dispute.retrieve(dispute_id) 30 | 31 | expect(dispute).to be_a(Stripe::Dispute) 32 | expect(dispute.id).to eq(dispute_id) 33 | expect(dispute.evidence.customer_name).to eq(nil) 34 | expect(dispute.evidence.product_description).to eq(nil) 35 | expect(dispute.evidence.shipping_documentation).to eq(nil) 36 | 37 | dispute.evidence = { 38 | :customer_name => 'Rebel Idealist', 39 | :product_description => 'Lorem ipsum dolor sit amet.', 40 | :shipping_documentation => 'fil_15BZxW2eZvKYlo2CvQbrn9dc', 41 | } 42 | dispute.save 43 | 44 | dispute = Stripe::Dispute.retrieve(dispute_id) 45 | 46 | expect(dispute).to be_a(Stripe::Dispute) 47 | expect(dispute.id).to eq(dispute_id) 48 | expect(dispute.evidence.customer_name).to eq('Rebel Idealist') 49 | expect(dispute.evidence.product_description).to eq('Lorem ipsum dolor sit amet.') 50 | expect(dispute.evidence.shipping_documentation).to eq('fil_15BZxW2eZvKYlo2CvQbrn9dc') 51 | end 52 | 53 | it "closes a dispute" do 54 | dispute_id = 'dp_75RsQX2eZvKYlo2C0EDCXSWQ' 55 | 56 | dispute = Stripe::Dispute.retrieve(dispute_id) 57 | 58 | expect(dispute).to be_a(Stripe::Dispute) 59 | expect(dispute.id).to eq(dispute_id) 60 | expect(dispute.status).to eq('under_review') 61 | 62 | dispute.close 63 | 64 | dispute = Stripe::Dispute.retrieve(dispute_id) 65 | 66 | expect(dispute).to be_a(Stripe::Dispute) 67 | expect(dispute.id).to eq(dispute_id) 68 | expect(dispute.status).to eq('lost') 69 | end 70 | 71 | describe "listing disputes" do 72 | 73 | it "retrieves all disputes" do 74 | disputes = Stripe::Dispute.list 75 | 76 | expect(disputes.count).to eq(10) 77 | expect(disputes.map &:id).to include('dp_05RsQX2eZvKYlo2C0FRTGSSA','dp_15RsQX2eZvKYlo2C0ERTYUIA', 'dp_25RsQX2eZvKYlo2C0ZXCVBNM', 'dp_35RsQX2eZvKYlo2C0QAZXSWE', 'dp_45RsQX2eZvKYlo2C0EDCVFRT', 'dp_55RsQX2eZvKYlo2C0OIKLJUY', 'dp_65RsQX2eZvKYlo2C0ASDFGHJ', 'dp_75RsQX2eZvKYlo2C0EDCXSWQ', 'dp_85RsQX2eZvKYlo2C0UJMCDET', 'dp_95RsQX2eZvKYlo2C0EDFRYUI') 78 | end 79 | 80 | it "retrieves disputes with a limit(3)" do 81 | disputes = Stripe::Dispute.list(limit: 3) 82 | 83 | expect(disputes.count).to eq(3) 84 | expected = ['dp_95RsQX2eZvKYlo2C0EDFRYUI','dp_85RsQX2eZvKYlo2C0UJMCDET', 'dp_75RsQX2eZvKYlo2C0EDCXSWQ'] 85 | expect(disputes.map &:id).to include(*expected) 86 | end 87 | 88 | end 89 | 90 | it "creates a dispute" do 91 | card_token = stripe_helper.generate_card_token(last4: "1123", exp_month: 11, exp_year: 2099) 92 | charge = Stripe::Charge.create(amount: 1000, currency: "usd", source: card_token) 93 | stripe_dispute_id = stripe_helper.upsert_stripe_object(:dispute, {amount: charge.amount, charge: charge.id}) 94 | stripe_dispute = Stripe::Dispute.retrieve(stripe_dispute_id) 95 | expect(stripe_dispute.charge).to eq(charge.id) 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /lib/stripe_mock/api/errors.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | def self.prepare_error(stripe_error, *handler_names) 3 | handler_names.push(:all) if handler_names.count == 0 4 | 5 | if @state == 'local' 6 | instance 7 | elsif @state == 'remote' 8 | client 9 | else 10 | raise UnstartedStateError 11 | end.error_queue.queue stripe_error, handler_names 12 | end 13 | 14 | def self.prepare_card_error(code, *handler_names) 15 | handler_names.push(:new_charge) if handler_names.count == 0 16 | 17 | error = CardErrors.build_error_for(code) 18 | if error.nil? 19 | raise StripeMockError, "Unrecognized stripe card error code: #{code}" 20 | end 21 | 22 | prepare_error error, *handler_names 23 | end 24 | 25 | module CardErrors 26 | def self.build_error_for(code) 27 | case code 28 | when :incorrect_number then build_card_error('The card number is incorrect', 'number', code: 'incorrect_number', http_status: 402) 29 | when :invalid_number then build_card_error('The card number is not a valid credit card number', 'number', code: 'invalid_number', http_status: 402) 30 | when :invalid_expiry_month then build_card_error("The card's expiration month is invalid", 'exp_month', code: 'invalid_expiry_month', http_status: 402) 31 | when :invalid_expiry_year then build_card_error("The card's expiration year is invalid", 'exp_year', code: 'invalid_expiry_year', http_status: 402) 32 | when :invalid_cvc then build_card_error("The card's security code is invalid", 'cvc', code: 'invalid_cvc', http_status: 402) 33 | when :expired_card then build_card_error('The card has expired', 'exp_month', code: 'expired_card', http_status: 402) 34 | when :incorrect_cvc then build_card_error("The card's security code is incorrect", 'cvc', code: 'incorrect_cvc', http_status: 402) 35 | when :card_declined then build_card_error('The card was declined', nil, code: 'card_declined', http_status: 402) 36 | when :missing then build_card_error('There is no card on a customer that is being charged.', nil, code: 'missing', http_status: 402) 37 | when :processing_error then build_card_error('An error occurred while processing the card', nil, code: 'processing_error', http_status: 402) 38 | when :card_error then build_card_error('The card number is not a valid credit card number.', 'number', code: 'invalid_number', http_status: 402) 39 | when :incorrect_zip then build_card_error('The zip code you supplied failed validation.', 'address_zip', code: 'incorrect_zip', http_status: 402) 40 | when :insufficient_funds then build_card_error('The card has insufficient funds to complete the purchase.', nil, code: 'insufficient_funds', http_status: 402) 41 | when :lost_card then build_card_error('The payment has been declined because the card is reported lost.', nil, code: 'lost_card', http_status: 402) 42 | when :stolen_card then build_card_error('The payment has been declined because the card is reported stolen.', nil, code: 'stolen_card', http_status: 402) 43 | end 44 | end 45 | 46 | def self.get_decline_code(code) 47 | decline_code_map = { 48 | card_declined: 'do_not_honor', 49 | missing: nil 50 | } 51 | decline_code_map.default = code.to_s 52 | 53 | code_key = code.to_sym 54 | decline_code_map[code_key] 55 | end 56 | 57 | def self.build_card_error(message, param, **kwargs) 58 | json_hash = { 59 | message: message, 60 | param: param, 61 | code: kwargs[:code], 62 | type: 'card_error', 63 | decline_code: get_decline_code(kwargs[:code]) 64 | } 65 | 66 | error_keyword_args = kwargs.merge(json_body: { error: json_hash }, http_body: { error: json_hash }.to_json) 67 | 68 | Stripe::CardError.new(message, param, **error_keyword_args) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/stripe_mock/client.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | class Client 3 | attr_reader :port, :state 4 | 5 | def initialize(port) 6 | @port = port 7 | 8 | DRb.start_service 9 | @pipe = DRbObject.new_with_uri "druby://localhost:#{port}" 10 | 11 | # Ensure client can connect to server 12 | timeout_wrap(5) { @pipe.ping } 13 | @state = 'ready' 14 | end 15 | 16 | def mock_request(method, url, api_key: nil, params: {}, headers: {}) 17 | timeout_wrap do 18 | @pipe.mock_request(method, url, api_key: api_key, params: params, headers: headers).tap {|result| 19 | response, api_key = result 20 | if response.is_a?(Hash) && response[:error_raised] == 'invalid_request' 21 | args, keyword_args = response[:error_params].first(2), response[:error_params].last 22 | raise Stripe::InvalidRequestError.new(*args, **keyword_args) 23 | end 24 | } 25 | end 26 | end 27 | 28 | def get_server_data(key) 29 | timeout_wrap { 30 | # Massage the data make this behave the same as the local StripeMock.start 31 | result = {} 32 | @pipe.get_data(key).each {|k,v| result[k] = Stripe::Util.symbolize_names(v) } 33 | result 34 | } 35 | end 36 | 37 | def error_queue 38 | timeout_wrap { @pipe.error_queue } 39 | end 40 | 41 | def set_server_debug(toggle) 42 | timeout_wrap { @pipe.set_debug(toggle) } 43 | end 44 | 45 | def server_debug? 46 | timeout_wrap { @pipe.debug? } 47 | end 48 | 49 | def set_server_global_id_prefix(value) 50 | timeout_wrap { @pipe.set_global_id_prefix(value) } 51 | end 52 | 53 | def server_global_id_prefix 54 | timeout_wrap { @pipe.global_id_prefix } 55 | end 56 | 57 | def generate_bank_token(recipient_params) 58 | timeout_wrap { @pipe.generate_bank_token(recipient_params) } 59 | end 60 | 61 | def generate_card_token(card_params) 62 | timeout_wrap { @pipe.generate_card_token(card_params) } 63 | end 64 | 65 | def generate_webhook_event(event_data) 66 | timeout_wrap { Stripe::Util.symbolize_names @pipe.generate_webhook_event(event_data) } 67 | end 68 | 69 | def get_conversion_rate 70 | timeout_wrap { @pipe.get_data(:conversion_rate) } 71 | end 72 | 73 | def set_conversion_rate(value) 74 | timeout_wrap { @pipe.set_conversion_rate(value) } 75 | end 76 | 77 | def set_account_balance(value) 78 | timeout_wrap { @pipe.set_account_balance(value) } 79 | end 80 | 81 | def destroy_resource(type, id) 82 | timeout_wrap { @pipe.destroy_resource(type, id) } 83 | end 84 | 85 | def clear_server_data 86 | timeout_wrap { @pipe.clear_data } 87 | end 88 | 89 | def upsert_stripe_object(object, attributes) 90 | timeout_wrap { @pipe.upsert_stripe_object(object, attributes) } 91 | end 92 | 93 | def close! 94 | self.cleanup 95 | StripeMock.stop_client(:clear_server_data => false) 96 | end 97 | 98 | def cleanup 99 | return if @state == 'closed' 100 | set_server_debug(false) 101 | @state = 'closed' 102 | end 103 | 104 | def timeout_wrap(tries=1) 105 | original_tries = tries 106 | begin 107 | raise ClosedClientConnectionError if @state == 'closed' 108 | yield 109 | rescue ClosedClientConnectionError 110 | raise 111 | rescue Errno::ECONNREFUSED, DRb::DRbConnError => e 112 | tries -= 1 113 | if tries > 0 114 | if tries == original_tries - 1 115 | print "Waiting for StripeMock Server.." 116 | else 117 | print '.' 118 | end 119 | sleep 1 120 | retry 121 | else 122 | raise StripeMock::ServerTimeoutError.new(e) 123 | end 124 | end 125 | end 126 | end 127 | 128 | end 129 | -------------------------------------------------------------------------------- /lib/stripe_mock/request_handlers/refunds.rb: -------------------------------------------------------------------------------- 1 | module StripeMock 2 | module RequestHandlers 3 | module Refunds 4 | 5 | def Refunds.included(klass) 6 | klass.add_handler 'post /v1/refunds', :new_refund 7 | klass.add_handler 'get /v1/refunds', :get_refunds 8 | klass.add_handler 'get /v1/refunds/(.*)', :get_refund 9 | klass.add_handler 'post /v1/refunds/(.*)', :update_refund 10 | end 11 | 12 | def new_refund(route, method_url, params, headers) 13 | if headers && headers[:idempotency_key] 14 | params[:idempotency_key] = headers[:idempotency_key] 15 | if refunds.any? 16 | original_refund = refunds.values.find { |c| c[:idempotency_key] == headers[:idempotency_key]} 17 | return refunds[original_refund[:id]] if original_refund 18 | end 19 | end 20 | 21 | if params[:payment_intent] 22 | payment_intent = assert_existence( 23 | :payment_intent, 24 | params[:payment_intent], 25 | payment_intents[params[:payment_intent]] 26 | ) 27 | charge = {} 28 | else 29 | charge = assert_existence :charge, params[:charge], charges[params[:charge]] 30 | payment_intent = {} 31 | end 32 | params[:amount] ||= payment_intent[:amount] 33 | params[:amount] ||= charge[:amount] 34 | id = new_id('re') 35 | bal_trans_params = { 36 | amount: params[:amount] * -1, 37 | source: id, 38 | type: 'refund' 39 | } 40 | balance_transaction_id = new_balance_transaction('txn', bal_trans_params) 41 | refund = Data.mock_refund params.merge( 42 | :balance_transaction => balance_transaction_id, 43 | :id => id, 44 | :charge => charge[:id], 45 | ) 46 | add_refund_to_charge(refund, charge) unless charge.empty? 47 | refunds[id] = refund 48 | 49 | if params[:expand] == ['balance_transaction'] 50 | refunds[id][:balance_transaction] = 51 | balance_transactions[balance_transaction_id] 52 | end 53 | refund 54 | end 55 | 56 | def update_refund(route, method_url, params, headers) 57 | route =~ method_url 58 | id = $1 59 | 60 | refund = assert_existence :refund, id, refunds[id] 61 | allowed = allowed_refund_params(params) 62 | disallowed = params.keys - allowed 63 | if disallowed.count > 0 64 | raise Stripe::InvalidRequestError.new("Received unknown parameters: #{disallowed.join(', ')}" , '', http_status: 400) 65 | end 66 | 67 | refunds[id] = Util.rmerge(refund, params) 68 | end 69 | 70 | def get_refunds(route, method_url, params, headers) 71 | params[:offset] ||= 0 72 | params[:limit] ||= 10 73 | 74 | clone = refunds.clone 75 | 76 | Data.mock_list_object(clone.values, params) 77 | end 78 | 79 | def get_refund(route, method_url, params, headers) 80 | route =~ method_url 81 | refund_id = $1 || params[:refund] 82 | assert_existence :refund, refund_id, refunds[refund_id] 83 | end 84 | 85 | private 86 | 87 | def ensure_refund_required_params(params) 88 | if non_integer_charge_amount?(params) 89 | raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400) 90 | elsif non_positive_charge_amount?(params) 91 | raise Stripe::InvalidRequestError.new('Invalid positive integer', 'amount', http_status: 400) 92 | elsif params[:charge].nil? 93 | raise Stripe::InvalidRequestError.new('Must provide the identifier of the charge to refund.', nil) 94 | end 95 | end 96 | 97 | def allowed_refund_params(params) 98 | [:metadata] 99 | end 100 | end 101 | end 102 | end 103 | --------------------------------------------------------------------------------