stripe 4.24.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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