mirror of
https://github.com/stripe/stripe-ruby.git
synced 2025-10-04 00:00:47 -04:00
Add testing infrastructure to use spec and fixtures
Adds some testing infrastructure that reads in the OpenAPI spec and its fixtures. These changes will allow us to starting porting over each of stripe-ruby's test suites.
This commit is contained in:
parent
e9d4e27a4b
commit
f0579950a7
29
test/api_fixtures.rb
Normal file
29
test/api_fixtures.rb
Normal file
@ -0,0 +1,29 @@
|
||||
# APIFixtures loads fixture data generated by the core Stripe API so that we
|
||||
# can have slightly more accurate and up-to-date resource information in our
|
||||
# tests.
|
||||
class APIFixtures
|
||||
def initialize
|
||||
@fixtures = ::JSON.parse(File.read("#{PROJECT_ROOT}/spec/fixtures.json"),
|
||||
symbolize_names: true)
|
||||
freeze_recursively(@fixtures)
|
||||
end
|
||||
|
||||
def [](name)
|
||||
@fixtures[name]
|
||||
end
|
||||
|
||||
def fetch(*args)
|
||||
@fixtures.fetch(*args)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def freeze_recursively(data)
|
||||
data.each do |k, v|
|
||||
if v.is_a?(Hash)
|
||||
freeze_recursively(v)
|
||||
end
|
||||
end
|
||||
data.freeze
|
||||
end
|
||||
end
|
240
test/api_stub_helpers.rb
Normal file
240
test/api_stub_helpers.rb
Normal file
@ -0,0 +1,240 @@
|
||||
require "json"
|
||||
|
||||
# Provides a set of helpers for a test suite that help to mock out the Stripe
|
||||
# API.
|
||||
module APIStubHelpers
|
||||
protected
|
||||
|
||||
# Uses Webmock to stub out the Stripe API for testing purposes. The stub will
|
||||
# by default respond on any routes that are defined in the bundled
|
||||
# hyper-schema with generated response data.
|
||||
#
|
||||
# An `override_app` can be specified to get finer grain control over how a
|
||||
# stubbed endpoint responds. It can be used to modify generated responses,
|
||||
# mock expectations, or even to override the default stub completely.
|
||||
def stub_api(override_app = nil, &block)
|
||||
if block
|
||||
override_app = Sinatra.new(OverrideSinatra, &block)
|
||||
elsif !override_app
|
||||
override_app = @@default_override_app
|
||||
end
|
||||
|
||||
stub_request(:any, /^#{Stripe.api_base}/).to_rack(new_api_stub(override_app))
|
||||
end
|
||||
|
||||
def stub_connect
|
||||
stub_request(:any, /^#{Stripe.connect_base}/).to_return(:body => "{}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# APIStubMiddleware intercepts a response generated by Committee's stubbing
|
||||
# middleware, and tries to replace it with a better version from a set of
|
||||
# sample fixtures data generated from Stripe's core API service.
|
||||
class APIStubMiddleware
|
||||
API_FIXTURES = APIFixtures.new
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
# We use a vendor specific prefix (`x-resourceId`) embedded in the schema
|
||||
# of any resource in our spec to identify it (e.g. "charge"). This allows
|
||||
# us to cross-reference that response with some data that we might find
|
||||
# in our fixtures file so that we can respond with a higher fidelity
|
||||
# response.
|
||||
schema = env["committee.response_schema"]
|
||||
resource_id = schema.data["x-resourceId"] || ""
|
||||
|
||||
if data = API_FIXTURES[resource_id.to_sym]
|
||||
# standard top-level API resource
|
||||
data = fixturize_lists_recursively(schema, data)
|
||||
env["committee.response"] = data
|
||||
elsif schema.properties["object"].enum == ["list"]
|
||||
# top level list (like from a list endpoint)
|
||||
data = fixturize_list(schema, env["committee.response"])
|
||||
env["committee.response"] = data
|
||||
else
|
||||
raise "no fixture for: #{resource_id}"
|
||||
end
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# If schema looks like a Stripe list object, then we look up the resource
|
||||
# that the list is supposed to include and inject it into `data` as a
|
||||
# fixture. Also calls into that other schema recursively so that sublists
|
||||
# within it will also be assigned a fixture.
|
||||
def fixturize_list(schema, data)
|
||||
object_schema = schema.properties["object"]
|
||||
if object_schema && object_schema.enum == ["list"]
|
||||
subschema = schema.properties["data"].items
|
||||
resource_id = subschema.data["x-resourceId"] || ""
|
||||
if subdata = API_FIXTURES[resource_id.to_sym]
|
||||
subdata = fixturize_lists_recursively(subschema, subdata)
|
||||
|
||||
data = data ? data.dup : {}
|
||||
data[:data] = [subdata]
|
||||
end
|
||||
end
|
||||
data
|
||||
end
|
||||
|
||||
# Examines each of the given schema's properties and calls #fixturize_list
|
||||
# on them so that any sublists will be populated with sample fixture data.
|
||||
def fixturize_lists_recursively(schema, data)
|
||||
data = data.dup
|
||||
schema.properties.each do |key, subschema|
|
||||
data[key.to_sym] = fixturize_list(subschema, data[key.to_sym])
|
||||
end
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
# A descendant of the standard `Sinatra::Base` with some added helpers to
|
||||
# make working with generated responses more convenient.
|
||||
class OverrideSinatra < Sinatra::Base
|
||||
# A simple hash-like class that doesn't allow any keys to be accessed or
|
||||
# defined that were not present on its initialization.
|
||||
#
|
||||
# Its secondary function is allowing indifferent access regardless of
|
||||
# whether a string or symbol is used as a key.
|
||||
#
|
||||
# The purpose of the class is to make modifying API responses safer by
|
||||
# disallowing the setting of keys that were not in the original response.
|
||||
class TempermentalHash
|
||||
# Initializes a TempermentalHash from a standard Hash. Note that
|
||||
# initialization is performed recursively so any hashes included as
|
||||
# values of the top-level hash will also be concerted.
|
||||
def initialize(hash)
|
||||
@hash = hash.dup
|
||||
@hash.each do |k, v|
|
||||
@hash[k] = TempermentalHash.new(v) if v.is_a?(Hash)
|
||||
end
|
||||
end
|
||||
|
||||
def [](key)
|
||||
get(key)
|
||||
end
|
||||
|
||||
def []=(key, val)
|
||||
set(key, val)
|
||||
end
|
||||
|
||||
def deep_merge!(hash, options = {})
|
||||
hash.each do |k, v|
|
||||
if v.is_a?(Hash)
|
||||
if !@hash[k].is_a?(Hash)
|
||||
unless options[:allow_undefined_keys]
|
||||
raise ArgumentError, "'#{k}' in stub response is not a hash " +
|
||||
"and cannot be deep merged"
|
||||
end
|
||||
end
|
||||
val = self.get(
|
||||
k,
|
||||
:allow_undefined_keys => options[:allow_undefined_keys]
|
||||
)
|
||||
|
||||
if val
|
||||
val.deep_merge!(v)
|
||||
else
|
||||
self.set(
|
||||
k, v,
|
||||
:allow_undefined_keys => options[:allow_undefined_keys]
|
||||
)
|
||||
end
|
||||
else
|
||||
self.set(
|
||||
k, v,
|
||||
:allow_undefined_keys => options[:allow_undefined_keys]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get(key, options = {})
|
||||
key = key.to_s
|
||||
check_key!(key) unless options[:allow_undefined_keys]
|
||||
@hash[key]
|
||||
end
|
||||
|
||||
def set(key, val, options = {})
|
||||
key = key.to_s
|
||||
check_key!(key) unless options[:allow_undefined_keys]
|
||||
@hash[key] = val
|
||||
end
|
||||
|
||||
def to_h
|
||||
h = {}
|
||||
@hash.each do |k, v|
|
||||
h[k] = v.is_a?(TempermentalHash) ? v.to_h : v
|
||||
end
|
||||
h
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_key!(key)
|
||||
unless @hash.key?(key)
|
||||
raise ArgumentError, "'#{key}' is not defined in stub response"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def modify_generated_response
|
||||
safe_hash = TempermentalHash.new(env["committee.response"])
|
||||
yield(safe_hash)
|
||||
env["committee.response"] = safe_hash.to_h
|
||||
end
|
||||
|
||||
# The hash of data generated based on OpenAPI spec information for the
|
||||
# requested route of the API.
|
||||
#
|
||||
# It's also worth nothing that this could be `nil` in the event of the
|
||||
# spec not knowing how to respond to the requested route.
|
||||
def generated_response
|
||||
env["committee.response"]
|
||||
end
|
||||
|
||||
# This instructs the response stubbing framework that it should *not*
|
||||
# respond with a generated response on this request. Instead, control is
|
||||
# wholly given over to the override method.
|
||||
def override_response!
|
||||
env["committee.suppress"] = true
|
||||
end
|
||||
|
||||
not_found do
|
||||
"endpoint not found in API stub: #{request.request_method} #{request.path_info}"
|
||||
end
|
||||
end
|
||||
|
||||
# Finds the latest OpenAPI specification in ROOT/spec/ and parses it for
|
||||
# use with Committee.
|
||||
def self.initialize_spec
|
||||
schema_data = ::JSON.parse(File.read("#{PROJECT_ROOT}/spec/spec.json"))
|
||||
|
||||
driver = Committee::Drivers::OpenAPI2.new
|
||||
driver.parse(schema_data)
|
||||
end
|
||||
|
||||
# Creates a new Rack app with Committee middleware wrapping an internal app.
|
||||
def new_api_stub(override_app)
|
||||
Rack::Builder.new {
|
||||
use Committee::Middleware::RequestValidation, schema: @@spec,
|
||||
params_response: true, strict: true
|
||||
use Committee::Middleware::Stub, schema: @@spec,
|
||||
call: true
|
||||
use APIStubMiddleware
|
||||
run override_app
|
||||
}
|
||||
end
|
||||
|
||||
# Parse and initialize the hyper-schema only once for the entire test suite.
|
||||
@@spec = initialize_spec
|
||||
|
||||
# The default override app. Doesn't respond on any route so generated
|
||||
# responses will always take precedence.
|
||||
@@default_override_app = Sinatra.new
|
||||
end
|
@ -1,17 +1,38 @@
|
||||
require 'committee'
|
||||
require 'sinatra'
|
||||
require 'stripe'
|
||||
require 'test/unit'
|
||||
require 'mocha/setup'
|
||||
require 'stringio'
|
||||
require 'shoulda/context'
|
||||
require 'webmock/test_unit'
|
||||
|
||||
PROJECT_ROOT = File.expand_path("../../", __FILE__)
|
||||
|
||||
require File.expand_path('../api_fixtures', __FILE__)
|
||||
require File.expand_path('../api_stub_helpers', __FILE__)
|
||||
require File.expand_path('../test_data', __FILE__)
|
||||
|
||||
class Test::Unit::TestCase
|
||||
include APIStubHelpers
|
||||
include Stripe::TestData
|
||||
include Mocha
|
||||
|
||||
# Fixtures are available in tests using something like:
|
||||
#
|
||||
# API_FIXTURES[:charge][:id]
|
||||
#
|
||||
API_FIXTURES = APIFixtures.new
|
||||
|
||||
setup do
|
||||
Stripe.api_key = "foo"
|
||||
|
||||
# Stub the Stripe API with a default stub. Note that this method can be
|
||||
# called again in test bodies in order to override responses on particular
|
||||
# endpoints.
|
||||
stub_api
|
||||
|
||||
stub_connect
|
||||
end
|
||||
|
||||
teardown do
|
||||
|
Loading…
x
Reference in New Issue
Block a user