test/stripe/stripe_client_test.rb in stripe-5.30.0 vs test/stripe/stripe_client_test.rb in stripe-5.31.0

- old
+ new

@@ -2,10 +2,34 @@ require ::File.expand_path("../test_helper", __dir__) module Stripe 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 + + should "support passing a full configuration object" do + config = Stripe.config.reverse_duplicate_merge({ api_key: "test_123", open_timeout: 100 }) + client = StripeClient.new(config) + assert_equal config, client.config + end + end + context ".active_client" do should "be .default_client outside of #request" do assert_equal StripeClient.default_client, StripeClient.active_client end @@ -80,13 +104,69 @@ Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_LAST_USED_EXPIRY + 1) assert_equal 1, StripeClient.maybe_gc_connection_managers # And as an additional check, the connection manager of the current - # thread context should have been set to `nil` as it was GCed. - assert_nil StripeClient.current_thread_context.default_connection_manager + # thread context should have been removed as it was GCed. + 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 context ".clear_all_connection_managers" do should "clear connection managers across all threads" do stub_request(:post, "#{Stripe.api_base}/path") @@ -127,10 +207,31 @@ threads.each { send_queue << true } # And finally, give all threads time to perform their check. threads.each(&:join) end + + should "clear only connection managers with a given configuration" do + StripeClient.clear_all_connection_managers + + client1 = StripeClient.new(read_timeout: 5.0) + StripeClient.default_connection_manager(client1.config) + client2 = StripeClient.new(read_timeout: 2.0) + StripeClient.default_connection_manager(client2.config) + + thread_contexts = StripeClient.instance_variable_get(:@thread_contexts_with_connection_managers) + assert_equal 1, thread_contexts.count + thread_context = thread_contexts.first + + refute_nil thread_context.default_connection_managers[client1.config.key] + refute_nil thread_context.default_connection_managers[client2.config.key] + + StripeClient.clear_all_connection_managers(config: client1.config) + + assert_nil thread_context.default_connection_managers[client1.config.key] + refute_nil thread_context.default_connection_managers[client2.config.key] + end end context ".default_client" do should "be a StripeClient" do assert_kind_of StripeClient, StripeClient.default_client @@ -158,15 +259,32 @@ other_thread_manager = StripeClient.default_connection_manager end thread.join refute_equal StripeClient.default_connection_manager, other_thread_manager 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 identical configurations" do + StripeClient.clear_all_connection_managers + + 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 context ".should_retry?" do setup do - Stripe.stubs(:max_network_retries).returns(2) + Stripe::StripeConfiguration.any_instance.stubs(:max_network_retries).returns(2) end should "retry on Errno::ECONNREFUSED" do assert StripeClient.should_retry?(Errno::ECONNREFUSED.new, method: :post, num_retries: 0) @@ -273,32 +391,32 @@ end context ".sleep_time" do should "should grow exponentially" do StripeClient.stubs(:rand).returns(1) - Stripe.stubs(:max_network_retry_delay).returns(999) + Stripe.config.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 * 2, StripeClient.sleep_time(2)) assert_equal(Stripe.initial_network_retry_delay * 4, StripeClient.sleep_time(3)) assert_equal(Stripe.initial_network_retry_delay * 8, StripeClient.sleep_time(4)) end should "enforce the max_network_retry_delay" do StripeClient.stubs(:rand).returns(1) - Stripe.stubs(:initial_network_retry_delay).returns(1) - Stripe.stubs(:max_network_retry_delay).returns(2) + Stripe.config.stubs(:initial_network_retry_delay).returns(1) + Stripe.config.stubs(:max_network_retry_delay).returns(2) assert_equal(1, StripeClient.sleep_time(1)) assert_equal(2, StripeClient.sleep_time(2)) assert_equal(2, StripeClient.sleep_time(3)) assert_equal(2, StripeClient.sleep_time(4)) end should "add some randomness" do random_value = 0.8 StripeClient.stubs(:rand).returns(random_value) - Stripe.stubs(:initial_network_retry_delay).returns(1) - Stripe.stubs(:max_network_retry_delay).returns(8) + Stripe.config.stubs(:initial_network_retry_delay).returns(1) + Stripe.config.stubs(:max_network_retry_delay).returns(8) base_value = Stripe.initial_network_retry_delay * (0.5 * (1 + random_value)) # the initial value cannot be smaller than the base, # so the randomness is ignored @@ -307,10 +425,27 @@ # after the first one, the randomness is applied assert_equal(base_value * 2, StripeClient.sleep_time(2)) assert_equal(base_value * 4, StripeClient.sleep_time(3)) assert_equal(base_value * 8, StripeClient.sleep_time(4)) 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.config.stubs(:initial_network_retry_delay).returns(100) + Stripe.config.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 context "#execute_request" do context "headers" do should "support literal headers" do @@ -340,10 +475,14 @@ # emit for responses. Mocha's `anything` parameter can't match inside # of a hash and is therefore not useful for this purpose. If we # switch over to rspec-mocks at some point, we can probably remove # this. Util.stubs(:monotonic_time).returns(0.0) + + # Stub the Stripe.config so that mocha matchers will succeed. Currently, + # mocha does not support using param matchers within hashes. + StripeClient.any_instance.stubs(:config).returns(Stripe.config) end should "produce appropriate logging" do body = JSON.generate(object: "account") @@ -351,33 +490,38 @@ account: "acct_123", api_version: "2010-11-12", idempotency_key: "abc", method: :post, num_retries: 0, - path: "/v1/account") + path: "/v1/account", + config: Stripe.config) Util.expects(:log_debug).with("Request details", body: "", idempotency_key: "abc", - query: nil) + query: nil, + config: Stripe.config) Util.expects(:log_info).with("Response from Stripe API", account: "acct_123", api_version: "2010-11-12", elapsed: 0.0, idempotency_key: "abc", method: :post, path: "/v1/account", request_id: "req_123", - status: 200) + status: 200, + config: Stripe.config) Util.expects(:log_debug).with("Response details", body: body, idempotency_key: "abc", - request_id: "req_123") + request_id: "req_123", + config: Stripe.config) Util.expects(:log_debug).with("Dashboard link for request", idempotency_key: "abc", 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.config) stub_request(:post, "#{Stripe.api_base}/v1/account") .to_return( body: body, headers: { @@ -402,20 +546,22 @@ account: nil, api_version: nil, idempotency_key: nil, method: :post, num_retries: 0, - path: "/v1/account") + path: "/v1/account", + config: Stripe.config) Util.expects(:log_info).with("Response from Stripe API", account: nil, api_version: nil, elapsed: 0.0, idempotency_key: nil, method: :post, path: "/v1/account", request_id: nil, - status: 500) + status: 500, + config: Stripe.config) error = { code: "code", message: "message", param: "param", @@ -426,11 +572,12 @@ error_code: error[:code], error_message: error[:message], error_param: error[:param], error_type: error[:type], idempotency_key: nil, - request_id: nil) + request_id: nil, + config: Stripe.config) stub_request(:post, "#{Stripe.api_base}/v1/account") .to_return( body: JSON.generate(error: error), status: 500 @@ -447,27 +594,30 @@ account: nil, api_version: nil, idempotency_key: nil, method: :post, num_retries: 0, - path: "/oauth/token") + path: "/oauth/token", + config: Stripe.config) Util.expects(:log_info).with("Response from Stripe API", account: nil, api_version: nil, elapsed: 0.0, idempotency_key: nil, method: :post, path: "/oauth/token", request_id: nil, - status: 400) + status: 400, + config: Stripe.config) Util.expects(:log_error).with("Stripe OAuth error", status: 400, error_code: "invalid_request", error_description: "No grant type specified", idempotency_key: nil, - request_id: nil) + request_id: nil, + config: Stripe.config) stub_request(:post, "#{Stripe.connect_base}/oauth/token") .to_return(body: JSON.generate(error: "invalid_request", error_description: "No grant type specified"), status: 400) @@ -786,11 +936,11 @@ end end context "idempotency keys" do setup do - Stripe.stubs(:max_network_retries).returns(2) + Stripe::StripeConfiguration.any_instance.stubs(:max_network_retries).returns(2) end should "not add an idempotency key to GET requests" do SecureRandom.expects(:uuid).times(0) stub_request(:get, "#{Stripe.api_base}/v1/charges/ch_123") @@ -836,11 +986,11 @@ end end context "retry logic" do setup do - Stripe.stubs(:max_network_retries).returns(2) + Stripe::StripeConfiguration.any_instance.stubs(:max_network_retries).returns(2) end should "retry failed requests and raise if error persists" do StripeClient.expects(:sleep_time).at_least_once.returns(0) stub_request(:post, "#{Stripe.api_base}/v1/charges") @@ -868,10 +1018,25 @@ end client = StripeClient.new client.execute_request(:post, "/v1/charges") 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 context "params serialization" do should "allows empty strings in params" do client = StripeClient.new @@ -1077,11 +1242,11 @@ end context "#proxy" do should "run the request through the proxy" do begin - StripeClient.current_thread_context.default_connection_manager = nil + StripeClient.clear_all_connection_managers Stripe.proxy = "http://user:pass@localhost:8080" client = StripeClient.new client.request {} @@ -1093,10 +1258,10 @@ assert_equal "user", connection.proxy_user assert_equal "pass", connection.proxy_pass ensure Stripe.proxy = nil - StripeClient.current_thread_context.default_connection_manager = nil + StripeClient.clear_all_connection_managers end end end context "#telemetry" do