Allow StripeClient to be configured per instance (#968)

This changes allows for each instance of StripeClient to have its own
configuration object instead of relying on the global config. Each
instance can be configured to override any global config values
previously set.
This commit is contained in:
Joel Taylor 2021-04-01 14:19:38 -07:00 committed by GitHub
parent f864e68bf7
commit 21643f0716
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 438 additions and 106 deletions

View File

@ -62,6 +62,8 @@ module Stripe
class << self class << self
extend Forwardable extend Forwardable
attr_reader :configuration
# User configurable options # User configurable options
def_delegators :@configuration, :api_key, :api_key= def_delegators :@configuration, :api_key, :api_key=
def_delegators :@configuration, :api_version, :api_version= def_delegators :@configuration, :api_version, :api_version=

View File

@ -15,8 +15,10 @@ module Stripe
# by `StripeClient` to determine whether a connection manager should be # by `StripeClient` to determine whether a connection manager should be
# garbage collected or not. # garbage collected or not.
attr_reader :last_used attr_reader :last_used
attr_reader :config
def initialize def initialize(config = Stripe.configuration)
@config = config
@active_connections = {} @active_connections = {}
@last_used = Util.monotonic_time @last_used = Util.monotonic_time
@ -117,17 +119,17 @@ module Stripe
# reused Go's default for `DefaultTransport`. # reused Go's default for `DefaultTransport`.
connection.keep_alive_timeout = 30 connection.keep_alive_timeout = 30
connection.open_timeout = Stripe.open_timeout connection.open_timeout = config.open_timeout
connection.read_timeout = Stripe.read_timeout connection.read_timeout = config.read_timeout
if connection.respond_to?(:write_timeout=) if connection.respond_to?(:write_timeout=)
connection.write_timeout = Stripe.write_timeout connection.write_timeout = config.write_timeout
end end
connection.use_ssl = uri.scheme == "https" connection.use_ssl = uri.scheme == "https"
if Stripe.verify_ssl_certs if config.verify_ssl_certs
connection.verify_mode = OpenSSL::SSL::VERIFY_PEER connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
connection.cert_store = Stripe.ca_store connection.cert_store = config.ca_store
else else
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
warn_ssl_verify_none warn_ssl_verify_none
@ -141,10 +143,10 @@ module Stripe
# out those pieces to make passing them into a new connection a little less # out those pieces to make passing them into a new connection a little less
# ugly. # ugly.
private def proxy_parts private def proxy_parts
if Stripe.proxy.nil? if config.proxy.nil?
[nil, nil, nil, nil] [nil, nil, nil, nil]
else else
u = URI.parse(Stripe.proxy) u = URI.parse(config.proxy)
[u.host, u.port, u.user, u.password] [u.host, u.port, u.user, u.password]
end end
end end

View File

@ -7,8 +7,8 @@ module Stripe
def self.execute_resource_request(method, url, params, opts) def self.execute_resource_request(method, url, params, opts)
opts = Util.normalize_opts(opts) opts = Util.normalize_opts(opts)
opts[:client] ||= StripeClient.active_client opts[:client] ||= opts[:client] || StripeClient.active_client
opts[:api_base] ||= Stripe.connect_base opts[:api_base] ||= opts[:client].config.connect_base
super(method, url, params, opts) super(method, url, params, opts)
end end
@ -29,7 +29,8 @@ module Stripe
end end
def self.authorize_url(params = {}, opts = {}) def self.authorize_url(params = {}, opts = {})
base = opts[:connect_base] || Stripe.connect_base client = opts[:client] || StripeClient.active_client
base = opts[:connect_base] || client.config.connect_base
path = "/oauth/authorize" path = "/oauth/authorize"
path = "/express" + path if opts[:express] path = "/express" + path if opts[:express]

View File

@ -45,12 +45,8 @@ module Stripe
end end
# @override To make id optional # @override To make id optional
def self.retrieve(id = ARGUMENT_NOT_PROVIDED, opts = {}) def self.retrieve(id = nil, opts = {})
id = if id.equal?(ARGUMENT_NOT_PROVIDED) Util.check_string_argument!(id) if id
nil
else
Util.check_string_argument!(id)
end
# Account used to be a singleton, where this method's signature was # Account used to be a singleton, where this method's signature was
# `(opts={})`. For the sake of not breaking folks who pass in an OAuth # `(opts={})`. For the sake of not breaking folks who pass in an OAuth
@ -136,11 +132,10 @@ module Stripe
client_id: client_id, client_id: client_id,
stripe_user_id: id, stripe_user_id: id,
} }
opts = @opts.merge(Util.normalize_opts(opts))
OAuth.deauthorize(params, opts) OAuth.deauthorize(params, opts)
end end
ARGUMENT_NOT_PROVIDED = Object.new
private def serialize_additional_owners(legal_entity, additional_owners) private def serialize_additional_owners(legal_entity, additional_owners)
original_value = original_value =
legal_entity legal_entity

View File

@ -25,8 +25,9 @@ module Stripe
end end
end end
config = opts[:client]&.config || Stripe.configuration
opts = { opts = {
api_base: Stripe.uploads_base, api_base: config.uploads_base,
content_type: MultipartEncoder::MULTIPART_FORM_DATA, content_type: MultipartEncoder::MULTIPART_FORM_DATA,
}.merge(Util.normalize_opts(opts)) }.merge(Util.normalize_opts(opts))
super super

View File

@ -9,19 +9,35 @@ module Stripe
class StripeClient class StripeClient
# A set of all known thread contexts across all threads and a mutex to # A set of all known thread contexts across all threads and a mutex to
# synchronize global access to them. # synchronize global access to them.
@thread_contexts_with_connection_managers = [] @thread_contexts_with_connection_managers = Set.new
@thread_contexts_with_connection_managers_mutex = Mutex.new @thread_contexts_with_connection_managers_mutex = Mutex.new
@last_connection_manager_gc = Util.monotonic_time @last_connection_manager_gc = Util.monotonic_time
# Initializes a new `StripeClient`. # Initializes a new StripeClient
# def initialize(config_overrides = {})
# Takes a connection manager object for backwards compatibility only, and
# that use is DEPRECATED.
def initialize(_connection_manager = nil)
@system_profiler = SystemProfiler.new @system_profiler = SystemProfiler.new
@last_request_metrics = nil @last_request_metrics = nil
# Supports accepting a connection manager object for backwards
# compatibility only, and that use is DEPRECATED.
@config_overrides = case config_overrides
when Stripe::ConnectionManager
{}
when String
{ api_key: config_overrides }
else
config_overrides
end
end end
# Always base config off the global Stripe configuration to ensure the
# client picks up any changes to the config.
def config
Stripe.configuration.reverse_duplicate_merge(@config_overrides)
end
attr_reader :options
# Gets a currently active `StripeClient`. Set for the current thread when # Gets a currently active `StripeClient`. Set for the current thread when
# `StripeClient#request` is being run so that API operations being executed # `StripeClient#request` is being run so that API operations being executed
# inside of that block can find the currently active client. It's reset to # inside of that block can find the currently active client. It's reset to
@ -51,8 +67,8 @@ module Stripe
# its connection manager and remove our reference to it. If it ever # its connection manager and remove our reference to it. If it ever
# makes a new request we'll give it a new connection manager and # makes a new request we'll give it a new connection manager and
# it'll go back into `@thread_contexts_with_connection_managers`. # it'll go back into `@thread_contexts_with_connection_managers`.
thread_context.default_connection_manager.clear thread_context.default_connection_managers.map { |_, cm| cm.clear }
thread_context.default_connection_manager = nil thread_context.reset_connection_managers
end end
@thread_contexts_with_connection_managers.clear @thread_contexts_with_connection_managers.clear
end end
@ -63,10 +79,11 @@ module Stripe
current_thread_context.default_client ||= StripeClient.new current_thread_context.default_client ||= StripeClient.new
end end
# A default connection manager for the current thread. # A default connection manager for the current thread scoped to the
def self.default_connection_manager # configuration object that may be provided.
current_thread_context.default_connection_manager ||= begin def self.default_connection_manager(config = Stripe.configuration)
connection_manager = ConnectionManager.new current_thread_context.default_connection_managers[config.key] ||= begin
connection_manager = ConnectionManager.new(config)
@thread_contexts_with_connection_managers_mutex.synchronize do @thread_contexts_with_connection_managers_mutex.synchronize do
maybe_gc_connection_managers maybe_gc_connection_managers
@ -80,8 +97,9 @@ module Stripe
# Checks if an error is a problem that we should retry on. This includes # Checks if an error is a problem that we should retry on. This includes
# both socket errors that may represent an intermittent problem and some # both socket errors that may represent an intermittent problem and some
# special HTTP statuses. # special HTTP statuses.
def self.should_retry?(error, method:, num_retries:) def self.should_retry?(error,
return false if num_retries >= Stripe.max_network_retries method:, num_retries:, config: Stripe.configuration)
return false if num_retries >= config.max_network_retries
case error case error
when Net::OpenTimeout, Net::ReadTimeout when Net::OpenTimeout, Net::ReadTimeout
@ -127,13 +145,13 @@ module Stripe
end end
end end
def self.sleep_time(num_retries) def self.sleep_time(num_retries, config: Stripe.configuration)
# Apply exponential backoff with initial_network_retry_delay on the # Apply exponential backoff with initial_network_retry_delay on the
# number of num_retries so far as inputs. Do not allow the number to # number of num_retries so far as inputs. Do not allow the number to
# exceed max_network_retry_delay. # exceed max_network_retry_delay.
sleep_seconds = [ sleep_seconds = [
Stripe.initial_network_retry_delay * (2**(num_retries - 1)), config.initial_network_retry_delay * (2**(num_retries - 1)),
Stripe.max_network_retry_delay, config.max_network_retry_delay,
].min ].min
# Apply some jitter by randomizing the value in the range of # Apply some jitter by randomizing the value in the range of
@ -141,9 +159,7 @@ module Stripe
sleep_seconds *= (0.5 * (1 + rand)) sleep_seconds *= (0.5 * (1 + rand))
# But never sleep less than the base sleep seconds. # But never sleep less than the base sleep seconds.
sleep_seconds = [Stripe.initial_network_retry_delay, sleep_seconds].max [config.initial_network_retry_delay, sleep_seconds].max
sleep_seconds
end end
# Gets the connection manager in use for the current `StripeClient`. # Gets the connection manager in use for the current `StripeClient`.
@ -187,8 +203,8 @@ module Stripe
raise ArgumentError, "path should be a string" \ raise ArgumentError, "path should be a string" \
unless path.is_a?(String) unless path.is_a?(String)
api_base ||= Stripe.api_base api_base ||= config.api_base
api_key ||= Stripe.api_key api_key ||= config.api_key
params = Util.objects_to_ids(params) params = Util.objects_to_ids(params)
check_api_key!(api_key) check_api_key!(api_key)
@ -231,10 +247,12 @@ module Stripe
context.query = query context.query = query
http_resp = execute_request_with_rescues(method, api_base, context) do http_resp = execute_request_with_rescues(method, api_base, context) do
self.class.default_connection_manager.execute_request(method, url, self.class
body: body, .default_connection_manager(config)
headers: headers, .execute_request(method, url,
query: query) body: body,
headers: headers,
query: query)
end end
begin begin
@ -246,13 +264,21 @@ module Stripe
# If being called from `StripeClient#request`, put the last response in # If being called from `StripeClient#request`, put the last response in
# thread-local memory so that it can be returned to the user. Don't store # thread-local memory so that it can be returned to the user. Don't store
# anything otherwise so that we don't leak memory. # anything otherwise so that we don't leak memory.
if self.class.current_thread_context.last_responses&.key?(object_id) store_last_response(object_id, resp)
self.class.current_thread_context.last_responses[object_id] = resp
end
[resp, api_key] [resp, api_key]
end end
def store_last_response(object_id, resp)
return unless last_response_has_key?(object_id)
self.class.current_thread_context.last_responses[object_id] = resp
end
def last_response_has_key?(object_id)
self.class.current_thread_context.last_responses&.key?(object_id)
end
# #
# private # private
# #
@ -328,11 +354,6 @@ module Stripe
# the user hasn't specified their own. # the user hasn't specified their own.
attr_accessor :default_client attr_accessor :default_client
# A default `ConnectionManager` for the thread. Normally shared between
# all `StripeClient` objects on a particular thread, and created so as to
# minimize the number of open connections that an application needs.
attr_accessor :default_connection_manager
# A temporary map of object IDs to responses from last executed API # A temporary map of object IDs to responses from last executed API
# calls. Used to return a responses from calls to `StripeClient#request`. # calls. Used to return a responses from calls to `StripeClient#request`.
# #
@ -345,6 +366,17 @@ module Stripe
# because that's wrapped in an `ensure` block, they should never leave # because that's wrapped in an `ensure` block, they should never leave
# garbage in `Thread.current`. # garbage in `Thread.current`.
attr_accessor :last_responses attr_accessor :last_responses
# A map of connection mangers for the thread. Normally shared between
# all `StripeClient` objects on a particular thread, and created so as to
# minimize the number of open connections that an application needs.
def default_connection_managers
@default_connection_managers ||= {}
end
def reset_connection_managers
@default_connection_managers = {}
end
end end
# Access data stored for `StripeClient` within the thread's current # Access data stored for `StripeClient` within the thread's current
@ -382,11 +414,19 @@ module Stripe
pruned_thread_contexts = [] pruned_thread_contexts = []
@thread_contexts_with_connection_managers.each do |thread_context| @thread_contexts_with_connection_managers.each do |thread_context|
connection_manager = thread_context.default_connection_manager thread_context
next if connection_manager.last_used > last_used_threshold .default_connection_managers
.each do |config_key, connection_manager|
next if connection_manager.last_used > last_used_threshold
connection_manager.clear
thread_context.default_connection_managers.delete(config_key)
end
end
@thread_contexts_with_connection_managers.each do |thread_context|
next unless thread_context.default_connection_managers.empty?
connection_manager.clear
thread_context.default_connection_manager = nil
pruned_thread_contexts << thread_context pruned_thread_contexts << thread_context
end end
@ -397,7 +437,7 @@ module Stripe
end end
private def api_url(url = "", api_base = nil) private def api_url(url = "", api_base = nil)
(api_base || Stripe.api_base) + url (api_base || config.api_base) + url
end end
private def check_api_key!(api_key) private def check_api_key!(api_key)
@ -471,7 +511,7 @@ module Stripe
notify_request_end(context, request_duration, http_status, notify_request_end(context, request_duration, http_status,
num_retries, user_data) num_retries, user_data)
if Stripe.enable_telemetry? && context.request_id if config.enable_telemetry? && context.request_id
request_duration_ms = (request_duration * 1000).to_i request_duration_ms = (request_duration * 1000).to_i
@last_request_metrics = @last_request_metrics =
StripeRequestMetrics.new(context.request_id, request_duration_ms) StripeRequestMetrics.new(context.request_id, request_duration_ms)
@ -498,9 +538,12 @@ module Stripe
notify_request_end(context, request_duration, http_status, num_retries, notify_request_end(context, request_duration, http_status, num_retries,
user_data) user_data)
if self.class.should_retry?(e, method: method, num_retries: num_retries) if self.class.should_retry?(e,
method: method,
num_retries: num_retries,
config: config)
num_retries += 1 num_retries += 1
sleep self.class.sleep_time(num_retries) sleep self.class.sleep_time(num_retries, config: config)
retry retry
end end
@ -622,7 +665,8 @@ module Stripe
error_param: error_data[:param], error_param: error_data[:param],
error_type: error_data[:type], error_type: error_data[:type],
idempotency_key: context.idempotency_key, idempotency_key: context.idempotency_key,
request_id: context.request_id) request_id: context.request_id,
config: config)
# The standard set of arguments that can be used to initialize most of # The standard set of arguments that can be used to initialize most of
# the exceptions. # the exceptions.
@ -671,7 +715,8 @@ module Stripe
error_code: error_code, error_code: error_code,
error_description: description, error_description: description,
idempotency_key: context.idempotency_key, idempotency_key: context.idempotency_key,
request_id: context.request_id) request_id: context.request_id,
config: config)
args = { args = {
http_status: resp.http_status, http_body: resp.http_body, http_status: resp.http_status, http_body: resp.http_body,
@ -703,7 +748,8 @@ module Stripe
Util.log_error("Stripe network error", Util.log_error("Stripe network error",
error_message: error.message, error_message: error.message,
idempotency_key: context.idempotency_key, idempotency_key: context.idempotency_key,
request_id: context.request_id) request_id: context.request_id,
config: config)
errors, message = NETWORK_ERROR_MESSAGES_MAP.detect do |(e, _)| errors, message = NETWORK_ERROR_MESSAGES_MAP.detect do |(e, _)|
error.is_a?(e) error.is_a?(e)
@ -714,7 +760,7 @@ module Stripe
"with Stripe. Please let us know at support@stripe.com." "with Stripe. Please let us know at support@stripe.com."
end end
api_base ||= Stripe.api_base api_base ||= config.api_base
message = message % api_base message = message % api_base
message += " Request was retried #{num_retries} times." if num_retries > 0 message += " Request was retried #{num_retries} times." if num_retries > 0
@ -735,7 +781,7 @@ module Stripe
"Content-Type" => "application/x-www-form-urlencoded", "Content-Type" => "application/x-www-form-urlencoded",
} }
if Stripe.enable_telemetry? && !@last_request_metrics.nil? if config.enable_telemetry? && !@last_request_metrics.nil?
headers["X-Stripe-Client-Telemetry"] = JSON.generate( headers["X-Stripe-Client-Telemetry"] = JSON.generate(
last_request_metrics: @last_request_metrics.payload last_request_metrics: @last_request_metrics.payload
) )
@ -743,12 +789,12 @@ module Stripe
# It is only safe to retry network failures on post and delete # It is only safe to retry network failures on post and delete
# requests if we add an Idempotency-Key header # requests if we add an Idempotency-Key header
if %i[post delete].include?(method) && Stripe.max_network_retries > 0 if %i[post delete].include?(method) && config.max_network_retries > 0
headers["Idempotency-Key"] ||= SecureRandom.uuid headers["Idempotency-Key"] ||= SecureRandom.uuid
end end
headers["Stripe-Version"] = Stripe.api_version if Stripe.api_version headers["Stripe-Version"] = config.api_version if config.api_version
headers["Stripe-Account"] = Stripe.stripe_account if Stripe.stripe_account headers["Stripe-Account"] = config.stripe_account if config.stripe_account
user_agent = @system_profiler.user_agent user_agent = @system_profiler.user_agent
begin begin
@ -772,11 +818,13 @@ module Stripe
idempotency_key: context.idempotency_key, idempotency_key: context.idempotency_key,
method: context.method, method: context.method,
num_retries: num_retries, num_retries: num_retries,
path: context.path) path: context.path,
config: config)
Util.log_debug("Request details", Util.log_debug("Request details",
body: context.body, body: context.body,
idempotency_key: context.idempotency_key, idempotency_key: context.idempotency_key,
query: context.query) query: context.query,
config: config)
end end
private def log_response(context, request_start, status, body) private def log_response(context, request_start, status, body)
@ -788,11 +836,13 @@ module Stripe
method: context.method, method: context.method,
path: context.path, path: context.path,
request_id: context.request_id, request_id: context.request_id,
status: status) status: status,
config: config)
Util.log_debug("Response details", Util.log_debug("Response details",
body: body, body: body,
idempotency_key: context.idempotency_key, idempotency_key: context.idempotency_key,
request_id: context.request_id) request_id: context.request_id,
config: config)
return unless context.request_id return unless context.request_id
@ -800,7 +850,8 @@ module Stripe
idempotency_key: context.idempotency_key, idempotency_key: context.idempotency_key,
request_id: context.request_id, request_id: context.request_id,
url: Util.request_id_dashboard_url(context.request_id, url: Util.request_id_dashboard_url(context.request_id,
context.api_key)) context.api_key),
config: config)
end end
private def log_response_error(context, request_start, error) private def log_response_error(context, request_start, error)
@ -810,7 +861,8 @@ module Stripe
error_message: error.message, error_message: error.message,
idempotency_key: context.idempotency_key, idempotency_key: context.idempotency_key,
method: context.method, method: context.method,
path: context.path) path: context.path,
config: config)
end end
# RequestLogContext stores information about a request that's begin made so # RequestLogContext stores information about a request that's begin made so

View File

@ -101,6 +101,14 @@ module Stripe
@max_network_retries = val.to_i @max_network_retries = val.to_i
end end
def max_network_retry_delay=(val)
@max_network_retry_delay = val.to_i
end
def initial_network_retry_delay=(val)
@initial_network_retry_delay = val.to_i
end
def open_timeout=(open_timeout) def open_timeout=(open_timeout)
@open_timeout = open_timeout @open_timeout = open_timeout
StripeClient.clear_all_connection_managers StripeClient.clear_all_connection_managers
@ -174,5 +182,13 @@ module Stripe
def enable_telemetry? def enable_telemetry?
enable_telemetry enable_telemetry
end end
# Generates a deterministic key to identify configuration objects with
# identical configuration values.
def key
instance_variables
.collect { |variable| instance_variable_get(variable) }
.join
end
end end
end end

View File

@ -76,24 +76,30 @@ module Stripe
end end
def self.log_error(message, data = {}) def self.log_error(message, data = {})
if !Stripe.logger.nil? || config = data.delete(:config) || Stripe.configuration
!Stripe.log_level.nil? && Stripe.log_level <= Stripe::LEVEL_ERROR logger = config.logger || Stripe.logger
if !logger.nil? ||
!config.log_level.nil? && config.log_level <= Stripe::LEVEL_ERROR
log_internal(message, data, color: :cyan, level: Stripe::LEVEL_ERROR, log_internal(message, data, color: :cyan, level: Stripe::LEVEL_ERROR,
logger: Stripe.logger, out: $stderr) logger: Stripe.logger, out: $stderr)
end end
end end
def self.log_info(message, data = {}) def self.log_info(message, data = {})
if !Stripe.logger.nil? || config = data.delete(:config) || Stripe.configuration
!Stripe.log_level.nil? && Stripe.log_level <= Stripe::LEVEL_INFO logger = config.logger || Stripe.logger
if !logger.nil? ||
!config.log_level.nil? && config.log_level <= Stripe::LEVEL_INFO
log_internal(message, data, color: :cyan, level: Stripe::LEVEL_INFO, log_internal(message, data, color: :cyan, level: Stripe::LEVEL_INFO,
logger: Stripe.logger, out: $stdout) logger: Stripe.logger, out: $stdout)
end end
end end
def self.log_debug(message, data = {}) def self.log_debug(message, data = {})
if !Stripe.logger.nil? || config = data.delete(:config) || Stripe.configuration
!Stripe.log_level.nil? && Stripe.log_level <= Stripe::LEVEL_DEBUG logger = config.logger || Stripe.logger
if !logger.nil? ||
!config.log_level.nil? && config.log_level <= Stripe::LEVEL_DEBUG
log_internal(message, data, color: :blue, level: Stripe::LEVEL_DEBUG, log_internal(message, data, color: :blue, level: Stripe::LEVEL_DEBUG,
logger: Stripe.logger, out: $stdout) logger: Stripe.logger, out: $stdout)
end end

View File

@ -79,6 +79,49 @@ module Stripe
end end
end end
context "when a StripeClient has different configurations" do
should "correctly initialize a connection" do
old_proxy = Stripe.proxy
old_open_timeout = Stripe.open_timeout
old_read_timeout = Stripe.read_timeout
begin
client = StripeClient.new(
proxy: "http://other:pass@localhost:8080",
open_timeout: 400,
read_timeout: 500,
verify_ssl_certs: true
)
conn = Stripe::ConnectionManager.new(client.config)
.connection_for("https://stripe.com")
# Host/port
assert_equal "stripe.com", conn.address
assert_equal 443, conn.port
# Proxy
assert_equal "localhost", conn.proxy_address
assert_equal 8080, conn.proxy_port
assert_equal "other", conn.proxy_user
assert_equal "pass", conn.proxy_pass
# Timeouts
assert_equal 400, conn.open_timeout
assert_equal 500, conn.read_timeout
assert_equal true, conn.use_ssl?
assert_equal OpenSSL::SSL::VERIFY_PEER, conn.verify_mode
assert_equal Stripe.ca_store, conn.cert_store
ensure
Stripe.proxy = old_proxy
Stripe.open_timeout = old_open_timeout
Stripe.read_timeout = old_read_timeout
end
end
end
should "produce the same connection multiple times" do should "produce the same connection multiple times" do
conn1 = @manager.connection_for("https://stripe.com") conn1 = @manager.connection_for("https://stripe.com")
conn2 = @manager.connection_for("https://stripe.com") conn2 = @manager.connection_for("https://stripe.com")

View File

@ -44,6 +44,14 @@ module Stripe
assert_equal("connect.stripe.com", uri.host) assert_equal("connect.stripe.com", uri.host)
assert_equal("/express/oauth/authorize", uri.path) assert_equal("/express/oauth/authorize", uri.path)
end end
should "override the api base path when a StripeClient is provided" do
client = Stripe::StripeClient.new(connect_base: "https://other.stripe.com")
uri_str = OAuth.authorize_url({}, client: client)
uri = URI.parse(uri_str)
assert_equal("other.stripe.com", uri.host)
end
end end
context ".token" do context ".token" do
@ -83,6 +91,29 @@ module Stripe
code: "this_is_an_authorization_code") code: "this_is_an_authorization_code")
assert_equal("another_access_token", resp.access_token) assert_equal("another_access_token", resp.access_token)
end end
should "override the api base path when a StripeClient is provided" do
stub_request(:post, "https://other.stripe.com/oauth/token")
.with(body: {
"grant_type" => "authorization_code",
"code" => "this_is_an_authorization_code",
})
.to_return(body: JSON.generate(access_token: "sk_access_token",
scope: "read_only",
livemode: false,
token_type: "bearer",
refresh_token: "sk_refresh_token",
stripe_user_id: "acct_test",
stripe_publishable_key: "pk_test"))
client = Stripe::StripeClient.new(connect_base: "https://other.stripe.com")
resp = OAuth.token(
{ grant_type: "authorization_code", code: "this_is_an_authorization_code" },
client: client
)
assert_equal("sk_access_token", resp.access_token)
end
end end
context ".deauthorize" do context ".deauthorize" do
@ -99,6 +130,20 @@ module Stripe
resp = OAuth.deauthorize(stripe_user_id: "acct_test_deauth") resp = OAuth.deauthorize(stripe_user_id: "acct_test_deauth")
assert_equal("acct_test_deauth", resp.stripe_user_id) assert_equal("acct_test_deauth", resp.stripe_user_id)
end end
should "override the api base path when a StripeClient is provided" do
stub_request(:post, "https://other.stripe.com/oauth/deauthorize")
.with(body: {
"client_id" => "ca_test",
"stripe_user_id" => "acct_test_deauth",
})
.to_return(body: JSON.generate(stripe_user_id: "acct_test_deauth"))
client = Stripe::StripeClient.new(connect_base: "https://other.stripe.com")
resp = OAuth.deauthorize({ stripe_user_id: "acct_test_deauth" }, client: client)
assert_equal("acct_test_deauth", resp.stripe_user_id)
end
end end
end end
end end

View File

@ -4,6 +4,24 @@ require ::File.expand_path("../test_helper", __dir__)
module Stripe module Stripe
class StripeClientTest < Test::Unit::TestCase class StripeClientTest < Test::Unit::TestCase
context "initializing a StripeClient" do
should "allow a String to be passed in order to set the api key" do
assert_equal StripeClient.new("test_123").config.api_key, "test_123"
end
should "allow for overrides via a Hash" do
config = { api_key: "test_123", open_timeout: 100 }
client = StripeClient.new(config)
assert_equal client.config.api_key, "test_123"
assert_equal client.config.open_timeout, 100
end
should "support deprecated ConnectionManager objects" do
assert StripeClient.new(Stripe::ConnectionManager.new).config.is_a?(Stripe::StripeConfiguration)
end
end
context ".active_client" do context ".active_client" do
should "be .default_client outside of #request" do should "be .default_client outside of #request" do
assert_equal StripeClient.default_client, StripeClient.active_client assert_equal StripeClient.default_client, StripeClient.active_client
@ -82,8 +100,64 @@ module Stripe
assert_equal 1, StripeClient.maybe_gc_connection_managers assert_equal 1, StripeClient.maybe_gc_connection_managers
# And as an additional check, the connection manager of the current # And as an additional check, the connection manager of the current
# thread context should have been set to `nil` as it was GCed. # thread context should have been removed as it was GCed.
assert_nil StripeClient.current_thread_context.default_connection_manager assert_equal({}, StripeClient.current_thread_context.default_connection_managers)
end
should "only garbage collect when all connection managers for a thread are expired" do
stub_request(:post, "#{Stripe.api_base}/v1/path")
.to_return(body: JSON.generate(object: "account"))
# Make sure we start with a blank slate (state may have been left in
# place by other tests).
StripeClient.clear_all_connection_managers
# Establish a base time.
t = 0.0
# And pretend that `StripeClient` was just initialized for the first
# time. (Don't access instance variables like this, but it's tricky to
# test properly otherwise.)
StripeClient.instance_variable_set(:@last_connection_manager_gc, t)
#
# t
#
Util.stubs(:monotonic_time).returns(t)
# Execute an initial request to ensure that a connection manager was
# created.
client = StripeClient.new
client.execute_request(:post, "/v1/path")
# Create a new client with a unique config to make sure the thread has two
# connection managers
active_client = StripeClient.new(max_network_retries: 10)
active_client.execute_request(:post, "/v1/path")
assert_equal 2, StripeClient.current_thread_context.default_connection_managers.keys.count
assert_equal nil, StripeClient.maybe_gc_connection_managers
# t + StripeClient::CONNECTION_MANAGER_GC_LAST_USED_EXPIRY + 1
#
# Move us far enough into the future that we're passed the horizons for
# both a GC run as well as well as the expiry age of a connection
# manager. That means the GC will run and collect the connection
# manager that we created above.
#
Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_LAST_USED_EXPIRY + 1)
# Manually set the active_client's last_used time into the future to prevent GC.
StripeClient.default_connection_manager(active_client.config)
.instance_variable_set(:@last_used, Util.monotonic_time + 1)
assert_equal 0, StripeClient.maybe_gc_connection_managers
# Move time into the future past the last GC round
current_time = Util.monotonic_time
Util.stubs(:monotonic_time).returns(current_time * 2)
assert_equal 1, StripeClient.maybe_gc_connection_managers
end end
end end
@ -160,11 +234,26 @@ module Stripe
thread.join thread.join
refute_equal StripeClient.default_connection_manager, other_thread_manager refute_equal StripeClient.default_connection_manager, other_thread_manager
end end
should "create a separate connection manager per configuration" do
config = Stripe::StripeConfiguration.setup { |c| c.open_timeout = 100 }
connection_manager_one = StripeClient.default_connection_manager
connection_manager_two = StripeClient.default_connection_manager(config)
assert_equal connection_manager_one.config.open_timeout, 30
assert_equal connection_manager_two.config.open_timeout, 100
end
should "create a single connection manager for identitical configurations" do
2.times { StripeClient.default_connection_manager(Stripe::StripeConfiguration.setup) }
assert_equal 1, StripeClient.instance_variable_get(:@thread_contexts_with_connection_managers).first.default_connection_managers.size
end
end end
context ".should_retry?" do context ".should_retry?" do
setup do setup do
Stripe.stubs(:max_network_retries).returns(2) Stripe::StripeConfiguration.any_instance.stubs(:max_network_retries).returns(2)
end end
should "retry on Errno::ECONNREFUSED" do should "retry on Errno::ECONNREFUSED" do
@ -275,7 +364,7 @@ module Stripe
context ".sleep_time" do context ".sleep_time" do
should "should grow exponentially" do should "should grow exponentially" do
StripeClient.stubs(:rand).returns(1) StripeClient.stubs(:rand).returns(1)
Stripe.stubs(:max_network_retry_delay).returns(999) Stripe.configuration.stubs(:max_network_retry_delay).returns(999)
assert_equal(Stripe.initial_network_retry_delay, StripeClient.sleep_time(1)) assert_equal(Stripe.initial_network_retry_delay, StripeClient.sleep_time(1))
assert_equal(Stripe.initial_network_retry_delay * 2, StripeClient.sleep_time(2)) assert_equal(Stripe.initial_network_retry_delay * 2, StripeClient.sleep_time(2))
assert_equal(Stripe.initial_network_retry_delay * 4, StripeClient.sleep_time(3)) assert_equal(Stripe.initial_network_retry_delay * 4, StripeClient.sleep_time(3))
@ -284,8 +373,8 @@ module Stripe
should "enforce the max_network_retry_delay" do should "enforce the max_network_retry_delay" do
StripeClient.stubs(:rand).returns(1) StripeClient.stubs(:rand).returns(1)
Stripe.stubs(:initial_network_retry_delay).returns(1) Stripe.configuration.stubs(:initial_network_retry_delay).returns(1)
Stripe.stubs(:max_network_retry_delay).returns(2) Stripe.configuration.stubs(:max_network_retry_delay).returns(2)
assert_equal(1, StripeClient.sleep_time(1)) assert_equal(1, StripeClient.sleep_time(1))
assert_equal(2, StripeClient.sleep_time(2)) assert_equal(2, StripeClient.sleep_time(2))
assert_equal(2, StripeClient.sleep_time(3)) assert_equal(2, StripeClient.sleep_time(3))
@ -295,8 +384,8 @@ module Stripe
should "add some randomness" do should "add some randomness" do
random_value = 0.8 random_value = 0.8
StripeClient.stubs(:rand).returns(random_value) StripeClient.stubs(:rand).returns(random_value)
Stripe.stubs(:initial_network_retry_delay).returns(1) Stripe.configuration.stubs(:initial_network_retry_delay).returns(1)
Stripe.stubs(:max_network_retry_delay).returns(8) Stripe.configuration.stubs(:max_network_retry_delay).returns(8)
base_value = Stripe.initial_network_retry_delay * (0.5 * (1 + random_value)) base_value = Stripe.initial_network_retry_delay * (0.5 * (1 + random_value))
@ -309,6 +398,23 @@ module Stripe
assert_equal(base_value * 4, StripeClient.sleep_time(3)) assert_equal(base_value * 4, StripeClient.sleep_time(3))
assert_equal(base_value * 8, StripeClient.sleep_time(4)) assert_equal(base_value * 8, StripeClient.sleep_time(4))
end end
should "permit passing in a configuration object" do
StripeClient.stubs(:rand).returns(1)
config = Stripe::StripeConfiguration.setup do |c|
c.initial_network_retry_delay = 1
c.max_network_retry_delay = 2
end
# Set the global configuration to be different than the client
Stripe.configuration.stubs(:initial_network_retry_delay).returns(100)
Stripe.configuration.stubs(:max_network_retry_delay).returns(200)
assert_equal(1, StripeClient.sleep_time(1, config: config))
assert_equal(2, StripeClient.sleep_time(2, config: config))
assert_equal(2, StripeClient.sleep_time(3, config: config))
assert_equal(2, StripeClient.sleep_time(4, config: config))
end
end end
context "#execute_request" do context "#execute_request" do
@ -342,6 +448,10 @@ module Stripe
# switch over to rspec-mocks at some point, we can probably remove # switch over to rspec-mocks at some point, we can probably remove
# this. # this.
Util.stubs(:monotonic_time).returns(0.0) Util.stubs(:monotonic_time).returns(0.0)
# Stub the Stripe configuration so that mocha matchers will succeed. Currently,
# mocha does not support using param matchers within hashes.
StripeClient.any_instance.stubs(:config).returns(Stripe.configuration)
end end
should "produce appropriate logging" do should "produce appropriate logging" do
@ -353,11 +463,13 @@ module Stripe
idempotency_key: "abc", idempotency_key: "abc",
method: :post, method: :post,
num_retries: 0, num_retries: 0,
path: "/v1/account") path: "/v1/account",
config: Stripe.configuration)
Util.expects(:log_debug).with("Request details", Util.expects(:log_debug).with("Request details",
body: "", body: "",
idempotency_key: "abc", idempotency_key: "abc",
query: nil) query: nil,
config: Stripe.configuration)
Util.expects(:log_info).with("Response from Stripe API", Util.expects(:log_info).with("Response from Stripe API",
account: "acct_123", account: "acct_123",
@ -367,15 +479,18 @@ module Stripe
method: :post, method: :post,
path: "/v1/account", path: "/v1/account",
request_id: "req_123", request_id: "req_123",
status: 200) status: 200,
config: Stripe.configuration)
Util.expects(:log_debug).with("Response details", Util.expects(:log_debug).with("Response details",
body: body, body: body,
idempotency_key: "abc", idempotency_key: "abc",
request_id: "req_123") request_id: "req_123",
config: Stripe.configuration)
Util.expects(:log_debug).with("Dashboard link for request", Util.expects(:log_debug).with("Dashboard link for request",
idempotency_key: "abc", idempotency_key: "abc",
request_id: "req_123", request_id: "req_123",
url: Util.request_id_dashboard_url("req_123", Stripe.api_key)) url: Util.request_id_dashboard_url("req_123", Stripe.api_key),
config: Stripe.configuration)
stub_request(:post, "#{Stripe.api_base}/v1/account") stub_request(:post, "#{Stripe.api_base}/v1/account")
.to_return( .to_return(
@ -404,7 +519,8 @@ module Stripe
idempotency_key: nil, idempotency_key: nil,
method: :post, method: :post,
num_retries: 0, num_retries: 0,
path: "/v1/account") path: "/v1/account",
config: Stripe.configuration)
Util.expects(:log_info).with("Response from Stripe API", Util.expects(:log_info).with("Response from Stripe API",
account: nil, account: nil,
api_version: nil, api_version: nil,
@ -413,7 +529,8 @@ module Stripe
method: :post, method: :post,
path: "/v1/account", path: "/v1/account",
request_id: nil, request_id: nil,
status: 500) status: 500,
config: Stripe.configuration)
error = { error = {
code: "code", code: "code",
@ -428,7 +545,8 @@ module Stripe
error_param: error[:param], error_param: error[:param],
error_type: error[:type], error_type: error[:type],
idempotency_key: nil, idempotency_key: nil,
request_id: nil) request_id: nil,
config: Stripe.configuration)
stub_request(:post, "#{Stripe.api_base}/v1/account") stub_request(:post, "#{Stripe.api_base}/v1/account")
.to_return( .to_return(
@ -449,7 +567,8 @@ module Stripe
idempotency_key: nil, idempotency_key: nil,
method: :post, method: :post,
num_retries: 0, num_retries: 0,
path: "/oauth/token") path: "/oauth/token",
config: Stripe.configuration)
Util.expects(:log_info).with("Response from Stripe API", Util.expects(:log_info).with("Response from Stripe API",
account: nil, account: nil,
api_version: nil, api_version: nil,
@ -458,14 +577,16 @@ module Stripe
method: :post, method: :post,
path: "/oauth/token", path: "/oauth/token",
request_id: nil, request_id: nil,
status: 400) status: 400,
config: Stripe.configuration)
Util.expects(:log_error).with("Stripe OAuth error", Util.expects(:log_error).with("Stripe OAuth error",
status: 400, status: 400,
error_code: "invalid_request", error_code: "invalid_request",
error_description: "No grant type specified", error_description: "No grant type specified",
idempotency_key: nil, idempotency_key: nil,
request_id: nil) request_id: nil,
config: Stripe.configuration)
stub_request(:post, "#{Stripe.connect_base}/oauth/token") stub_request(:post, "#{Stripe.connect_base}/oauth/token")
.to_return(body: JSON.generate(error: "invalid_request", .to_return(body: JSON.generate(error: "invalid_request",
@ -788,7 +909,7 @@ module Stripe
context "idempotency keys" do context "idempotency keys" do
setup do setup do
Stripe.stubs(:max_network_retries).returns(2) Stripe::StripeConfiguration.any_instance.stubs(:max_network_retries).returns(2)
end end
should "not add an idempotency key to GET requests" do should "not add an idempotency key to GET requests" do
@ -838,7 +959,7 @@ module Stripe
context "retry logic" do context "retry logic" do
setup do setup do
Stripe.stubs(:max_network_retries).returns(2) Stripe::StripeConfiguration.any_instance.stubs(:max_network_retries).returns(2)
end end
should "retry failed requests and raise if error persists" do should "retry failed requests and raise if error persists" do
@ -870,6 +991,21 @@ module Stripe
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/charges") client.execute_request(:post, "/v1/charges")
end end
should "pass the client configuration when retrying" do
StripeClient.expects(:sleep_time)
.with(any_of(1, 2),
has_entry(:config, kind_of(Stripe::StripeConfiguration)))
.at_least_once.returns(0)
stub_request(:post, "#{Stripe.api_base}/v1/charges")
.to_raise(Errno::ECONNREFUSED.new)
client = StripeClient.new
assert_raises Stripe::APIConnectionError do
client.execute_request(:post, "/v1/charges")
end
end
end end
context "params serialization" do context "params serialization" do
@ -1079,7 +1215,7 @@ module Stripe
context "#proxy" do context "#proxy" do
should "run the request through the proxy" do should "run the request through the proxy" do
begin begin
StripeClient.current_thread_context.default_connection_manager = nil StripeClient.clear_all_connection_managers
Stripe.proxy = "http://user:pass@localhost:8080" Stripe.proxy = "http://user:pass@localhost:8080"
@ -1095,7 +1231,7 @@ module Stripe
ensure ensure
Stripe.proxy = nil Stripe.proxy = nil
StripeClient.current_thread_context.default_connection_manager = nil StripeClient.clear_all_connection_managers
end end
end end
end end

View File

@ -20,6 +20,7 @@ module Stripe
assert_equal "https://api.stripe.com", config.api_base assert_equal "https://api.stripe.com", config.api_base
assert_equal "https://connect.stripe.com", config.connect_base assert_equal "https://connect.stripe.com", config.connect_base
assert_equal "https://files.stripe.com", config.uploads_base assert_equal "https://files.stripe.com", config.uploads_base
assert_equal nil, config.api_version
end end
should "allow for overrides when a block is passed" do should "allow for overrides when a block is passed" do
@ -41,7 +42,7 @@ module Stripe
c.open_timeout = 100 c.open_timeout = 100
end end
duped_config = config.reverse_duplicate_merge(read_timeout: 500) duped_config = config.reverse_duplicate_merge(read_timeout: 500, api_version: "2018-08-02")
assert_equal config.open_timeout, duped_config.open_timeout assert_equal config.open_timeout, duped_config.open_timeout
assert_equal 500, duped_config.read_timeout assert_equal 500, duped_config.read_timeout
@ -57,6 +58,24 @@ module Stripe
end end
end end
context "#max_network_retry_delay=" do
should "coerce the option into an integer" do
config = Stripe::StripeConfiguration.setup
config.max_network_retry_delay = "10"
assert_equal 10, config.max_network_retry_delay
end
end
context "#initial_network_retry_delay=" do
should "coerce the option into an integer" do
config = Stripe::StripeConfiguration.setup
config.initial_network_retry_delay = "10"
assert_equal 10, config.initial_network_retry_delay
end
end
context "#log_level=" do context "#log_level=" do
should "be backwards compatible with old values" do should "be backwards compatible with old values" do
config = Stripe::StripeConfiguration.setup config = Stripe::StripeConfiguration.setup
@ -127,5 +146,14 @@ module Stripe
config.verify_ssl_certs = false config.verify_ssl_certs = false
end end
end end
context "#key" do
should "generate the same key when values are identicial" do
assert_equal StripeConfiguration.setup.key, StripeConfiguration.setup.key
custom_config = StripeConfiguration.setup { |c| c.open_timeout = 1000 }
refute_equal StripeConfiguration.setup.key, custom_config.key
end
end
end end
end end

View File

@ -114,6 +114,11 @@ class StripeTest < Test::Unit::TestCase
assert_equal "https://other.stripe.com", Stripe.api_base assert_equal "https://other.stripe.com", Stripe.api_base
end end
should "allow api_version to be configured" do
Stripe.api_version = "2018-02-28"
assert_equal "2018-02-28", Stripe.api_version
end
should "allow connect_base to be configured" do should "allow connect_base to be configured" do
Stripe.connect_base = "https://other.stripe.com" Stripe.connect_base = "https://other.stripe.com"
assert_equal "https://other.stripe.com", Stripe.connect_base assert_equal "https://other.stripe.com", Stripe.connect_base