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:
Brandur 2017-02-01 16:06:08 -08:00
parent e9d4e27a4b
commit f0579950a7
3 changed files with 290 additions and 0 deletions

29
test/api_fixtures.rb Normal file
View 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
View 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

View File

@ -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