Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add supporting classes for test helper generation #1034

Merged
merged 10 commits into from
Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/stripe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
require "stripe/list_object"
require "stripe/error_object"
require "stripe/api_resource"
require "stripe/api_resource_test_helpers"
require "stripe/singleton_api_resource"
require "stripe/webhook"
require "stripe/stripe_configuration"
Expand Down
19 changes: 2 additions & 17 deletions lib/stripe/api_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,7 @@ def self.save_nested_resource(name)
# adds a `capture` class method to the resource class that, when called,
# will send a POST request to `/v1/<object_name>/capture`.
def self.custom_method(name, http_verb:, http_path: nil)
unless %i[get post delete].include?(http_verb)
raise ArgumentError,
"Invalid http_verb value: #{http_verb.inspect}. Should be one " \
"of :get, :post or :delete."
end
http_path ||= name.to_s
define_singleton_method(name) do |id, params = {}, opts = {}|
unless id.is_a?(String)
raise ArgumentError,
"id should be a string representing the ID of an API resource"
end

url = "#{resource_url}/#{CGI.escape(id)}/#{CGI.escape(http_path)}"
resp, opts = execute_resource_request(http_verb, url, params, opts)
Util.convert_to_stripe_object(resp.data, opts)
end
Util.custom_method self, self, name, http_verb, http_path
end

def resource_url
Expand All @@ -105,7 +90,7 @@ def self.retrieve(id, opts = {})
instance
end

protected def request_stripe_object(method:, path:, params:, opts: {})
def request_stripe_object(method:, path:, params:, opts: {})
resp, opts = execute_resource_request(method, path, params, opts)

# If we're getting back this thing, update; otherwise, instantiate.
Expand Down
25 changes: 25 additions & 0 deletions lib/stripe/api_resource_test_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Stripe
class APIResourceTestHelpers
def initialize(resource)
@resource = resource
end

# Adds a custom method to a test helper. This is used to add support for
# non-CRUDL API requests, e.g. capturing charges. custom_method takes the
# following parameters:
# - name: the name of the custom method to create (as a symbol)
# - http_verb: the HTTP verb for the API request (:get, :post, or :delete)
# - http_path: the path to append to the resource's URL. If not provided,
# the name is used as the path
#
# For example, this call:
# custom_method :capture, http_verb: post
# adds a `capture` class method to the resource class that, when called,
# will send a POST request to `/v1/<object_name>/capture`.
def self.custom_method(name, http_verb:, http_path: nil)
Util.custom_method self::RESOURCE_CLASS, self, name, http_verb, http_path
end
end
end
42 changes: 42 additions & 0 deletions lib/stripe/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,48 @@ def self.object_name_matches_class?(object_name, klass)
Util.object_classes[object_name] == klass
end

# Adds a custom method to a resource class. This is used to add support for
# non-CRUDL API requests, e.g. capturing charges. custom_method takes the
# following parameters:
# - name: the name of the custom method to create (as a symbol)
# - http_verb: the HTTP verb for the API request (:get, :post, or :delete)
# - http_path: the path to append to the resource's URL. If not provided,
# the name is used as the path
# - resource: the resource implementation class
# - target: the class that custom static method will be added to
#
# For example, this call:
# custom_method :capture, http_verb: post
# adds a `capture` class method to the resource class that, when called,
# will send a POST request to `/v1/<object_name>/capture`.
def self.custom_method(resource, target, name, http_verb, http_path)
unless %i[get post delete].include?(http_verb)
pakrym-stripe marked this conversation as resolved.
Show resolved Hide resolved
raise ArgumentError,
"Invalid http_verb value: #{http_verb.inspect}. Should be one " \
"of :get, :post or :delete."
end
http_path ||= name.to_s
target.define_singleton_method(name) do |id, params = {}, opts = {}|
unless id.is_a?(String)
raise ArgumentError,
"id should be a string representing the ID of an API resource"
end

url = "#{resource.resource_url}/"\
"#{CGI.escape(id)}/"\
"#{CGI.escape(http_path)}"

resp, opts = resource.execute_resource_request(
http_verb,
url,
params,
opts
)

Util.convert_to_stripe_object(resp.data, opts)
end
end

# Converts a hash of fields or an array of hashes into a +StripeObject+ or
# array of +StripeObject+s. These new objects will be created as a concrete
# type as dictated by their `object` field (e.g. an `object` value of
Expand Down
76 changes: 76 additions & 0 deletions test/stripe/api_resource_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,82 @@ def read_stream(params = {}, opts = {}, &read_body_chunk_block)
end
end

context "test helpers" do
class TestHelperAPIResource < APIResource
OBJECT_NAME = "hello"

def test_helpers
TestHelpers.new(self)
end

class TestHelpers < APIResourceTestHelpers
RESOURCE_CLASS = TestHelperAPIResource

custom_method :say_hello, http_verb: :post

def say_hello(params = {}, opts = {})
@resource.request_stripe_object(
method: :post,
path: @resource.resource_url + "/say_hello",
params: params,
opts: opts
)
end
end
end

setup do
Util.instance_variable_set(
:@object_classes,
Stripe::ObjectTypes.object_names_to_classes.merge(
"hello" => TestHelperAPIResource
)
)
end
teardown do
Util.class.instance_variable_set(:@object_classes, Stripe::ObjectTypes.object_names_to_classes)
end

should "make requests appropriately" do
stub_request(:post, "#{Stripe.api_base}/v1/hellos/hi_123/say_hello")
.with(body: { foo: "bar" }, headers: { "Stripe-Account" => "acct_hi" })
.to_return(body: JSON.generate("object" => "hello"))

hello = TestHelperAPIResource.new(id: "hi_123")
hello.test_helpers.say_hello({ foo: "bar" }, stripe_account: "acct_hi")
end

should "forward opts" do
stub_request(:post, "#{Stripe.api_base}/v1/hellos/hi_123/say_hello")
.with(body: { foo: "bar" }, headers: { "Stripe-Account" => "acct_hi" })
.to_return(body: JSON.generate("object" => "hello"))

hello = TestHelperAPIResource.new("hi_123", stripe_account: "acct_hi")
hello.test_helpers.say_hello({ foo: "bar" })
end

should "return resource" do
stub_request(:post, "#{Stripe.api_base}/v1/hellos/hi_123/say_hello")
.with(body: { foo: "bar" }, headers: { "Stripe-Account" => "acct_hi" })
.to_return(body: JSON.generate({ object: "hello", result: "hi!" }))

hello = TestHelperAPIResource.new(id: "hi_123")
new_hello = hello.test_helpers.say_hello({ foo: "bar" }, stripe_account: "acct_hi")
assert new_hello.is_a? TestHelperAPIResource
assert_equal "hi!", new_hello.result
end

should "be callable statically" do
stub_request(:post, "#{Stripe.api_base}/v1/hellos/hi_123/say_hello")
.with(body: { foo: "bar" }, headers: { "Stripe-Account" => "acct_hi" })
.to_return(body: JSON.generate({ object: "hello", result: "hi!" }))

new_hello = TestHelperAPIResource::TestHelpers.say_hello("hi_123", { foo: "bar" }, stripe_account: "acct_hi")
pakrym-stripe marked this conversation as resolved.
Show resolved Hide resolved
assert new_hello.is_a? TestHelperAPIResource
assert_equal "hi!", new_hello.result
end
end

@@fixtures = {} # rubocop:disable Style/ClassVars
setup do
if @@fixtures.empty?
Expand Down