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 OpenAPI # spec 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 stub_request(:any, /^#{Stripe.api_base}/).to_rack(new_api_stub) 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` that we can use to enrich # certain types of responses. class APIStubApp < Sinatra::Base not_found do "endpoint not found in API stub: #{request.request_method} #{request.path_info}" end end # Finds the latest OpenAPI specification in ROOT/openapi/ and parses it for # use with Committee. def self.initialize_spec spec_data = ::JSON.parse(File.read("#{PROJECT_ROOT}/openapi/spec.json")) driver = Committee::Drivers::OpenAPI2.new driver.parse(spec_data) end # Creates a new Rack app with Committee middleware it. def new_api_stub 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 APIStubApp.new } end # Parse and initialize the OpenAPI spec 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