stripe 4.24.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +17 -4
  3. data/.rubocop_todo.yml +10 -9
  4. data/.travis.yml +1 -5
  5. data/CHANGELOG.md +22 -0
  6. data/Gemfile +2 -12
  7. data/README.md +10 -10
  8. data/Rakefile +8 -7
  9. data/VERSION +1 -1
  10. data/lib/stripe.rb +56 -15
  11. data/lib/stripe/api_operations/list.rb +0 -6
  12. data/lib/stripe/connection_manager.rb +131 -0
  13. data/lib/stripe/error_object.rb +94 -0
  14. data/lib/stripe/errors.rb +15 -2
  15. data/lib/stripe/list_object.rb +2 -1
  16. data/lib/stripe/multipart_encoder.rb +131 -0
  17. data/lib/stripe/object_types.rb +0 -1
  18. data/lib/stripe/resources.rb +0 -1
  19. data/lib/stripe/resources/account.rb +1 -5
  20. data/lib/stripe/resources/account_link.rb +1 -1
  21. data/lib/stripe/resources/alipay_account.rb +1 -1
  22. data/lib/stripe/resources/apple_pay_domain.rb +1 -1
  23. data/lib/stripe/resources/application_fee.rb +1 -12
  24. data/lib/stripe/resources/application_fee_refund.rb +1 -1
  25. data/lib/stripe/resources/balance.rb +1 -1
  26. data/lib/stripe/resources/balance_transaction.rb +1 -1
  27. data/lib/stripe/resources/bank_account.rb +1 -1
  28. data/lib/stripe/resources/bitcoin_receiver.rb +1 -1
  29. data/lib/stripe/resources/bitcoin_transaction.rb +1 -1
  30. data/lib/stripe/resources/capability.rb +1 -1
  31. data/lib/stripe/resources/card.rb +1 -1
  32. data/lib/stripe/resources/charge.rb +7 -69
  33. data/lib/stripe/resources/checkout/session.rb +1 -1
  34. data/lib/stripe/resources/country_spec.rb +1 -1
  35. data/lib/stripe/resources/coupon.rb +1 -1
  36. data/lib/stripe/resources/credit_note.rb +1 -1
  37. data/lib/stripe/resources/customer.rb +3 -63
  38. data/lib/stripe/resources/customer_balance_transaction.rb +1 -1
  39. data/lib/stripe/resources/discount.rb +1 -1
  40. data/lib/stripe/resources/dispute.rb +1 -7
  41. data/lib/stripe/resources/ephemeral_key.rb +1 -1
  42. data/lib/stripe/resources/event.rb +1 -1
  43. data/lib/stripe/resources/exchange_rate.rb +1 -1
  44. data/lib/stripe/resources/file.rb +3 -13
  45. data/lib/stripe/resources/file_link.rb +1 -1
  46. data/lib/stripe/resources/invoice.rb +6 -1
  47. data/lib/stripe/resources/invoice_item.rb +1 -1
  48. data/lib/stripe/resources/invoice_line_item.rb +1 -1
  49. data/lib/stripe/resources/issuing/authorization.rb +1 -1
  50. data/lib/stripe/resources/issuing/card.rb +1 -1
  51. data/lib/stripe/resources/issuing/card_details.rb +1 -1
  52. data/lib/stripe/resources/issuing/cardholder.rb +1 -1
  53. data/lib/stripe/resources/issuing/dispute.rb +1 -1
  54. data/lib/stripe/resources/issuing/transaction.rb +1 -1
  55. data/lib/stripe/resources/login_link.rb +1 -1
  56. data/lib/stripe/resources/order.rb +1 -9
  57. data/lib/stripe/resources/order_return.rb +1 -1
  58. data/lib/stripe/resources/payment_intent.rb +1 -1
  59. data/lib/stripe/resources/payment_method.rb +1 -1
  60. data/lib/stripe/resources/payout.rb +1 -7
  61. data/lib/stripe/resources/person.rb +1 -1
  62. data/lib/stripe/resources/plan.rb +1 -1
  63. data/lib/stripe/resources/product.rb +1 -1
  64. data/lib/stripe/resources/radar/early_fraud_warning.rb +1 -1
  65. data/lib/stripe/resources/radar/value_list.rb +1 -1
  66. data/lib/stripe/resources/radar/value_list_item.rb +1 -1
  67. data/lib/stripe/resources/recipient.rb +1 -5
  68. data/lib/stripe/resources/recipient_transfer.rb +1 -1
  69. data/lib/stripe/resources/refund.rb +1 -1
  70. data/lib/stripe/resources/reporting/report_run.rb +1 -1
  71. data/lib/stripe/resources/reporting/report_type.rb +1 -1
  72. data/lib/stripe/resources/reversal.rb +1 -1
  73. data/lib/stripe/resources/review.rb +1 -1
  74. data/lib/stripe/resources/setup_intent.rb +1 -1
  75. data/lib/stripe/resources/sigma/scheduled_query_run.rb +1 -1
  76. data/lib/stripe/resources/sku.rb +1 -1
  77. data/lib/stripe/resources/source.rb +1 -7
  78. data/lib/stripe/resources/source_transaction.rb +1 -1
  79. data/lib/stripe/resources/subscription.rb +9 -9
  80. data/lib/stripe/resources/subscription_item.rb +1 -1
  81. data/lib/stripe/resources/subscription_schedule.rb +1 -1
  82. data/lib/stripe/resources/tax_id.rb +1 -1
  83. data/lib/stripe/resources/tax_rate.rb +1 -1
  84. data/lib/stripe/resources/terminal/connection_token.rb +1 -1
  85. data/lib/stripe/resources/terminal/location.rb +1 -1
  86. data/lib/stripe/resources/terminal/reader.rb +1 -1
  87. data/lib/stripe/resources/three_d_secure.rb +1 -1
  88. data/lib/stripe/resources/token.rb +1 -1
  89. data/lib/stripe/resources/topup.rb +1 -1
  90. data/lib/stripe/resources/transfer.rb +1 -6
  91. data/lib/stripe/resources/usage_record.rb +1 -17
  92. data/lib/stripe/resources/usage_record_summary.rb +1 -1
  93. data/lib/stripe/resources/webhook_endpoint.rb +1 -1
  94. data/lib/stripe/stripe_client.rb +281 -183
  95. data/lib/stripe/stripe_object.rb +4 -23
  96. data/lib/stripe/stripe_response.rb +53 -21
  97. data/lib/stripe/util.rb +10 -11
  98. data/lib/stripe/version.rb +1 -1
  99. data/lib/stripe/webhook.rb +1 -1
  100. data/stripe.gemspec +6 -9
  101. data/test/stripe/account_test.rb +0 -16
  102. data/test/stripe/api_operations_test.rb +2 -2
  103. data/test/stripe/api_resource_test.rb +2 -10
  104. data/test/stripe/charge_test.rb +0 -16
  105. data/test/stripe/connection_manager_test.rb +138 -0
  106. data/test/stripe/customer_test.rb +1 -44
  107. data/test/stripe/errors_test.rb +29 -8
  108. data/test/stripe/file_test.rb +0 -10
  109. data/test/stripe/invoice_test.rb +17 -1
  110. data/test/stripe/list_object_test.rb +0 -16
  111. data/test/stripe/login_link_test.rb +1 -1
  112. data/test/stripe/multipart_encoder_test.rb +130 -0
  113. data/test/stripe/payment_intent_test.rb +1 -1
  114. data/test/stripe/setup_intent_test.rb +1 -1
  115. data/test/stripe/source_test.rb +0 -18
  116. data/test/stripe/stripe_client_test.rb +214 -29
  117. data/test/stripe/stripe_object_test.rb +7 -35
  118. data/test/stripe/stripe_response_test.rb +70 -24
  119. data/test/stripe/subscription_test.rb +2 -2
  120. data/test/stripe/webhook_test.rb +2 -2
  121. data/test/stripe_mock.rb +4 -3
  122. data/test/stripe_test.rb +0 -13
  123. data/test/test_helper.rb +10 -5
  124. metadata +11 -39
  125. data/lib/stripe/resources/issuer_fraud_record.rb +0 -9
  126. data/test/stripe/file_upload_test.rb +0 -79
  127. data/test/stripe/issuer_fraud_record_test.rb +0 -20
  128. data/test/stripe/usage_record_test.rb +0 -28
@@ -53,50 +53,6 @@ module Stripe
53
53
  end
54
54
  end
55
55
 
56
- context "#create_subscription" do
57
- should "create a new subscription" do
58
- customer = Stripe::Customer.retrieve("cus_123")
59
- subscription = customer.create_subscription(items: [{ plan: "silver" }])
60
- assert_requested :post, "#{Stripe.api_base}/v1/customers/#{customer.id}/subscriptions"
61
- assert subscription.is_a?(Stripe::Subscription)
62
- end
63
- end
64
-
65
- context "#create_upcoming_invoice" do
66
- should "create a new invoice" do
67
- customer = Stripe::Customer.retrieve("cus_123")
68
- invoice = customer.create_upcoming_invoice
69
- assert_requested :post, "#{Stripe.api_base}/v1/invoices"
70
- assert invoice.is_a?(Stripe::Invoice)
71
- end
72
- end
73
-
74
- context "#update_subscription" do
75
- should "update a subscription" do
76
- customer = Stripe::Customer.retrieve("cus_123")
77
-
78
- # deprecated API and not in schema
79
- stub_request(:post, "#{Stripe.api_base}/v1/customers/#{customer.id}/subscription")
80
- .with(body: { plan: "silver" })
81
- .to_return(body: JSON.generate(object: "subscription"))
82
- subscription = customer.update_subscription(plan: "silver")
83
- assert subscription.is_a?(Stripe::Subscription)
84
- end
85
- end
86
-
87
- context "#cancel_subscription" do
88
- should "cancel a subscription" do
89
- customer = Stripe::Customer.retrieve("cus_123")
90
-
91
- # deprecated API and not in schema
92
- stub_request(:delete, "#{Stripe.api_base}/v1/customers/#{customer.id}/subscription")
93
- .with(query: { at_period_end: "true" })
94
- .to_return(body: JSON.generate(object: "subscription"))
95
- subscription = customer.cancel_subscription(at_period_end: "true")
96
- assert subscription.is_a?(Stripe::Subscription)
97
- end
98
- end
99
-
100
56
  context "#delete_discount" do
101
57
  should "delete a discount" do
102
58
  customer = Stripe::Customer.retrieve("cus_123")
@@ -113,6 +69,7 @@ module Stripe
113
69
  assert discount.is_a?(Stripe::Discount)
114
70
  end
115
71
  end
72
+
116
73
  context "#create_source" do
117
74
  should "create a source" do
118
75
  Stripe::Customer.create_source(
@@ -4,16 +4,37 @@ require ::File.expand_path("../test_helper", __dir__)
4
4
 
5
5
  module Stripe
6
6
  class StripeErrorTest < Test::Unit::TestCase
7
- context "#to_s" do
8
- should "convert to string" do
9
- e = StripeError.new("message")
10
- assert_equal "message", e.to_s
7
+ context "StripeError" do
8
+ context "#initialize" do
9
+ should "initialize error if json_body is set" do
10
+ e = StripeError.new("message", json_body: { error: { code: "some_error" } })
11
+ assert_not_nil e.error
12
+ assert_equal "some_error", e.error.code
13
+ assert_nil e.error.charge
14
+ end
15
+ end
16
+
17
+ context "#to_s" do
18
+ should "convert to string" do
19
+ e = StripeError.new("message")
20
+ assert_equal "message", e.to_s
21
+
22
+ e = StripeError.new("message", http_status: 200)
23
+ assert_equal "(Status 200) message", e.to_s
11
24
 
12
- e = StripeError.new("message", http_status: 200)
13
- assert_equal "(Status 200) message", e.to_s
25
+ e = StripeError.new("message", http_status: nil, http_body: nil, json_body: nil, http_headers: { request_id: "request-id" })
26
+ assert_equal "(Request request-id) message", e.to_s
27
+ end
28
+ end
29
+ end
14
30
 
15
- e = StripeError.new("message", http_status: nil, http_body: nil, json_body: nil, http_headers: { request_id: "request-id" })
16
- assert_equal "(Request request-id) message", e.to_s
31
+ context "OAuth::OAuthError" do
32
+ context "#initialize" do
33
+ should "initialize error if json_body is set" do
34
+ e = OAuth::OAuthError.new("message", "description", json_body: { error: "some_oauth_error" })
35
+ assert_not_nil e.error
36
+ assert_equal "some_oauth_error", e.error.error
37
+ end
17
38
  end
18
39
  end
19
40
  end
@@ -52,16 +52,6 @@ module Stripe
52
52
  assert file.is_a?(Stripe::File)
53
53
  end
54
54
 
55
- should "be creatable with Faraday::UploadIO" do
56
- file = Stripe::File.create(
57
- purpose: "dispute_evidence",
58
- file: Faraday::UploadIO.new(::File.new(__FILE__), nil),
59
- file_link_data: { create: true }
60
- )
61
- assert_requested :post, "#{Stripe.uploads_base}/v1/files"
62
- assert file.is_a?(Stripe::File)
63
- end
64
-
65
55
  should "be creatable with a string" do
66
56
  file = Stripe::File.create(
67
57
  purpose: "dispute_evidence",
@@ -142,7 +142,7 @@ module Stripe
142
142
  end
143
143
  end
144
144
 
145
- context "#upcoming" do
145
+ context ".upcoming" do
146
146
  should "retrieve upcoming invoices" do
147
147
  invoice = Stripe::Invoice.upcoming(
148
148
  customer: "cus_123",
@@ -192,6 +192,22 @@ module Stripe
192
192
  end
193
193
  end
194
194
 
195
+ context ".list_upcoming_line_items" do
196
+ should "retrieve upcoming invoices" do
197
+ line_items = Stripe::Invoice.list_upcoming_line_items(
198
+ customer: "cus_123",
199
+ subscription: "sub_123"
200
+ )
201
+ assert_requested :get, "#{Stripe.api_base}/v1/invoices/upcoming/lines",
202
+ query: {
203
+ customer: "cus_123",
204
+ subscription: "sub_123",
205
+ }
206
+ assert line_items.data.is_a?(Array)
207
+ assert line_items.data[0].is_a?(Stripe::InvoiceLineItem)
208
+ end
209
+ end
210
+
195
211
  context "#void_invoice" do
196
212
  should "void invoice" do
197
213
  invoice = Stripe::Invoice.retrieve("in_123")
@@ -132,22 +132,6 @@ module Stripe
132
132
  next_list = list.previous_page
133
133
  assert_equal({ expand: ["data.source"], limit: 3 }, next_list.filters)
134
134
  end
135
-
136
- #
137
- # backward compatibility
138
- #
139
-
140
- # note that the name #all is deprecated, as is using it fetch the next page
141
- # in a list
142
- should "be able to retrieve full lists given a listobject" do
143
- c = Stripe::Charge.all
144
- assert c.is_a?(Stripe::ListObject)
145
- assert_equal("/v1/charges", c.resource_url)
146
- all = c.all
147
- assert all.is_a?(Stripe::ListObject)
148
- assert_equal("/v1/charges", all.resource_url)
149
- assert all.data.is_a?(Array)
150
- end
151
135
  end
152
136
  end
153
137
 
@@ -12,7 +12,7 @@ module Stripe
12
12
  "data" => [],
13
13
  "has_more" => false,
14
14
  "object" => "list",
15
- "url" => "/v1/accounts/acct_123/login_links",
15
+ "url" => "/v1/accounts/acct_123/login_links",
16
16
  },
17
17
  }
18
18
  @account = Stripe::Account.construct_from(account_fixture)
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require ::File.expand_path("../test_helper", __dir__)
4
+
5
+ module Stripe
6
+ class MultipartEncoderTest < Test::Unit::TestCase
7
+ should "multipart encode parameters" do
8
+ Tempfile.create("image.jpg") do |f|
9
+ f.write "file-content"
10
+ f.flush
11
+ f.rewind
12
+
13
+ encoder = MultipartEncoder.new
14
+ encoder.encode(
15
+ file: f,
16
+ other_param: "other-param-content"
17
+ )
18
+ encoder.close
19
+ body = encoder.body
20
+
21
+ assert_equal <<~BODY.rstrip, body
22
+ --#{encoder.boundary}\r
23
+ Content-Disposition: form-data; name="file"; filename="#{::File.basename(f.path)}"\r
24
+ Content-Type: application/octet-stream\r
25
+ \r
26
+ file-content\r
27
+ --#{encoder.boundary}\r
28
+ Content-Disposition: form-data; name="other_param"\r
29
+ \r
30
+ other-param-content\r
31
+ --#{encoder.boundary}--
32
+ BODY
33
+ end
34
+ end
35
+
36
+ should "encode file-like objects" do
37
+ klass = Class.new do
38
+ def read
39
+ "klass-read-content"
40
+ end
41
+ end
42
+
43
+ encoder = MultipartEncoder.new
44
+ encoder.encode(
45
+ file_like: klass.new
46
+ )
47
+ encoder.close
48
+ body = encoder.body
49
+
50
+ assert_equal <<~BODY.rstrip, body
51
+ --#{encoder.boundary}\r
52
+ Content-Disposition: form-data; name="file_like"; filename="blob"\r
53
+ Content-Type: application/octet-stream\r
54
+ \r
55
+ klass-read-content\r
56
+ --#{encoder.boundary}--
57
+ BODY
58
+ end
59
+
60
+ should "escape quotes and line break characters in parameter names" do
61
+ encoder = MultipartEncoder.new
62
+ encoder.encode(
63
+ %("quoted\n\r") => "content"
64
+ )
65
+ encoder.close
66
+ body = encoder.body
67
+
68
+ assert_equal <<~BODY.rstrip, body
69
+ --#{encoder.boundary}\r
70
+ Content-Disposition: form-data; name="%22quoted %22"\r
71
+ \r
72
+ content\r
73
+ --#{encoder.boundary}--
74
+ BODY
75
+ end
76
+
77
+ context ".encode" do
78
+ should "provide an easy encoding shortcut" do
79
+ body, content_type = MultipartEncoder.encode(
80
+ param: "content"
81
+ )
82
+ assert_include body, %(Content-Disposition: form-data; name="param")
83
+ assert_include content_type, "#{MultipartEncoder::MULTIPART_FORM_DATA}; boundary="
84
+ end
85
+ end
86
+
87
+ context "#body" do
88
+ should "error if not yet closed" do
89
+ encoder = MultipartEncoder.new
90
+
91
+ e = assert_raises RuntimeError do
92
+ encoder.body
93
+ end
94
+ assert_equal "object must be closed before getting body", e.message
95
+ end
96
+ end
97
+
98
+ context "#close" do
99
+ should "error if closed twice" do
100
+ encoder = MultipartEncoder.new
101
+ encoder.close
102
+
103
+ e = assert_raises RuntimeError do
104
+ encoder.close
105
+ end
106
+ assert_equal "object already closed", e.message
107
+ end
108
+ end
109
+
110
+ context "#content_type" do
111
+ should "produce a content type containing boundary" do
112
+ encoder = MultipartEncoder.new
113
+ assert_equal "#{MultipartEncoder::MULTIPART_FORM_DATA}; boundary=#{encoder.boundary}",
114
+ encoder.content_type
115
+ end
116
+ end
117
+
118
+ context "#encode" do
119
+ should "error if already closed" do
120
+ encoder = MultipartEncoder.new
121
+ encoder.close
122
+
123
+ e = assert_raises RuntimeError do
124
+ encoder.encode(param: "content")
125
+ end
126
+ assert_equal "no more parameters can be written to closed object", e.message
127
+ end
128
+ end
129
+ end
130
+ end
@@ -4,7 +4,7 @@ require ::File.expand_path("../test_helper", __dir__)
4
4
 
5
5
  module Stripe
6
6
  class PaymentIntentTest < Test::Unit::TestCase
7
- TEST_RESOURCE_ID = "pi_123".freeze
7
+ TEST_RESOURCE_ID = "pi_123"
8
8
 
9
9
  should "be listable" do
10
10
  payment_intents = Stripe::PaymentIntent.list
@@ -4,7 +4,7 @@ require ::File.expand_path("../test_helper", __dir__)
4
4
 
5
5
  module Stripe
6
6
  class SetupIntentTest < Test::Unit::TestCase
7
- TEST_RESOURCE_ID = "seti_123".freeze
7
+ TEST_RESOURCE_ID = "seti_123"
8
8
 
9
9
  should "be listable" do
10
10
  setup_intents = Stripe::SetupIntent.list
@@ -51,24 +51,6 @@ module Stripe
51
51
  end
52
52
  end
53
53
 
54
- context "#delete" do
55
- should "warn that #delete is deprecated" do
56
- old_stderr = $stderr
57
- $stderr = StringIO.new
58
- begin
59
- source = Stripe::Source.construct_from(customer: "cus_123",
60
- id: "src_123",
61
- object: "source")
62
- source.delete
63
- message = "NOTE: Stripe::Source#delete is " \
64
- "deprecated; use #detach instead"
65
- assert_match Regexp.new(message), $stderr.string
66
- ensure
67
- $stderr = old_stderr
68
- end
69
- end
70
- end
71
-
72
54
  should "not be listable" do
73
55
  assert_raises NoMethodError do
74
56
  Stripe::Source.list
@@ -17,6 +17,50 @@ module Stripe
17
17
  end
18
18
  end
19
19
 
20
+ context ".clear_all_connection_managers" do
21
+ should "clear connection managers across all threads" do
22
+ stub_request(:post, "#{Stripe.api_base}/path")
23
+ .to_return(body: JSON.generate(object: "account"))
24
+
25
+ num_threads = 3
26
+
27
+ # Poorly named class -- note this is actually a concurrent queue.
28
+ recv_queue = Queue.new
29
+ send_queue = Queue.new
30
+
31
+ threads = num_threads.times.map do |_|
32
+ Thread.start do
33
+ # Causes a connection manager to be created on this thread and a
34
+ # connection within that manager to be created for API access.
35
+ manager = StripeClient.default_connection_manager
36
+ manager.execute_request(:post, "#{Stripe.api_base}/path")
37
+
38
+ # Signal to the main thread we're ready.
39
+ recv_queue << true
40
+
41
+ # Wait for the main thread to signal continue.
42
+ send_queue.pop
43
+
44
+ # This check isn't great, but it's otherwise difficult to tell that
45
+ # anything happened with just the public-facing API.
46
+ assert_equal({}, manager.instance_variable_get(:@active_connections))
47
+ end
48
+ end
49
+
50
+ # Wait for threads to start up.
51
+ threads.each { recv_queue.pop }
52
+
53
+ # Do the clear (the method we're actually trying to test).
54
+ StripeClient.clear_all_connection_managers
55
+
56
+ # Tell threads to run their check.
57
+ threads.each { send_queue << true }
58
+
59
+ # And finally, give all threads time to perform their check.
60
+ threads.each(&:join)
61
+ end
62
+ end
63
+
20
64
  context ".default_client" do
21
65
  should "be a StripeClient" do
22
66
  assert_kind_of StripeClient, StripeClient.default_client
@@ -32,18 +76,19 @@ module Stripe
32
76
  end
33
77
  end
34
78
 
35
- context ".default_conn" do
36
- should "be a Faraday::Connection" do
37
- assert_kind_of Faraday::Connection, StripeClient.default_conn
79
+ context ".default_connection_manager" do
80
+ should "be a ConnectionManager" do
81
+ assert_kind_of ConnectionManager,
82
+ StripeClient.default_connection_manager
38
83
  end
39
84
 
40
85
  should "be a different connection on each thread" do
41
- other_thread_conn = nil
86
+ other_thread_manager = nil
42
87
  thread = Thread.new do
43
- other_thread_conn = StripeClient.default_conn
88
+ other_thread_manager = StripeClient.default_connection_manager
44
89
  end
45
90
  thread.join
46
- refute_equal StripeClient.default_conn, other_thread_conn
91
+ refute_equal StripeClient.default_connection_manager, other_thread_manager
47
92
  end
48
93
  end
49
94
 
@@ -52,26 +97,54 @@ module Stripe
52
97
  Stripe.stubs(:max_network_retries).returns(2)
53
98
  end
54
99
 
55
- should "retry on timeout" do
56
- assert StripeClient.should_retry?(Faraday::TimeoutError.new(""), 0)
100
+ should "retry on Errno::ECONNREFUSED" do
101
+ assert StripeClient.should_retry?(Errno::ECONNREFUSED.new,
102
+ method: :post, num_retries: 0)
103
+ end
104
+
105
+ should "retry on Net::OpenTimeout" do
106
+ assert StripeClient.should_retry?(Net::OpenTimeout.new,
107
+ method: :post, num_retries: 0)
108
+ end
109
+
110
+ should "retry on Net::ReadTimeout" do
111
+ assert StripeClient.should_retry?(Net::ReadTimeout.new,
112
+ method: :post, num_retries: 0)
113
+ end
114
+
115
+ should "retry on SocketError" do
116
+ assert StripeClient.should_retry?(SocketError.new,
117
+ method: :post, num_retries: 0)
118
+ end
119
+
120
+ should "retry on a 409 Conflict" do
121
+ assert StripeClient.should_retry?(Stripe::StripeError.new(http_status: 409),
122
+ method: :post, num_retries: 0)
57
123
  end
58
124
 
59
- should "retry on a failed connection" do
60
- assert StripeClient.should_retry?(Faraday::ConnectionFailed.new(""), 0)
125
+ should "retry on a 500 Internal Server Error when non-POST" do
126
+ assert StripeClient.should_retry?(Stripe::StripeError.new(http_status: 500),
127
+ method: :get, num_retries: 0)
61
128
  end
62
129
 
63
- should "retry on a conflict" do
64
- error = make_rate_limit_error
65
- e = Faraday::ClientError.new(error[:error][:message], status: 409)
66
- assert StripeClient.should_retry?(e, 0)
130
+ should "retry on a 503 Service Unavailable" do
131
+ assert StripeClient.should_retry?(Stripe::StripeError.new(http_status: 503),
132
+ method: :post, num_retries: 0)
67
133
  end
68
134
 
69
135
  should "not retry at maximum count" do
70
- refute StripeClient.should_retry?(RuntimeError.new, Stripe.max_network_retries)
136
+ refute StripeClient.should_retry?(RuntimeError.new,
137
+ method: :post, num_retries: Stripe.max_network_retries)
71
138
  end
72
139
 
73
140
  should "not retry on a certificate validation error" do
74
- refute StripeClient.should_retry?(Faraday::SSLError.new(""), 0)
141
+ refute StripeClient.should_retry?(OpenSSL::SSL::SSLError.new,
142
+ method: :post, num_retries: 0)
143
+ end
144
+
145
+ should "not retry on a 500 Internal Server Error when POST" do
146
+ refute StripeClient.should_retry?(Stripe::StripeError.new(http_status: 500),
147
+ method: :post, num_retries: 0)
75
148
  end
76
149
  end
77
150
 
@@ -115,15 +188,16 @@ module Stripe
115
188
  end
116
189
 
117
190
  context "#initialize" do
118
- should "set Stripe.default_conn" do
191
+ should "set Stripe.default_connection_manager" do
119
192
  client = StripeClient.new
120
- assert_equal StripeClient.default_conn, client.conn
193
+ assert_equal StripeClient.default_connection_manager,
194
+ client.connection_manager
121
195
  end
122
196
 
123
197
  should "set a different connection if one was specified" do
124
- conn = Faraday.new
125
- client = StripeClient.new(conn)
126
- assert_equal conn, client.conn
198
+ connection_manager = ConnectionManager.new
199
+ client = StripeClient.new(connection_manager)
200
+ assert_equal connection_manager, client.connection_manager
127
201
  end
128
202
  end
129
203
 
@@ -178,7 +252,7 @@ module Stripe
178
252
  Util.expects(:log_debug).with("Request details",
179
253
  body: "",
180
254
  idempotency_key: "abc",
181
- query_params: nil)
255
+ query: nil)
182
256
 
183
257
  Util.expects(:log_info).with("Response from Stripe API",
184
258
  account: "acct_123",
@@ -403,6 +477,20 @@ module Stripe
403
477
  assert_equal 'Invalid response object from API: "" (HTTP response code was 200)', e.message
404
478
  end
405
479
 
480
+ should "handle low level error" do
481
+ stub_request(:post, "#{Stripe.api_base}/v1/charges")
482
+ .to_raise(Errno::ECONNREFUSED.new)
483
+
484
+ client = StripeClient.new
485
+ e = assert_raises Stripe::APIConnectionError do
486
+ client.execute_request(:post, "/v1/charges")
487
+ end
488
+
489
+ assert_equal StripeClient::ERROR_MESSAGE_CONNECTION % Stripe.api_base +
490
+ "\n\n(Network error: Connection refused)",
491
+ e.message
492
+ end
493
+
406
494
  should "handle error response with unknown value" do
407
495
  stub_request(:post, "#{Stripe.api_base}/v1/charges")
408
496
  .to_return(body: JSON.generate(bar: "foo"), status: 500)
@@ -738,14 +826,106 @@ module Stripe
738
826
 
739
827
  should "reset local thread state after a call" do
740
828
  begin
741
- Thread.current[:stripe_client] = :stripe_client
829
+ StripeClient.current_thread_context.active_client = :stripe_client
742
830
 
743
831
  client = StripeClient.new
744
832
  client.request {}
745
833
 
746
- assert_equal :stripe_client, Thread.current[:stripe_client]
834
+ assert_equal :stripe_client,
835
+ StripeClient.current_thread_context.active_client
747
836
  ensure
748
- Thread.current[:stripe_client] = nil
837
+ StripeClient.current_thread_context.active_client = nil
838
+ end
839
+ end
840
+
841
+ should "correctly return last responses despite multiple clients" do
842
+ charge_resp = { object: "charge" }
843
+ coupon_resp = { object: "coupon" }
844
+
845
+ stub_request(:post, "#{Stripe.api_base}/v1/charges")
846
+ .to_return(body: JSON.generate(charge_resp))
847
+ stub_request(:post, "#{Stripe.api_base}/v1/coupons")
848
+ .to_return(body: JSON.generate(coupon_resp))
849
+
850
+ client1 = StripeClient.new
851
+ client2 = StripeClient.new
852
+
853
+ client2_resp = nil
854
+ _charge, client1_resp = client1.request do
855
+ Charge.create
856
+
857
+ # This is contrived, but we run one client nested in the `request`
858
+ # block of another one just to ensure that the parent is still
859
+ # unwinding when this goes through. If the parent's last response
860
+ # were to be overridden by this client (through a bug), then it would
861
+ # happen here.
862
+ _coupon, client2_resp = client2.request do
863
+ Coupon.create
864
+ end
865
+ end
866
+
867
+ assert_equal charge_resp, client1_resp.data
868
+ assert_equal coupon_resp, client2_resp.data
869
+ end
870
+
871
+ should "correctly return last responses despite multiple threads" do
872
+ charge_resp = { object: "charge" }
873
+ coupon_resp = { object: "coupon" }
874
+
875
+ stub_request(:post, "#{Stripe.api_base}/v1/charges")
876
+ .to_return(body: JSON.generate(charge_resp))
877
+ stub_request(:post, "#{Stripe.api_base}/v1/coupons")
878
+ .to_return(body: JSON.generate(coupon_resp))
879
+
880
+ client = StripeClient.new
881
+
882
+ # Poorly named class -- note this is actually a concurrent queue.
883
+ recv_queue = Queue.new
884
+ send_queue = Queue.new
885
+
886
+ # Start a thread, make an API request, but then idle in the `request`
887
+ # block until the main thread has been able to make its own API request
888
+ # and signal that it's done. If this thread's last response were to be
889
+ # overridden by the main thread (through a bug), then this routine
890
+ # should suss it out.
891
+ resp1 = nil
892
+ thread = Thread.start do
893
+ _charge, resp1 = client.request do
894
+ Charge.create
895
+
896
+ # Idle in `request` block until main thread signals.
897
+ send_queue.pop
898
+ end
899
+
900
+ # Signal main thread that we're done and it can run its checks.
901
+ recv_queue << true
902
+ end
903
+
904
+ # Make an API request.
905
+ _coupon, resp2 = client.request do
906
+ Coupon.create
907
+ end
908
+
909
+ # Tell background thread to finish `request`, then wait for it to
910
+ # signal back to us that it's ready.
911
+ send_queue << true
912
+ recv_queue.pop
913
+
914
+ assert_equal charge_resp, resp1.data
915
+ assert_equal coupon_resp, resp2.data
916
+
917
+ # And for maximum hygiene, make sure that our thread rejoins.
918
+ thread.join
919
+ end
920
+
921
+ should "error if calls to #request are nested on the same thread" do
922
+ client = StripeClient.new
923
+ client.request do
924
+ e = assert_raises(RuntimeError) do
925
+ client.request {}
926
+ end
927
+ assert_equal "calls to StripeClient#request cannot be nested within a thread",
928
+ e.message
749
929
  end
750
930
  end
751
931
  end
@@ -753,18 +933,23 @@ module Stripe
753
933
  context "#proxy" do
754
934
  should "run the request through the proxy" do
755
935
  begin
756
- Thread.current[:stripe_client_default_conn] = nil
936
+ StripeClient.current_thread_context.default_connection_manager = nil
757
937
 
758
- Stripe.proxy = "http://localhost:8080"
938
+ Stripe.proxy = "http://user:pass@localhost:8080"
759
939
 
760
940
  client = StripeClient.new
761
941
  client.request {}
762
942
 
763
- assert_equal "http://localhost:8080", Stripe::StripeClient.default_conn.proxy.uri.to_s
943
+ connection = Stripe::StripeClient.default_connection_manager.connection_for(Stripe.api_base)
944
+
945
+ assert_equal "localhost", connection.proxy_address
946
+ assert_equal 8080, connection.proxy_port
947
+ assert_equal "user", connection.proxy_user
948
+ assert_equal "pass", connection.proxy_pass
764
949
  ensure
765
950
  Stripe.proxy = nil
766
951
 
767
- Thread.current[:stripe_client_default_conn] = nil
952
+ StripeClient.current_thread_context.default_connection_manager = nil
768
953
  end
769
954
  end
770
955
  end