stripe 1.30.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +4 -0
  3. data/.github/ISSUE_TEMPLATE.md +5 -0
  4. data/.travis.yml +3 -14
  5. data/Gemfile +28 -4
  6. data/History.txt +180 -0
  7. data/README.md +147 -0
  8. data/Rakefile +10 -0
  9. data/VERSION +1 -1
  10. data/bin/stripe-console +12 -5
  11. data/lib/data/ca-certificates.crt +3868 -5114
  12. data/lib/stripe/account.rb +43 -23
  13. data/lib/stripe/alipay_account.rb +20 -0
  14. data/lib/stripe/api_operations/create.rb +2 -2
  15. data/lib/stripe/api_operations/delete.rb +2 -2
  16. data/lib/stripe/api_operations/list.rb +2 -3
  17. data/lib/stripe/api_operations/request.rb +9 -3
  18. data/lib/stripe/api_operations/save.rb +85 -0
  19. data/lib/stripe/api_resource.rb +38 -5
  20. data/lib/stripe/apple_pay_domain.rb +12 -0
  21. data/lib/stripe/application_fee.rb +8 -8
  22. data/lib/stripe/application_fee_refund.rb +7 -3
  23. data/lib/stripe/balance_transaction.rb +1 -1
  24. data/lib/stripe/bank_account.rb +13 -4
  25. data/lib/stripe/bitcoin_receiver.rb +6 -6
  26. data/lib/stripe/bitcoin_transaction.rb +1 -1
  27. data/lib/stripe/card.rb +9 -5
  28. data/lib/stripe/charge.rb +38 -20
  29. data/lib/stripe/country_spec.rb +9 -0
  30. data/lib/stripe/coupon.rb +1 -1
  31. data/lib/stripe/customer.rb +12 -10
  32. data/lib/stripe/dispute.rb +4 -5
  33. data/lib/stripe/errors.rb +92 -0
  34. data/lib/stripe/file_upload.rb +1 -1
  35. data/lib/stripe/invoice.rb +7 -7
  36. data/lib/stripe/invoice_item.rb +1 -1
  37. data/lib/stripe/list_object.rb +8 -7
  38. data/lib/stripe/order.rb +12 -4
  39. data/lib/stripe/order_return.rb +9 -0
  40. data/lib/stripe/plan.rb +1 -1
  41. data/lib/stripe/product.rb +2 -10
  42. data/lib/stripe/recipient.rb +1 -1
  43. data/lib/stripe/refund.rb +1 -1
  44. data/lib/stripe/reversal.rb +7 -3
  45. data/lib/stripe/singleton_api_resource.rb +3 -3
  46. data/lib/stripe/sku.rb +2 -2
  47. data/lib/stripe/source.rb +11 -0
  48. data/lib/stripe/stripe_client.rb +396 -0
  49. data/lib/stripe/stripe_object.rb +167 -91
  50. data/lib/stripe/stripe_response.rb +48 -0
  51. data/lib/stripe/subscription.rb +15 -9
  52. data/lib/stripe/subscription_item.rb +12 -0
  53. data/lib/stripe/three_d_secure.rb +9 -0
  54. data/lib/stripe/transfer.rb +4 -5
  55. data/lib/stripe/util.rb +105 -33
  56. data/lib/stripe/version.rb +1 -1
  57. data/lib/stripe.rb +69 -266
  58. data/spec/fixtures.json +1409 -0
  59. data/spec/fixtures.yaml +1153 -0
  60. data/spec/spec.json +19949 -0
  61. data/spec/spec.yaml +15504 -0
  62. data/stripe.gemspec +5 -18
  63. data/test/api_fixtures.rb +29 -0
  64. data/test/api_stub_helpers.rb +125 -0
  65. data/test/stripe/account_test.rb +163 -211
  66. data/test/stripe/alipay_account_test.rb +19 -0
  67. data/test/stripe/api_operations_test.rb +31 -0
  68. data/test/stripe/api_resource_test.rb +174 -340
  69. data/test/stripe/apple_pay_domain_test.rb +33 -0
  70. data/test/stripe/application_fee_refund_test.rb +22 -31
  71. data/test/stripe/application_fee_test.rb +6 -14
  72. data/test/stripe/balance_test.rb +3 -3
  73. data/test/stripe/bank_account_test.rb +41 -0
  74. data/test/stripe/bitcoin_receiver_test.rb +51 -42
  75. data/test/stripe/bitcoin_transaction_test.rb +11 -19
  76. data/test/stripe/charge_test.rb +39 -98
  77. data/test/stripe/country_spec_test.rb +20 -0
  78. data/test/stripe/coupon_test.rb +35 -11
  79. data/test/stripe/customer_card_test.rb +25 -46
  80. data/test/stripe/customer_test.rb +89 -61
  81. data/test/stripe/dispute_test.rb +28 -31
  82. data/test/stripe/errors_test.rb +18 -0
  83. data/test/stripe/file_upload_test.rb +32 -24
  84. data/test/stripe/invoice_item_test.rb +55 -0
  85. data/test/stripe/invoice_test.rb +50 -24
  86. data/test/stripe/list_object_test.rb +57 -45
  87. data/test/stripe/order_return_test.rb +21 -0
  88. data/test/stripe/order_test.rb +41 -34
  89. data/test/stripe/plan_test.rb +52 -0
  90. data/test/stripe/product_test.rb +31 -25
  91. data/test/stripe/recipient_card_test.rb +23 -40
  92. data/test/stripe/recipient_test.rb +50 -0
  93. data/test/stripe/refund_test.rb +20 -36
  94. data/test/stripe/reversal_test.rb +27 -31
  95. data/test/stripe/sku_test.rb +39 -13
  96. data/test/stripe/source_test.rb +43 -0
  97. data/test/stripe/stripe_client_test.rb +428 -0
  98. data/test/stripe/stripe_object_test.rb +186 -13
  99. data/test/stripe/stripe_response_test.rb +46 -0
  100. data/test/stripe/subscription_item_test.rb +54 -0
  101. data/test/stripe/subscription_test.rb +40 -52
  102. data/test/stripe/three_d_secure_test.rb +23 -0
  103. data/test/stripe/transfer_test.rb +38 -13
  104. data/test/stripe/util_test.rb +48 -16
  105. data/test/stripe_test.rb +25 -0
  106. data/test/test_data.rb +5 -621
  107. data/test/test_helper.rb +24 -24
  108. metadata +60 -139
  109. data/README.rdoc +0 -68
  110. data/gemfiles/default-with-activesupport.gemfile +0 -10
  111. data/gemfiles/json.gemfile +0 -12
  112. data/gemfiles/yajl.gemfile +0 -12
  113. data/lib/stripe/api_operations/update.rb +0 -58
  114. data/lib/stripe/errors/api_connection_error.rb +0 -4
  115. data/lib/stripe/errors/api_error.rb +0 -4
  116. data/lib/stripe/errors/authentication_error.rb +0 -4
  117. data/lib/stripe/errors/card_error.rb +0 -12
  118. data/lib/stripe/errors/invalid_request_error.rb +0 -11
  119. data/lib/stripe/errors/rate_limit_error.rb +0 -4
  120. data/lib/stripe/errors/stripe_error.rb +0 -26
  121. data/test/stripe/charge_refund_test.rb +0 -55
  122. data/test/stripe/metadata_test.rb +0 -129
@@ -0,0 +1,396 @@
1
+ module Stripe
2
+ # StripeClient executes requests against the Stripe API and allows a user to
3
+ # recover both a resource a call returns as well as a response object that
4
+ # contains information on the HTTP call.
5
+ class StripeClient
6
+ attr_accessor :conn
7
+
8
+ # Initializes a new StripeClient. Expects a Faraday connection object, and
9
+ # uses a default connection unless one is passed.
10
+ def initialize(conn = nil)
11
+ self.conn = conn || self.class.default_conn
12
+ @system_profiler = SystemProfiler.new
13
+ end
14
+
15
+ def self.active_client
16
+ Thread.current[:stripe_client] || default_client
17
+ end
18
+
19
+ def self.default_client
20
+ @default_client ||= StripeClient.new(default_conn)
21
+ end
22
+
23
+ # A default Faraday connection to be used when one isn't configured. This
24
+ # object should never be mutated, and instead instantiating your own
25
+ # connection and wrapping it in a StripeClient object should be preferred.
26
+ def self.default_conn
27
+ # We're going to keep connections around so that we can take advantage
28
+ # of connection re-use, so make sure that we have a separate connection
29
+ # object per thread.
30
+ Thread.current[:stripe_client_default_conn] ||= begin
31
+ conn = Faraday.new do |c|
32
+ c.use Faraday::Request::UrlEncoded
33
+ c.use Faraday::Response::RaiseError
34
+ c.adapter Faraday.default_adapter
35
+ end
36
+
37
+ if Stripe.verify_ssl_certs
38
+ conn.ssl.verify = true
39
+ conn.ssl.cert_store = Stripe.ca_store
40
+ else
41
+ conn.ssl.verify = false
42
+
43
+ unless @verify_ssl_warned
44
+ @verify_ssl_warned = true
45
+ $stderr.puts("WARNING: Running without SSL cert verification. " \
46
+ "You should never do this in production. " \
47
+ "Execute 'Stripe.verify_ssl_certs = true' to enable verification.")
48
+ end
49
+ end
50
+
51
+ conn
52
+ end
53
+ end
54
+
55
+ # Checks if an error is a problem that we should retry on. This includes both
56
+ # socket errors that may represent an intermittent problem and some special
57
+ # HTTP statuses.
58
+ def self.should_retry?(e, retry_count)
59
+ return false if retry_count >= Stripe.max_network_retries
60
+
61
+ # Retry on timeout-related problems (either on open or read).
62
+ return true if e.is_a?(Faraday::TimeoutError)
63
+
64
+ # Destination refused the connection, the connection was reset, or a
65
+ # variety of other connection failures. This could occur from a single
66
+ # saturated server, so retry in case it's intermittent.
67
+ return true if e.is_a?(Faraday::ConnectionFailed)
68
+
69
+ if e.is_a?(Faraday::ClientError) && e.response
70
+ # 409 conflict
71
+ return true if e.response[:status] == 409
72
+ end
73
+
74
+ false
75
+ end
76
+
77
+ def self.sleep_time(retry_count)
78
+ # Apply exponential backoff with initial_network_retry_delay on the
79
+ # number of attempts so far as inputs. Do not allow the number to exceed
80
+ # max_network_retry_delay.
81
+ sleep_seconds = [Stripe.initial_network_retry_delay * (2 ** (retry_count - 1)), Stripe.max_network_retry_delay].min
82
+
83
+ # Apply some jitter by randomizing the value in the range of (sleep_seconds
84
+ # / 2) to (sleep_seconds).
85
+ sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
86
+
87
+ # But never sleep less than the base sleep seconds.
88
+ sleep_seconds = [Stripe.initial_network_retry_delay, sleep_seconds].max
89
+
90
+ sleep_seconds
91
+ end
92
+
93
+ # Executes the API call within the given block. Usage looks like:
94
+ #
95
+ # client = StripeClient.new
96
+ # charge, resp = client.request { Charge.create }
97
+ #
98
+ def request(&block)
99
+ @last_response = nil
100
+ old_stripe_client = Thread.current[:stripe_client]
101
+ Thread.current[:stripe_client] = self
102
+
103
+ begin
104
+ res = block.call
105
+ [res, @last_response]
106
+ ensure
107
+ Thread.current[:stripe_client] = old_stripe_client
108
+ end
109
+ end
110
+
111
+ def execute_request(method, url,
112
+ api_base: nil, api_key: nil, headers: {}, params: {})
113
+
114
+ api_base ||= Stripe.api_base
115
+ api_key ||= Stripe.api_key
116
+
117
+ check_api_key!(api_key)
118
+
119
+ params = Util.objects_to_ids(params)
120
+ url = api_url(url, api_base)
121
+
122
+ case method.to_s.downcase.to_sym
123
+ when :get, :head, :delete
124
+ # Make params into GET parameters
125
+ url += "#{URI.parse(url).query ? '&' : '?'}#{Util.encode_parameters(params)}" if params && params.any?
126
+ payload = nil
127
+ else
128
+ if headers[:content_type] && headers[:content_type] == "multipart/form-data"
129
+ payload = params
130
+ else
131
+ payload = Util.encode_parameters(params)
132
+ end
133
+ end
134
+
135
+ http_resp = execute_request_with_rescues(api_base, 0) do
136
+ conn.run_request(
137
+ method,
138
+ url,
139
+ payload,
140
+ # TODO: Convert RestClient-style parameters.
141
+ request_headers(api_key, method).update(headers)
142
+ ) do |req|
143
+ req.options.open_timeout = Stripe.open_timeout
144
+ req.options.timeout = Stripe.read_timeout
145
+ end
146
+ end
147
+
148
+ begin
149
+ resp = StripeResponse.from_faraday_response(http_resp)
150
+ rescue JSON::ParserError
151
+ raise general_api_error(http_resp.code, http_resp.body)
152
+ end
153
+
154
+ # Allows StripeClient#request to return a response object to a caller.
155
+ @last_response = resp
156
+ [resp, api_key]
157
+ end
158
+
159
+ private
160
+
161
+ def api_url(url='', api_base=nil)
162
+ (api_base || Stripe.api_base) + url
163
+ end
164
+
165
+ def check_api_key!(api_key)
166
+ unless api_key
167
+ raise AuthenticationError.new('No API key provided. ' \
168
+ 'Set your API key using "Stripe.api_key = <API-KEY>". ' \
169
+ 'You can generate API keys from the Stripe web interface. ' \
170
+ 'See https://stripe.com/api for details, or email support@stripe.com ' \
171
+ 'if you have any questions.')
172
+ end
173
+
174
+ if api_key =~ /\s/
175
+ raise AuthenticationError.new('Your API key is invalid, as it contains ' \
176
+ 'whitespace. (HINT: You can double-check your API key from the ' \
177
+ 'Stripe web interface. See https://stripe.com/api for details, or ' \
178
+ 'email support@stripe.com if you have any questions.)')
179
+ end
180
+ end
181
+
182
+ def execute_request_with_rescues(api_base, retry_count, &block)
183
+ begin
184
+ resp = block.call
185
+
186
+ # We rescue all exceptions from a request so that we have an easy spot to
187
+ # implement our retry logic across the board. We'll re-raise if it's a type
188
+ # of exception that we didn't expect to handle.
189
+ rescue => e
190
+ if self.class.should_retry?(e, retry_count)
191
+ retry_count = retry_count + 1
192
+ sleep self.class.sleep_time(retry_count)
193
+ retry
194
+ end
195
+
196
+ case e
197
+ when Faraday::ClientError
198
+ if e.response
199
+ handle_api_error(e.response)
200
+ else
201
+ handle_network_error(e, retry_count, api_base)
202
+ end
203
+
204
+ # Only handle errors when we know we can do so, and re-raise otherwise.
205
+ # This should be pretty infrequent.
206
+ else
207
+ raise
208
+ end
209
+ end
210
+
211
+ resp
212
+ end
213
+
214
+ def general_api_error(status, body)
215
+ APIError.new("Invalid response object from API: #{body.inspect} " +
216
+ "(HTTP response code was #{status})",
217
+ http_status: status, http_body: body)
218
+ end
219
+
220
+
221
+ def handle_api_error(http_resp)
222
+ begin
223
+ resp = StripeResponse.from_faraday_hash(http_resp)
224
+ error = resp.data[:error]
225
+
226
+ unless error && error.is_a?(Hash)
227
+ raise StripeError.new("Indeterminate error")
228
+ end
229
+
230
+ rescue JSON::ParserError, StripeError
231
+ raise general_api_error(http_resp[:status], http_resp[:body])
232
+ end
233
+
234
+ case resp.http_status
235
+ when 400, 404
236
+ error = InvalidRequestError.new(
237
+ error[:message], error[:param],
238
+ http_status: resp.http_status, http_body: resp.http_body,
239
+ json_body: resp.data, http_headers: resp.http_headers
240
+ )
241
+ when 401
242
+ error = AuthenticationError.new(
243
+ error[:message],
244
+ http_status: resp.http_status, http_body: resp.http_body,
245
+ json_body: resp.data, http_headers: resp.http_headers
246
+ )
247
+ when 402
248
+ error = CardError.new(
249
+ error[:message], error[:param], error[:code],
250
+ http_status: resp.http_status, http_body: resp.http_body,
251
+ json_body: resp.data, http_headers: resp.http_headers
252
+ )
253
+ when 403
254
+ error = PermissionError.new(
255
+ error[:message],
256
+ http_status: resp.http_status, http_body: resp.http_body,
257
+ json_body: resp.data, http_headers: resp.http_headers
258
+ )
259
+ when 429
260
+ error = RateLimitError.new(
261
+ error[:message],
262
+ http_status: resp.http_status, http_body: resp.http_body,
263
+ json_body: resp.data, http_headers: resp.http_headers
264
+ )
265
+ else
266
+ error = APIError.new(
267
+ error[:message],
268
+ http_status: resp.http_status, http_body: resp.http_body,
269
+ json_body: resp.data, http_headers: resp.http_headers
270
+ )
271
+ end
272
+
273
+ error.response = resp
274
+ raise(error)
275
+ end
276
+
277
+ def handle_network_error(e, retry_count, api_base=nil)
278
+ case e
279
+ when Faraday::ConnectionFailed
280
+ message = "Unexpected error communicating when trying to connect to Stripe. " \
281
+ "You may be seeing this message because your DNS is not working. " \
282
+ "To check, try running 'host stripe.com' from the command line."
283
+
284
+ when Faraday::SSLError
285
+ message = "Could not establish a secure connection to Stripe, you may " \
286
+ "need to upgrade your OpenSSL version. To check, try running " \
287
+ "'openssl s_client -connect api.stripe.com:443' from the " \
288
+ "command line."
289
+
290
+ when Faraday::TimeoutError
291
+ api_base = Stripe.api_base unless api_base
292
+ message = "Could not connect to Stripe (#{api_base}). " \
293
+ "Please check your internet connection and try again. " \
294
+ "If this problem persists, you should check Stripe's service status at " \
295
+ "https://twitter.com/stripestatus, or let us know at support@stripe.com."
296
+
297
+ else
298
+ message = "Unexpected error communicating with Stripe. " \
299
+ "If this problem persists, let us know at support@stripe.com."
300
+
301
+ end
302
+
303
+ if retry_count > 0
304
+ message += " Request was retried #{retry_count} times."
305
+ end
306
+
307
+ raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
308
+ end
309
+
310
+ def request_headers(api_key, method)
311
+ headers = {
312
+ 'User-Agent' => "Stripe/v1 RubyBindings/#{Stripe::VERSION}",
313
+ 'Authorization' => "Bearer #{api_key}",
314
+ 'Content-Type' => 'application/x-www-form-urlencoded'
315
+ }
316
+
317
+ # It is only safe to retry network failures on post and delete
318
+ # requests if we add an Idempotency-Key header
319
+ if [:post, :delete].include?(method) && Stripe.max_network_retries > 0
320
+ headers['Idempotency-Key'] ||= SecureRandom.uuid
321
+ end
322
+
323
+ headers['Stripe-Version'] = Stripe.api_version if Stripe.api_version
324
+ headers['Stripe-Account'] = Stripe.stripe_account if Stripe.stripe_account
325
+
326
+ user_agent = @system_profiler.user_agent
327
+ begin
328
+ headers.update(
329
+ 'X-Stripe-Client-User-Agent' => JSON.generate(user_agent)
330
+ )
331
+ rescue => e
332
+ headers.update(
333
+ 'X-Stripe-Client-Raw-User-Agent' => user_agent.inspect,
334
+ :error => "#{e} (#{e.class})"
335
+ )
336
+ end
337
+
338
+ headers
339
+ end
340
+
341
+ # SystemProfiler extracts information about the system that we're running
342
+ # in so that we can generate a rich user agent header to help debug
343
+ # integrations.
344
+ class SystemProfiler
345
+ def self.get_uname
346
+ if File.exist?('/proc/version')
347
+ File.read('/proc/version').strip
348
+ else
349
+ case RbConfig::CONFIG['host_os']
350
+ when /linux|darwin|bsd|sunos|solaris|cygwin/i
351
+ get_uname_from_system
352
+ when /mswin|mingw/i
353
+ get_uname_from_system_ver
354
+ else
355
+ "unknown platform"
356
+ end
357
+ end
358
+ end
359
+
360
+ def self.get_uname_from_system
361
+ (`uname -a 2>/dev/null` || '').strip
362
+ rescue Errno::ENOENT
363
+ "uname executable not found"
364
+ rescue Errno::ENOMEM # couldn't create subprocess
365
+ "uname lookup failed"
366
+ end
367
+
368
+ def self.get_uname_from_system_ver
369
+ (`ver` || '').strip
370
+ rescue Errno::ENOENT
371
+ "ver executable not found"
372
+ rescue Errno::ENOMEM # couldn't create subprocess
373
+ "uname lookup failed"
374
+ end
375
+
376
+ def initialize
377
+ @uname = self.class.get_uname
378
+ end
379
+
380
+ def user_agent
381
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
382
+
383
+ {
384
+ :bindings_version => Stripe::VERSION,
385
+ :lang => 'ruby',
386
+ :lang_version => lang_version,
387
+ :platform => RUBY_PLATFORM,
388
+ :engine => defined?(RUBY_ENGINE) ? RUBY_ENGINE : '',
389
+ :publisher => 'stripe',
390
+ :uname => @uname,
391
+ :hostname => Socket.gethostname,
392
+ }
393
+ end
394
+ end
395
+ end
396
+ end