Add support for making a request and receiving the response as a stream. (#983)

This commit is contained in:
Dominic Charley-Roy 2021-06-24 10:24:11 -04:00 committed by GitHub
parent 28e6d19a90
commit 59eb8d06cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1045 additions and 725 deletions

View File

@ -6,6 +6,28 @@ module Stripe
module ClassMethods module ClassMethods
def execute_resource_request(method, url, def execute_resource_request(method, url,
params = {}, opts = {}) params = {}, opts = {})
execute_resource_request_internal(
:execute_request, method, url, params, opts
)
end
def execute_resource_request_stream(method, url,
params = {}, opts = {},
&read_body_chunk_block)
execute_resource_request_internal(
:execute_request_stream,
method,
url,
params,
opts,
&read_body_chunk_block
)
end
private def execute_resource_request_internal(client_request_method_sym,
method, url,
params, opts,
&read_body_chunk_block)
params ||= {} params ||= {}
error_on_invalid_params(params) error_on_invalid_params(params)
@ -22,10 +44,12 @@ module Stripe
client = headers.delete(:client) client = headers.delete(:client)
# Assume all remaining opts must be headers # Assume all remaining opts must be headers
resp, opts[:api_key] = client.execute_request( resp, opts[:api_key] = client.send(
client_request_method_sym,
method, url, method, url,
api_base: api_base, api_key: api_key, api_base: api_base, api_key: api_key,
headers: headers, params: params headers: headers, params: params,
&read_body_chunk_block
) )
# Hash#select returns an array before 1.9 # Hash#select returns an array before 1.9
@ -89,6 +113,15 @@ module Stripe
self.class.execute_resource_request(method, url, params, opts) self.class.execute_resource_request(method, url, params, opts)
end end
protected def execute_resource_request_stream(method, url,
params = {}, opts = {},
&read_body_chunk_block)
opts = @opts.merge(Util.normalize_opts(opts))
self.class.execute_resource_request_stream(
method, url, params, opts, &read_body_chunk_block
)
end
# See notes on `alias` above. # See notes on `alias` above.
alias request execute_resource_request alias request execute_resource_request
end end

View File

@ -115,5 +115,13 @@ module Stripe
Util.convert_to_stripe_object(resp.data, opts) Util.convert_to_stripe_object(resp.data, opts)
end end
end end
protected def request_stream(method:, path:, params:, opts: {},
&read_body_chunk_block)
resp, = execute_resource_request_stream(
method, path, params, opts, &read_body_chunk_block
)
resp
end
end end
end end

View File

@ -66,7 +66,8 @@ module Stripe
# Executes an HTTP request to the given URI with the given method. Also # Executes an HTTP request to the given URI with the given method. Also
# allows a request body, headers, and query string to be specified. # allows a request body, headers, and query string to be specified.
def execute_request(method, uri, body: nil, headers: nil, query: nil) def execute_request(method, uri, body: nil, headers: nil, query: nil,
&block)
# Perform some basic argument validation because it's easy to get # Perform some basic argument validation because it's easy to get
# confused between strings and hashes for things like body and query # confused between strings and hashes for things like body and query
# parameters. # parameters.
@ -92,8 +93,22 @@ module Stripe
u.path u.path
end end
method_name = method.to_s.upcase
has_response_body = method_name != "HEAD"
request = Net::HTTPGenericRequest.new(
method_name,
(body ? true : false),
has_response_body,
path,
headers
)
@mutex.synchronize do @mutex.synchronize do
connection.send_request(method.to_s.upcase, path, body, headers) # The block parameter is special here. If a block is provided, the block
# is invoked with the Net::HTTPResponse. However, the body will not have
# been read yet in the block, and can be streamed by calling
# HTTPResponse#read_body.
connection.request(request, body, &block)
end end
end end

View File

@ -213,62 +213,9 @@ module Stripe
def execute_request(method, path, def execute_request(method, path,
api_base: nil, api_key: nil, headers: {}, params: {}) api_base: nil, api_key: nil, headers: {}, params: {})
raise ArgumentError, "method should be a symbol" \ http_resp, api_key = execute_request_internal(
unless method.is_a?(Symbol) method, path, api_base, api_key, headers, params
raise ArgumentError, "path should be a string" \ )
unless path.is_a?(String)
api_base ||= config.api_base
api_key ||= config.api_key
params = Util.objects_to_ids(params)
check_api_key!(api_key)
body_params = nil
query_params = nil
case method
when :get, :head, :delete
query_params = params
else
body_params = params
end
query_params, path = merge_query_params(query_params, path)
headers = request_headers(api_key, method)
.update(Util.normalize_headers(headers))
url = api_url(path, api_base)
# Merge given query parameters with any already encoded in the path.
query = query_params ? Util.encode_parameters(query_params) : nil
# Encoding body parameters is a little more complex because we may have
# to send a multipart-encoded body. `body_log` is produced separately as
# a log-friendly variant of the encoded form. File objects are displayed
# as such instead of as their file contents.
body, body_log =
body_params ? encode_body(body_params, headers) : [nil, nil]
# stores information on the request we're about to make so that we don't
# have to pass as many parameters around for logging.
context = RequestLogContext.new
context.account = headers["Stripe-Account"]
context.api_key = api_key
context.api_version = headers["Stripe-Version"]
context.body = body_log
context.idempotency_key = headers["Idempotency-Key"]
context.method = method
context.path = path
context.query = query
http_resp = execute_request_with_rescues(method, api_base, context) do
self.class
.default_connection_manager(config)
.execute_request(method, url,
body: body,
headers: headers,
query: query)
end
begin begin
resp = StripeResponse.from_net_http(http_resp) resp = StripeResponse.from_net_http(http_resp)
@ -284,6 +231,38 @@ module Stripe
[resp, api_key] [resp, api_key]
end end
# Executes a request and returns the body as a stream instead of converting
# it to a StripeObject. This should be used for any request where we expect
# an arbitrary binary response.
#
# A `read_body_chunk` block can be passed, which will be called repeatedly
# with the body chunks read from the socket.
#
# If a block is passed, a StripeHeadersOnlyResponse is returned as the
# block is expected to do all the necessary body processing. If no block is
# passed, then a StripeStreamResponse is returned containing an IO stream
# with the response body.
def execute_request_stream(method, path,
api_base: nil, api_key: nil,
headers: {}, params: {},
&read_body_chunk_block)
unless block_given?
raise ArgumentError,
"execute_request_stream requires a read_body_chunk_block"
end
http_resp, api_key = execute_request_internal(
method, path, api_base, api_key, headers, params, &read_body_chunk_block
)
# When the read_body_chunk_block is given, we no longer have access to the
# response body at this point and so return a response object containing
# only the headers. This is because the body was consumed by the block.
resp = StripeHeadersOnlyResponse.from_net_http(http_resp)
[resp, api_key]
end
def store_last_response(object_id, resp) def store_last_response(object_id, resp)
return unless last_response_has_key?(object_id) return unless last_response_has_key?(object_id)
@ -451,6 +430,83 @@ module Stripe
pruned_contexts.count pruned_contexts.count
end end
private def execute_request_internal(method, path,
api_base, api_key, headers, params,
&read_body_chunk_block)
raise ArgumentError, "method should be a symbol" \
unless method.is_a?(Symbol)
raise ArgumentError, "path should be a string" \
unless path.is_a?(String)
api_base ||= config.api_base
api_key ||= config.api_key
params = Util.objects_to_ids(params)
check_api_key!(api_key)
body_params = nil
query_params = nil
case method
when :get, :head, :delete
query_params = params
else
body_params = params
end
query_params, path = merge_query_params(query_params, path)
headers = request_headers(api_key, method)
.update(Util.normalize_headers(headers))
url = api_url(path, api_base)
# Merge given query parameters with any already encoded in the path.
query = query_params ? Util.encode_parameters(query_params) : nil
# Encoding body parameters is a little more complex because we may have
# to send a multipart-encoded body. `body_log` is produced separately as
# a log-friendly variant of the encoded form. File objects are displayed
# as such instead of as their file contents.
body, body_log =
body_params ? encode_body(body_params, headers) : [nil, nil]
# stores information on the request we're about to make so that we don't
# have to pass as many parameters around for logging.
context = RequestLogContext.new
context.account = headers["Stripe-Account"]
context.api_key = api_key
context.api_version = headers["Stripe-Version"]
context.body = body_log
context.idempotency_key = headers["Idempotency-Key"]
context.method = method
context.path = path
context.query = query
# A block can be passed in to read the content directly from the response.
# We want to execute this block only when the response was actually
# successful. When it wasn't, we defer to the standard error handling as
# we have to read the body and parse the error JSON.
response_block =
if block_given?
lambda do |response|
unless should_handle_as_error(response.code.to_i)
response.read_body(&read_body_chunk_block)
end
end
end
http_resp = execute_request_with_rescues(method, api_base, context) do
self.class
.default_connection_manager(config)
.execute_request(method, url,
body: body,
headers: headers,
query: query,
&response_block)
end
[http_resp, api_key]
end
private def api_url(url = "", api_base = nil) private def api_url(url = "", api_base = nil)
(api_base || config.api_base) + url (api_base || config.api_base) + url
end end
@ -490,6 +546,7 @@ module Stripe
# that's more condusive to logging. # that's more condusive to logging.
flattened_params = flattened_params =
flattened_params.map { |k, v| [k, v.is_a?(String) ? v : v.to_s] }.to_h flattened_params.map { |k, v| [k, v.is_a?(String) ? v : v.to_s] }.to_h
else else
body = Util.encode_parameters(body_params) body = Util.encode_parameters(body_params)
end end
@ -503,6 +560,10 @@ module Stripe
[body, body_log] [body, body_log]
end end
private def should_handle_as_error(http_status)
http_status >= 400
end
private def execute_request_with_rescues(method, api_base, context) private def execute_request_with_rescues(method, api_base, context)
num_retries = 0 num_retries = 0
@ -520,7 +581,9 @@ module Stripe
http_status = resp.code.to_i http_status = resp.code.to_i
context = context.dup_from_response_headers(resp) context = context.dup_from_response_headers(resp)
handle_error_response(resp, context) if http_status >= 400 if should_handle_as_error(http_status)
handle_error_response(resp, context)
end
log_response(context, request_start, http_status, resp.body) log_response(context, request_start, http_status, resp.body)
notify_request_end(context, request_duration, http_status, notify_request_end(context, request_duration, http_status,

View File

@ -1,14 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
module Stripe module Stripe
# StripeResponse encapsulates some vitals of a response that came back from
# the Stripe API.
class StripeResponse
# Headers provides an access wrapper to an API response's header data. It # Headers provides an access wrapper to an API response's header data. It
# mainly exists so that we don't need to expose the entire # mainly exists so that we don't need to expose the entire
# `Net::HTTPResponse` object while still getting some of its benefits like # `Net::HTTPResponse` object while still getting some of its benefits like
# case-insensitive access to header names and flattening of header values. # case-insensitive access to header names and flattening of header values.
class Headers class StripeResponseHeaders
# Initializes a Headers object from a Net::HTTP::HTTPResponse object. # Initializes a Headers object from a Net::HTTP::HTTPResponse object.
def self.from_net_http(resp) def self.from_net_http(resp)
new(resp.to_hash) new(resp.to_hash)
@ -51,13 +48,7 @@ module Stripe
end end
end end
# The data contained by the HTTP body of the response deserialized from module StripeResponseBase
# JSON.
attr_accessor :data
# The raw HTTP body of the response.
attr_accessor :http_body
# A Hash of the HTTP headers of the response. # A Hash of the HTTP headers of the response.
attr_accessor :http_headers attr_accessor :http_headers
@ -67,15 +58,52 @@ module Stripe
# The Stripe request ID of the response. # The Stripe request ID of the response.
attr_accessor :request_id attr_accessor :request_id
def self.populate_for_net_http(resp, http_resp)
resp.http_headers = StripeResponseHeaders.from_net_http(http_resp)
resp.http_status = http_resp.code.to_i
resp.request_id = http_resp["request-id"]
end
end
# StripeResponse encapsulates some vitals of a response that came back from
# the Stripe API.
class StripeResponse
include StripeResponseBase
# The data contained by the HTTP body of the response deserialized from
# JSON.
attr_accessor :data
# The raw HTTP body of the response.
attr_accessor :http_body
# Initializes a StripeResponse object from a Net::HTTP::HTTPResponse # Initializes a StripeResponse object from a Net::HTTP::HTTPResponse
# object. # object.
def self.from_net_http(http_resp) def self.from_net_http(http_resp)
resp = StripeResponse.new resp = StripeResponse.new
resp.data = JSON.parse(http_resp.body, symbolize_names: true) resp.data = JSON.parse(http_resp.body, symbolize_names: true)
resp.http_body = http_resp.body resp.http_body = http_resp.body
resp.http_headers = Headers.from_net_http(http_resp) StripeResponseBase.populate_for_net_http(resp, http_resp)
resp.http_status = http_resp.code.to_i resp
resp.request_id = http_resp["request-id"] end
end
# We have to alias StripeResponseHeaders to StripeResponse::Headers, as this
# class used to be embedded within StripeResponse and we want to be backwards
# compatible.
StripeResponse::Headers = StripeResponseHeaders
# StripeHeadersOnlyResponse includes only header-related vitals of the
# response. This is used for streaming requests where the response was read
# directly in a block and we explicitly don't want to store the body of the
# response in memory.
class StripeHeadersOnlyResponse
include StripeResponseBase
# Initializes a StripeHeadersOnlyResponse object from a
# Net::HTTP::HTTPResponse object.
def self.from_net_http(http_resp)
resp = StripeHeadersOnlyResponse.new
StripeResponseBase.populate_for_net_http(resp, http_resp)
resp resp
end end
end end

View File

@ -613,6 +613,58 @@ module Stripe
end end
end end
context "#request_stream" do
class StreamTestAPIResource < APIResource
OBJECT_NAME = "stream"
def read_stream(params = {}, opts = {}, &read_body_chunk_block)
request_stream(
method: :get,
path: resource_url + "/read",
params: params,
opts: opts,
&read_body_chunk_block
)
end
end
setup do
Util.instance_variable_set(
:@object_classes,
Stripe::ObjectTypes.object_names_to_classes.merge(
"stream" => StreamTestAPIResource
)
)
end
teardown do
Util.class.instance_variable_set(:@object_classes, Stripe::ObjectTypes.object_names_to_classes)
end
should "supports requesting with a block" do
stub_request(:get, "#{Stripe.api_base}/v1/streams/hi_123/read")
.with(query: { foo: "bar" }, headers: { "Stripe-Account" => "acct_hi" })
.to_return(body: "response body")
accumulated_body = +""
resp = StreamTestAPIResource.new(id: "hi_123").read_stream({ foo: "bar" }, stripe_account: "acct_hi") do |body_chunk|
accumulated_body << body_chunk
end
assert_instance_of Stripe::StripeHeadersOnlyResponse, resp
assert_equal "response body", accumulated_body
end
should "fail when requesting without a block" do
stub_request(:get, "#{Stripe.api_base}/v1/streams/hi_123/read")
.with(query: { foo: "bar" }, headers: { "Stripe-Account" => "acct_hi" })
.to_return(body: "response body")
assert_raises ArgumentError do
StreamTestAPIResource.new(id: "hi_123").read_stream({ foo: "bar" }, stripe_account: "acct_hi")
end
end
end
@@fixtures = {} # rubocop:disable Style/ClassVars @@fixtures = {} # rubocop:disable Style/ClassVars
setup do setup do
if @@fixtures.empty? if @@fixtures.empty?

View File

@ -159,6 +159,27 @@ module Stripe
query: "query=bar") query: "query=bar")
end end
should "make a request with a block" do
stub_request(:post, "#{Stripe.api_base}/path?query=bar")
.with(
body: "body=foo",
headers: { "Stripe-Account" => "bar" }
)
.to_return(body: "HTTP response body")
accumulated_body = +""
@manager.execute_request(:post, "#{Stripe.api_base}/path",
body: "body=foo",
headers: { "Stripe-Account" => "bar" },
query: "query=bar") do |res|
res.read_body do |body_chunk|
accumulated_body << body_chunk
end
end
assert_equal "HTTP response body", accumulated_body
end
should "perform basic argument validation" do should "perform basic argument validation" do
e = assert_raises ArgumentError do e = assert_raises ArgumentError do
@manager.execute_request("POST", "#{Stripe.api_base}/path") @manager.execute_request("POST", "#{Stripe.api_base}/path")

View File

@ -446,7 +446,14 @@ module Stripe
end end
end end
context "#execute_request" do %w[execute_request execute_request_stream].each do |request_method|
context "request processing for #{request_method}" do
setup do
@read_body_chunk_block = if request_method == "execute_request_stream"
proc { |body_chunk| body_chunk }
end
end
context "headers" do context "headers" do
should "support literal headers" do should "support literal headers" do
stub_request(:post, "#{Stripe.api_base}/v1/account") stub_request(:post, "#{Stripe.api_base}/v1/account")
@ -454,8 +461,9 @@ module Stripe
.to_return(body: JSON.generate(object: "account")) .to_return(body: JSON.generate(object: "account"))
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/account", client.send(request_method, :post, "/v1/account",
headers: { "Stripe-Account" => "bar" }) headers: { "Stripe-Account" => "bar" },
&@read_body_chunk_block)
end end
should "support RestClient-style header keys" do should "support RestClient-style header keys" do
@ -464,8 +472,9 @@ module Stripe
.to_return(body: JSON.generate(object: "account")) .to_return(body: JSON.generate(object: "account"))
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/account", client.send(request_method, :post, "/v1/account",
headers: { stripe_account: "bar" }) headers: { stripe_account: "bar" },
&@read_body_chunk_block)
end end
end end
@ -510,11 +519,19 @@ module Stripe
request_id: "req_123", request_id: "req_123",
status: 200, status: 200,
config: Stripe.config) config: Stripe.config)
Util.expects(:log_debug).with("Response details", Util.expects(:log_debug).with do |message, data|
body: body, if message == "Response details" &&
idempotency_key: "abc", data[:idempotency_key] == "abc" &&
request_id: "req_123", data[:request_id] == "req_123" &&
config: Stripe.config) data[:config] == Stripe.config
# Streaming requests have a different body.
if request_method == "execute_request_stream"
data[:body].is_a? Net::ReadAdapter
else
data[:body] == body
end
end
end
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",
@ -533,12 +550,13 @@ module Stripe
) )
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/account", client.send(request_method, :post, "/v1/account",
headers: { headers: {
"Idempotency-Key" => "abc", "Idempotency-Key" => "abc",
"Stripe-Account" => "acct_123", "Stripe-Account" => "acct_123",
"Stripe-Version" => "2010-11-12", "Stripe-Version" => "2010-11-12",
}) },
&@read_body_chunk_block)
end end
should "produce logging on API error" do should "produce logging on API error" do
@ -585,7 +603,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
assert_raises Stripe::APIError do assert_raises Stripe::APIError do
client.execute_request(:post, "/v1/account") client.send(request_method, :post, "/v1/account",
&@read_body_chunk_block)
end end
end end
@ -624,7 +643,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
opts = { api_base: Stripe.connect_base } opts = { api_base: Stripe.connect_base }
assert_raises Stripe::OAuth::InvalidRequestError do assert_raises Stripe::OAuth::InvalidRequestError do
client.execute_request(:post, "/oauth/token", **opts) client.send(request_method, :post, "/oauth/token", **opts,
&@read_body_chunk_block)
end end
end end
end end
@ -640,7 +660,8 @@ module Stripe
.to_return(body: JSON.generate(object: "account")) .to_return(body: JSON.generate(object: "account"))
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/account") client.send(request_method, :post, "/v1/account",
&@read_body_chunk_block)
ensure ensure
Stripe.stripe_account = old Stripe.stripe_account = old
end end
@ -653,8 +674,9 @@ module Stripe
.to_return(body: JSON.generate(object: "account")) .to_return(body: JSON.generate(object: "account"))
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/account", client.send(request_method, :post, "/v1/account",
headers: { stripe_account: stripe_account }) headers: { stripe_account: stripe_account },
&@read_body_chunk_block)
end end
should "not send it otherwise" do should "not send it otherwise" do
@ -664,7 +686,8 @@ module Stripe
end.to_return(body: JSON.generate(object: "account")) end.to_return(body: JSON.generate(object: "account"))
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/account") client.send(request_method, :post, "/v1/account",
&@read_body_chunk_block)
end end
end end
@ -700,7 +723,8 @@ module Stripe
end.to_return(body: JSON.generate(object: "account")) end.to_return(body: JSON.generate(object: "account"))
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/account") client.send(request_method, :post, "/v1/account",
&@read_body_chunk_block)
ensure ensure
Stripe.app_info = old Stripe.app_info = old
end end
@ -715,23 +739,12 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::APIError do e = assert_raises Stripe::APIError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal 'Invalid response object from API: "" (HTTP response code was 500)', e.message assert_equal 'Invalid response object from API: "" (HTTP response code was 500)', e.message
end end
should "handle success response with empty body" do
stub_request(:post, "#{Stripe.api_base}/v1/charges")
.to_return(body: "", status: 200)
client = StripeClient.new
e = assert_raises Stripe::APIError do
client.execute_request(:post, "/v1/charges")
end
assert_equal 'Invalid response object from API: "" (HTTP response code was 200)', e.message
end
should "feed a request ID through to the error object" do should "feed a request ID through to the error object" do
stub_request(:post, "#{Stripe.api_base}/v1/charges") stub_request(:post, "#{Stripe.api_base}/v1/charges")
.to_return(body: JSON.generate(make_missing_id_error), .to_return(body: JSON.generate(make_missing_id_error),
@ -741,7 +754,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::InvalidRequestError do e = assert_raises Stripe::InvalidRequestError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal("req_123", e.request_id) assert_equal("req_123", e.request_id)
end end
@ -753,7 +767,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::APIConnectionError do e = assert_raises Stripe::APIConnectionError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal StripeClient::ERROR_MESSAGE_CONNECTION % Stripe.api_base + assert_equal StripeClient::ERROR_MESSAGE_CONNECTION % Stripe.api_base +
"\n\n(Network error: Connection refused)", "\n\n(Network error: Connection refused)",
@ -767,7 +782,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::APIError do e = assert_raises Stripe::APIError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal 'Invalid response object from API: "{\"bar\":\"foo\"}" (HTTP response code was 500)', e.message assert_equal 'Invalid response object from API: "{\"bar\":\"foo\"}" (HTTP response code was 500)', e.message
end end
@ -782,7 +798,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::IdempotencyError do e = assert_raises Stripe::IdempotencyError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal(400, e.http_status) assert_equal(400, e.http_status)
assert_equal(true, e.json_body.is_a?(Hash)) assert_equal(true, e.json_body.is_a?(Hash))
@ -795,7 +812,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::InvalidRequestError do e = assert_raises Stripe::InvalidRequestError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal(400, e.http_status) assert_equal(400, e.http_status)
assert_equal(true, e.json_body.is_a?(Hash)) assert_equal(true, e.json_body.is_a?(Hash))
@ -808,7 +826,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::AuthenticationError do e = assert_raises Stripe::AuthenticationError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal(401, e.http_status) assert_equal(401, e.http_status)
assert_equal(true, e.json_body.is_a?(Hash)) assert_equal(true, e.json_body.is_a?(Hash))
@ -821,7 +840,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::CardError do e = assert_raises Stripe::CardError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal(402, e.http_status) assert_equal(402, e.http_status)
assert_equal(true, e.json_body.is_a?(Hash)) assert_equal(true, e.json_body.is_a?(Hash))
@ -836,7 +856,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::PermissionError do e = assert_raises Stripe::PermissionError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal(403, e.http_status) assert_equal(403, e.http_status)
assert_equal(true, e.json_body.is_a?(Hash)) assert_equal(true, e.json_body.is_a?(Hash))
@ -849,7 +870,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::InvalidRequestError do e = assert_raises Stripe::InvalidRequestError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal(404, e.http_status) assert_equal(404, e.http_status)
assert_equal(true, e.json_body.is_a?(Hash)) assert_equal(true, e.json_body.is_a?(Hash))
@ -862,7 +884,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
e = assert_raises Stripe::RateLimitError do e = assert_raises Stripe::RateLimitError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_equal(429, e.http_status) assert_equal(429, e.http_status)
assert_equal(true, e.json_body.is_a?(Hash)) assert_equal(true, e.json_body.is_a?(Hash))
@ -877,7 +900,8 @@ module Stripe
opts = { api_base: Stripe.connect_base } opts = { api_base: Stripe.connect_base }
e = assert_raises Stripe::OAuth::InvalidRequestError do e = assert_raises Stripe::OAuth::InvalidRequestError do
client.execute_request(:post, "/oauth/token", **opts) client.send(request_method, :post, "/oauth/token", **opts,
&@read_body_chunk_block)
end end
assert_equal(400, e.http_status) assert_equal(400, e.http_status)
@ -893,7 +917,8 @@ module Stripe
opts = { api_base: Stripe.connect_base } opts = { api_base: Stripe.connect_base }
e = assert_raises Stripe::OAuth::InvalidGrantError do e = assert_raises Stripe::OAuth::InvalidGrantError do
client.execute_request(:post, "/oauth/token", **opts) client.send(request_method, :post, "/oauth/token", **opts,
&@read_body_chunk_block)
end end
assert_equal(400, e.http_status) assert_equal(400, e.http_status)
@ -910,7 +935,8 @@ module Stripe
opts = { api_base: Stripe.connect_base } opts = { api_base: Stripe.connect_base }
e = assert_raises Stripe::OAuth::InvalidClientError do e = assert_raises Stripe::OAuth::InvalidClientError do
client.execute_request(:post, "/oauth/deauthorize", **opts) client.send(request_method, :post, "/oauth/deauthorize", **opts,
&@read_body_chunk_block)
end end
assert_equal(401, e.http_status) assert_equal(401, e.http_status)
@ -927,7 +953,8 @@ module Stripe
opts = { api_base: Stripe.connect_base } opts = { api_base: Stripe.connect_base }
e = assert_raises Stripe::OAuth::OAuthError do e = assert_raises Stripe::OAuth::OAuthError do
client.execute_request(:post, "/oauth/deauthorize", **opts) client.send(request_method, :post, "/oauth/deauthorize", **opts,
&@read_body_chunk_block)
end end
assert_equal(401, e.http_status) assert_equal(401, e.http_status)
@ -948,7 +975,8 @@ module Stripe
req.headers["Idempotency-Key"].nil? req.headers["Idempotency-Key"].nil?
end.to_return(body: JSON.generate(object: "charge")) end.to_return(body: JSON.generate(object: "charge"))
client = StripeClient.new client = StripeClient.new
client.execute_request(:get, "/v1/charges/ch_123") client.send(request_method, :get, "/v1/charges/ch_123",
&@read_body_chunk_block)
end end
should "ensure there is always an idempotency_key on POST requests" do should "ensure there is always an idempotency_key on POST requests" do
@ -957,7 +985,8 @@ module Stripe
.with(headers: { "Idempotency-Key" => "random_key" }) .with(headers: { "Idempotency-Key" => "random_key" })
.to_return(body: JSON.generate(object: "charge")) .to_return(body: JSON.generate(object: "charge"))
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
should "ensure there is always an idempotency_key on DELETE requests" do should "ensure there is always an idempotency_key on DELETE requests" do
@ -966,7 +995,8 @@ module Stripe
.with(headers: { "Idempotency-Key" => "random_key" }) .with(headers: { "Idempotency-Key" => "random_key" })
.to_return(body: JSON.generate(object: "charge")) .to_return(body: JSON.generate(object: "charge"))
client = StripeClient.new client = StripeClient.new
client.execute_request(:delete, "/v1/charges/ch_123") client.send(request_method, :delete, "/v1/charges/ch_123",
&@read_body_chunk_block)
end end
should "not override a provided idempotency_key" do should "not override a provided idempotency_key" do
@ -981,8 +1011,9 @@ module Stripe
.to_return(body: JSON.generate(object: "charge")) .to_return(body: JSON.generate(object: "charge"))
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/charges", client.send(request_method, :post, "/v1/charges",
headers: { idempotency_key: "provided_key" }) headers: { idempotency_key: "provided_key" },
&@read_body_chunk_block)
end end
end end
@ -998,7 +1029,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
err = assert_raises Stripe::APIConnectionError do err = assert_raises Stripe::APIConnectionError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
assert_match(/Request was retried 2 times/, err.message) assert_match(/Request was retried 2 times/, err.message)
end end
@ -1018,7 +1050,8 @@ module Stripe
end end
client = StripeClient.new client = StripeClient.new
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
should "pass the client configuration when retrying" do should "pass the client configuration when retrying" do
@ -1032,7 +1065,8 @@ module Stripe
client = StripeClient.new client = StripeClient.new
assert_raises Stripe::APIConnectionError do assert_raises Stripe::APIConnectionError do
client.execute_request(:post, "/v1/charges") client.send(request_method, :post, "/v1/charges",
&@read_body_chunk_block)
end end
end end
end end
@ -1040,10 +1074,9 @@ module Stripe
context "params serialization" do context "params serialization" do
should "allows empty strings in params" do should "allows empty strings in params" do
client = StripeClient.new client = StripeClient.new
client.execute_request(:get, "/v1/invoices/upcoming", params: { client.send(request_method, :get, "/v1/invoices/upcoming",
customer: "cus_123", params: { customer: "cus_123", coupon: "" },
coupon: "", &@read_body_chunk_block)
})
assert_requested( assert_requested(
:get, :get,
"#{Stripe.api_base}/v1/invoices/upcoming?", "#{Stripe.api_base}/v1/invoices/upcoming?",
@ -1056,10 +1089,9 @@ module Stripe
should "filter nils in params" do should "filter nils in params" do
client = StripeClient.new client = StripeClient.new
client.execute_request(:get, "/v1/invoices/upcoming", params: { client.send(request_method, :get, "/v1/invoices/upcoming",
customer: "cus_123", params: { customer: "cus_123", coupon: nil },
coupon: nil, &@read_body_chunk_block)
})
assert_requested( assert_requested(
:get, :get,
"#{Stripe.api_base}/v1/invoices/upcoming?", "#{Stripe.api_base}/v1/invoices/upcoming?",
@ -1071,9 +1103,9 @@ module Stripe
should "merge query parameters in URL and params" do should "merge query parameters in URL and params" do
client = StripeClient.new client = StripeClient.new
client.execute_request(:get, "/v1/invoices/upcoming?coupon=25OFF", params: { client.send(request_method, :get, "/v1/invoices/upcoming?coupon=25OFF",
customer: "cus_123", params: { customer: "cus_123" },
}) &@read_body_chunk_block)
assert_requested( assert_requested(
:get, :get,
"#{Stripe.api_base}/v1/invoices/upcoming?", "#{Stripe.api_base}/v1/invoices/upcoming?",
@ -1086,9 +1118,9 @@ module Stripe
should "prefer query parameters in params when specified in URL as well" do should "prefer query parameters in params when specified in URL as well" do
client = StripeClient.new client = StripeClient.new
client.execute_request(:get, "/v1/invoices/upcoming?customer=cus_query", params: { client.send(request_method, :get, "/v1/invoices/upcoming?customer=cus_query",
customer: "cus_param", params: { customer: "cus_param" },
}) &@read_body_chunk_block)
assert_requested( assert_requested(
:get, :get,
"#{Stripe.api_base}/v1/invoices/upcoming?", "#{Stripe.api_base}/v1/invoices/upcoming?",
@ -1099,6 +1131,48 @@ module Stripe
end end
end end
end end
end
context "#execute_request" do
should "handle success response with empty body" do
stub_request(:post, "#{Stripe.api_base}/v1/charges")
.to_return(body: "", status: 200)
client = StripeClient.new
e = assert_raises Stripe::APIError do
client.execute_request(:post, "/v1/charges")
end
assert_equal 'Invalid response object from API: "" (HTTP response code was 200)', e.message
end
end
context "#execute_request_stream" do
should "requires a block" do
client = StripeClient.new
e = assert_raises ArgumentError do
client.execute_request_stream(:post, "/v1/charges")
end
assert_equal "execute_request_stream requires a read_body_chunk_block", e.message
end
should "executes the read_body_chunk_block when passed" do
stub_request(:post, "#{Stripe.api_base}/v1/charges")
.to_return(body: "response body", status: 200)
client = StripeClient.new
accumulated_body = +""
resp, = client.execute_request_stream(:post, "/v1/charges") do |body_chunk|
accumulated_body << body_chunk
end
assert_instance_of Stripe::StripeHeadersOnlyResponse, resp
assert_equal "response body", accumulated_body
end
end
context "#connection_manager" do context "#connection_manager" do
should "warn that #connection_manager is deprecated" do should "warn that #connection_manager is deprecated" do

View File

@ -4,12 +4,12 @@ require ::File.expand_path("../test_helper", __dir__)
module Stripe module Stripe
class StripeResponseTest < Test::Unit::TestCase class StripeResponseTest < Test::Unit::TestCase
context "Headers" do context "StripeResponseHeaders" do
should "allow case-insensitive header access" do should "allow case-insensitive header access" do
headers = { "Request-Id" => "request-id" } headers = { "Request-Id" => "request-id" }
http_resp = create_net_http_resp(200, "", headers) http_resp = create_net_http_resp(200, "", headers)
headers = StripeResponse::Headers.from_net_http(http_resp) headers = StripeResponseHeaders.from_net_http(http_resp)
assert_equal "request-id", headers["request-id"] assert_equal "request-id", headers["request-id"]
assert_equal "request-id", headers["Request-Id"] assert_equal "request-id", headers["Request-Id"]
@ -17,26 +17,26 @@ module Stripe
end end
should "initialize without error" do should "initialize without error" do
StripeResponse::Headers.new({}) StripeResponseHeaders.new({})
StripeResponse::Headers.new("Request-Id" => []) StripeResponseHeaders.new("Request-Id" => [])
StripeResponse::Headers.new("Request-Id" => ["request-id"]) StripeResponseHeaders.new("Request-Id" => ["request-id"])
end end
should "initialize with error on a malformed hash" do should "initialize with error on a malformed hash" do
assert_raises(ArgumentError) do assert_raises(ArgumentError) do
StripeResponse::Headers.new(nil) StripeResponseHeaders.new(nil)
end end
assert_raises(ArgumentError) do assert_raises(ArgumentError) do
StripeResponse::Headers.new(1 => []) StripeResponseHeaders.new(1 => [])
end end
assert_raises(ArgumentError) do assert_raises(ArgumentError) do
StripeResponse::Headers.new("Request-Id" => 1) StripeResponseHeaders.new("Request-Id" => 1)
end end
assert_raises(ArgumentError) do assert_raises(ArgumentError) do
StripeResponse::Headers.new("Request-Id" => [1]) StripeResponseHeaders.new("Request-Id" => [1])
end end
end end
@ -44,7 +44,7 @@ module Stripe
old_stderr = $stderr old_stderr = $stderr
$stderr = StringIO.new $stderr = StringIO.new
begin begin
headers = StripeResponse::Headers.new("Duplicated" => %w[a b]) headers = StripeResponseHeaders.new("Duplicated" => %w[a b])
assert_equal "a", headers["Duplicated"] assert_equal "a", headers["Duplicated"]
assert_equal "Duplicate header values for `Duplicated`; returning only first", assert_equal "Duplicate header values for `Duplicated`; returning only first",
$stderr.string.rstrip $stderr.string.rstrip
@ -54,22 +54,48 @@ module Stripe
end end
end end
[StripeResponse, StripeHeadersOnlyResponse].each do |response_class|
context "StripeResponseBase mixin for #{response_class}" do
context ".from_net_http" do context ".from_net_http" do
should "converts to StripeResponse" do should "populate the base fields" do
code = 200 code = 200
body = '{"foo": "bar"}' body = '{"foo": "bar"}'
headers = { "Request-Id" => "request-id" } headers = { "Request-Id" => "request-id" }
http_resp = create_net_http_resp(code, body, headers) http_resp = create_net_http_resp(code, body, headers)
resp = StripeResponse.from_net_http(http_resp) resp = response_class.from_net_http(http_resp)
assert_equal JSON.parse(body, symbolize_names: true), resp.data
assert_equal body, resp.http_body
assert_equal "request-id", resp.http_headers["Request-ID"] assert_equal "request-id", resp.http_headers["Request-ID"]
assert_equal code, resp.http_status assert_equal code, resp.http_status
assert_equal "request-id", resp.request_id assert_equal "request-id", resp.request_id
end end
end end
end
end
context "#StripeResponse" do
context ".from_net_http" do
should "converts to StripeResponse" do
code = 200
body = '{"foo": "bar"}'
http_resp = create_net_http_resp(code, body, {})
resp = StripeResponse.from_net_http(http_resp)
assert_instance_of StripeResponse, resp
assert_equal JSON.parse(body, symbolize_names: true), resp.data
assert_equal body, resp.http_body
end
end
context "Headers backwards compatibility" do
should "alias StripeResponseHeaders" do
headers = StripeResponse::Headers.new("Request-Id" => ["request-id"])
assert_instance_of StripeResponseHeaders, headers
end
end
end
# Synthesizes a `Net::HTTPResponse` object for testing purposes. # Synthesizes a `Net::HTTPResponse` object for testing purposes.
private def create_net_http_resp(code, body, headers) private def create_net_http_resp(code, body, headers)