Renames and fixes specs in api and specs project to prefix OpenApi module. Gem name to open_api-rswag

This commit is contained in:
Jay Danielian 2019-07-27 14:53:01 -04:00
parent db3f321b45
commit 13f7007b2f
27 changed files with 1488 additions and 1451 deletions

View File

@ -6,7 +6,7 @@ Gem::Specification.new do |s|
s.version = ENV['TRAVIS_TAG'] || '0.0.0'
s.authors = ["Richie Morris", "Jay Danielian"]
s.email = ["domaindrivendev@gmail.com"]
s.homepage = "https://github.com/domaindrivendev/rswag"
s.homepage = "https://github.com/jdanielian/rswag"
s.summary = "A Rails Engine that exposes Swagger files as JSON endpoints"
s.description = "Open up your API to the phenomenal Swagger ecosystem by exposing Swagger files, that describe your service, as JSON endpoints"
s.license = "MIT"

View File

@ -0,0 +1,29 @@
require 'rspec/core'
require 'open_api/rswag/specs/example_group_helpers'
require 'open_api/rswag/specs/example_helpers'
require 'open_api/rswag/specs/configuration'
require 'open_api/rswag/specs/railtie' if defined?(Rails::Railtie)
module OpenApi
module Rswag
module Specs
# Extend RSpec with a swagger-based DSL
::RSpec.configure do |c|
c.add_setting :swagger_root
c.add_setting :swagger_docs
c.add_setting :swagger_dry_run
c.extend ExampleGroupHelpers, type: :request
c.include ExampleHelpers, type: :request
end
def self.config
@config ||= Configuration.new(RSpec.configuration)
end
# Support Rails 3+ and RSpec 2+ (sigh!)
RAILS_VERSION = Rails::VERSION::MAJOR
RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
end
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
module OpenApi
module Rswag
module Specs
class Configuration
def initialize(rspec_config)
@rspec_config = rspec_config
end
def swagger_root
@swagger_root ||= begin
if @rspec_config.swagger_root.nil?
raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb'
end
@rspec_config.swagger_root
end
end
def swagger_docs
@swagger_docs ||= begin
if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb'
end
@rspec_config.swagger_docs
end
end
def swagger_dry_run
@swagger_dry_run ||= begin
@rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
end
end
def get_swagger_doc(name)
return swagger_docs.values.first if name.nil?
raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]
swagger_docs[name]
end
end
class ConfigurationError < StandardError; end
end
end
end

View File

@ -0,0 +1,266 @@
# frozen_string_literal: true
require 'hashie'
module OpenApi
module Rswag
module Specs
module ExampleGroupHelpers
def path(template, metadata = {}, &block)
metadata[:path_item] = { template: template }
describe(template, metadata, &block)
end
%i[get post patch put delete head].each do |verb|
define_method(verb) do |summary, &block|
api_metadata = { operation: { verb: verb, summary: summary } }
describe(verb, api_metadata, &block)
end
end
%i[operationId deprecated security].each do |attr_name|
define_method(attr_name) do |value|
metadata[:operation][attr_name] = value
end
end
# NOTE: 'description' requires special treatment because ExampleGroup already
# defines a method with that name. Provide an override that supports the existing
# functionality while also setting the appropriate metadata if applicable
def description(value = nil)
return super() if value.nil?
metadata[:operation][:description] = value
end
# These are array properties - note the splat operator
%i[tags consumes produces schemes].each do |attr_name|
define_method(attr_name) do |*value|
metadata[:operation][attr_name] = value
end
end
# NICE TO HAVE
# TODO: update generator templates to include 3.0 syntax
# TODO: setup travis CI?
# MUST HAVES
# TODO: *** look at handling different ways schemas can be defined in 3.0 for requestBody and response
# can we handle all of them?
# Then can look at handling different request_body things like $ref, etc
# TODO: look at adding request_body method to handle diffs in Open API 2.0 to 3.0
# TODO: look at adding examples in content request_body
# https://swagger.io/docs/specification/describing-request-body/
# need to make sure we output requestBody in the swagger generator .json
# also need to make sure that it can handle content: , required: true/false, schema: ref
def request_body(attributes)
# can make this generic, and accept any incoming hash (like parameter method)
attributes.compact!
if metadata[:operation][:requestBody].blank?
metadata[:operation][:requestBody] = attributes
elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content]
# merge in
content_hash = metadata[:operation][:requestBody][:content]
incoming_content_hash = attributes[:content]
content_hash.merge!(incoming_content_hash) if incoming_content_hash
end
end
def request_body_json(schema:, required: true, description: nil, examples: nil)
passed_examples = Array(examples)
content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} }
request_body(description: description, required: required, content: content_hash)
if passed_examples.any?
# the request_factory is going to have to resolve the different ways that the example can be given
# it can contain a 'value' key which is a direct hash (easiest)
# it can contain a 'external_value' key which makes an external call to load the json
# it can contain a '$ref' key. Which points to #/components/examples/blog
passed_examples.each do |passed_example|
if passed_example.is_a?(Symbol)
example_key_name = passed_example
# TODO: write more tests around this adding to the parameter
# if symbol try and use save_request_example
param_attributes = { name: example_key_name, in: :body, required: required, param_value: example_key_name, schema: schema }
parameter(param_attributes)
elsif passed_example.is_a?(Hash) && passed_example[:externalValue]
param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example[:externalValue], schema: schema }
parameter(param_attributes)
elsif passed_example.is_a?(Hash) && passed_example['$ref']
param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example['$ref'], schema: schema }
parameter(param_attributes)
end
end
end
end
def request_body_text_plain(required: false, description: nil, examples: nil)
content_hash = { 'test/plain' => { schema: {type: :string}, examples: examples }.compact! || {} }
request_body(description: description, required: required, content: content_hash)
end
# TODO: add examples to this like we can for json, might be large lift as many assumptions are made on content-type
def request_body_xml(schema:,required: false, description: nil, examples: nil)
passed_examples = Array(examples)
content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} }
request_body(description: description, required: required, content: content_hash)
end
def request_body_multipart(schema:, description: nil)
content_hash = { 'multipart/form-data' => { schema: schema }}
request_body(description: description, content: content_hash)
schema.extend(Hashie::Extensions::DeepLocate)
file_properties = schema.deep_locate -> (_k, v, _obj) { v == :binary }
hash_locator = []
file_properties.each do |match|
hash_match = schema.deep_locate -> (_k, v, _obj) { v == match }
hash_locator.concat(hash_match) unless hash_match.empty?
end
property_hashes = hash_locator.flat_map do |locator|
locator.select { |_k,v| file_properties.include?(v) }
end
property_hashes.each do |property_hash|
file_name = property_hash.keys.first
parameter name: file_name, in: :formData, type: :file, required: true
end
end
def parameter(attributes)
if attributes[:in] && attributes[:in].to_sym == :path
attributes[:required] = true
end
if attributes[:type] && attributes[:schema].nil?
attributes[:schema] = {type: attributes[:type]}
end
if metadata.key?(:operation)
metadata[:operation][:parameters] ||= []
metadata[:operation][:parameters] << attributes
else
metadata[:path_item][:parameters] ||= []
metadata[:path_item][:parameters] << attributes
end
end
def response(code, description, metadata = {}, &block)
metadata[:response] = { code: code, description: description }
context(description, metadata, &block)
end
def schema(value, content_type: 'application/json')
content_hash = {content_type => {schema: value}}
metadata[:response][:content] = content_hash
end
def header(name, attributes)
metadata[:response][:headers] ||= {}
if attributes[:type] && attributes[:schema].nil?
attributes[:schema] = {type: attributes[:type]}
attributes.delete(:type)
end
metadata[:response][:headers][name] = attributes
end
# NOTE: Similar to 'description', 'examples' need to handle the case when
# being invoked with no params to avoid overriding 'examples' method of
# rspec-core ExampleGroup
def examples(example = nil)
return super() if example.nil?
metadata[:response][:examples] = example
end
# checks the examples in the parameters should be able to add $ref and externalValue examples.
# This syntax would look something like this in the integration _spec.rb file
#
# request_body_json schema: { '$ref' => '#/components/schemas/blog' },
# examples: [:blog, {name: :external_blog,
# externalValue: 'http://api.sample.org/myjson_example'},
# {name: :another_example,
# '$ref' => '#/components/examples/flexible_blog_example'}]
# The first value :blog, points to a let param of the same name, and is used to make the request in the
# integration test (it is used to build the request payload)
#
# The second item in the array shows how to add an externalValue for the examples in the requestBody section
# The third item shows how to add a $ref item that points to the components/examples section of the swagger spec.
#
# NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui
# will not show it yet
def merge_other_examples!(example_metadata)
# example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
content_node = example_metadata[:operation][:requestBody][:content]['application/json']
return unless content_node
external_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue] } || {}
ref_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref'] } || {}
examples_node = content_node[:examples] ||= {}
nodes_to_add = []
nodes_to_add << external_example unless external_example.empty?
nodes_to_add << ref_example unless ref_example.empty?
nodes_to_add.each do |node|
json_request_examples = examples_node ||= {}
other_name = node[:name][:name]
other_key = node[:name][:externalValue] ? :externalValue : '$ref'
if other_name
json_request_examples.merge!(other_name => {other_key => node[:param_value]})
end
end
end
def run_test!(&block)
# NOTE: rspec 2.x support
if RSPEC_VERSION < 3
before do
submit_request(example.metadata)
end
it "returns a #{metadata[:response][:code]} response" do
assert_response_matches_metadata(metadata)
block.call(response) if block_given?
end
else
before do |example|
submit_request(example.metadata) #
end
it "returns a #{metadata[:response][:code]} response" do |example|
assert_response_matches_metadata(example.metadata, &block)
example.instance_exec(response, &block) if block_given?
end
after do |example|
body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:required] }
if body_parameter && respond_to?(body_parameter[:name]) && example.metadata[:operation][:requestBody][:content]['application/json']
# save response examples by default
example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } unless response.body.to_s.empty?
# save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test
if response.code.to_s =~ /^2\d{2}$/
example.metadata[:operation][:requestBody][:content]['application/json'] = { examples: {} } unless example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
json_request_examples = example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) }
example.metadata[:operation][:requestBody][:content]['application/json'][:examples] = json_request_examples
end
end
self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody]
end
end
end
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'open_api/rswag/specs/request_factory'
require 'open_api/rswag/specs/response_validator'
module OpenApi
module Rswag
module Specs
module ExampleHelpers
def submit_request(metadata)
request = RequestFactory.new.build_request(metadata, self)
if RAILS_VERSION < 5
send(
request[:verb],
request[:path],
request[:payload],
request[:headers]
)
else
send(
request[:verb],
request[:path],
params: request[:payload],
headers: request[:headers]
)
end
end
def assert_response_matches_metadata(metadata)
ResponseValidator.new.validate!(metadata, response)
end
end
end
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'json-schema'
module OpenApi
module Rswag
module Specs
class ExtendedSchema < JSON::Schema::Draft4
def initialize
super
@attributes['type'] = ExtendedTypeAttribute
@uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema')
@names = ['http://tempuri.org/rswag/specs/extended_schema']
end
end
class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
def self.validate(current_schema, data, fragments, processor, validator, options = {})
return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true)
super
end
end
JSON::Validator.register_validator(ExtendedSchema.new)
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module OpenApi
module Rswag
module Specs
class Railtie < ::Rails::Railtie
rake_tasks do
load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __dir__)
end
end
end
end
end

View File

@ -0,0 +1,166 @@
# frozen_string_literal: true
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/conversions'
require 'json'
module OpenApi
module Rswag
module Specs
class RequestFactory
def initialize(config = ::OpenApi::Rswag::Specs.config)
@config = config
end
def build_request(metadata, example)
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
parameters = expand_parameters(metadata, swagger_doc, example)
{}.tap do |request|
add_verb(request, metadata)
add_path(request, metadata, swagger_doc, parameters, example)
add_headers(request, metadata, swagger_doc, parameters, example)
add_payload(request, parameters, example)
end
end
private
def expand_parameters(metadata, swagger_doc, example)
operation_params = metadata[:operation][:parameters] || []
path_item_params = metadata[:path_item][:parameters] || []
security_params = derive_security_params(metadata, swagger_doc)
# NOTE: Use of + instead of concat to avoid mutation of the metadata object
(operation_params + path_item_params + security_params)
.map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p }
.uniq { |p| p[:name] }
.reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
end
def derive_security_params(metadata, swagger_doc)
requirements = metadata[:operation][:security] || swagger_doc[:security] || []
scheme_names = requirements.flat_map(&:keys)
components = swagger_doc[:components] || {}
schemes = (components[:securitySchemes] || {}).slice(*scheme_names).values
schemes.map do |scheme|
param = scheme[:type] == :apiKey ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
param.merge(type: :string, required: requirements.one?)
end
end
def resolve_parameter(ref, swagger_doc)
key = ref.sub('#/parameters/', '').to_sym
definitions = swagger_doc[:parameters]
raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
definitions[key]
end
def add_verb(request, metadata)
request[:verb] = metadata[:operation][:verb]
end
def add_path(request, metadata, swagger_doc, parameters, example)
template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
request[:path] = template.tap do |template|
parameters.select { |p| p[:in] == :path }.each do |p|
template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
end
parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
template.concat(i == 0 ? '?' : '&')
template.concat(build_query_string_part(p, example.send(p[:name])))
end
end
end
def build_query_string_part(param, value)
name = param[:name]
return "#{name}=#{value}" unless param[:type].to_sym == :array
case param[:collectionFormat]
when :ssv
"#{name}=#{value.join(' ')}"
when :tsv
"#{name}=#{value.join('\t')}"
when :pipes
"#{name}=#{value.join('|')}"
when :multi
value.map { |v| "#{name}=#{v}" }.join('&')
else
"#{name}=#{value.join(',')}" # csv is default
end
end
def add_headers(request, metadata, swagger_doc, parameters, example)
tuples = parameters
.select { |p| p[:in] == :header }
.map { |p| [p[:name], example.send(p[:name]).to_s] }
# Accept header
produces = metadata[:operation][:produces] || swagger_doc[:produces]
if produces
accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first
tuples << ['Accept', accept]
end
# Content-Type header
consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
if consumes
content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first
tuples << ['Content-Type', content_type]
end
# Rails test infrastructure requires rackified headers
rackified_tuples = tuples.map do |pair|
[
case pair[0]
when 'Accept' then 'HTTP_ACCEPT'
when 'Content-Type' then 'CONTENT_TYPE'
when 'Authorization' then 'HTTP_AUTHORIZATION'
else pair[0]
end,
pair[1]
]
end
request[:headers] = Hash[rackified_tuples]
end
def add_payload(request, parameters, example)
content_type = request[:headers]['CONTENT_TYPE']
return if content_type.nil?
if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type)
request[:payload] = build_form_payload(parameters, example)
else
request[:payload] = build_json_payload(parameters, example)
end
end
def build_form_payload(parameters, example)
# See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
# Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
# Rails test infrastructure allows us to send the values directly as a hash
# PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
tuples = parameters
.select { |p| p[:in] == :formData }
.map { |p| [p[:name], example.send(p[:name])] }
Hash[tuples]
end
def build_json_payload(parameters, example)
body_param = parameters.select { |p| p[:in] == :body && p[:name].is_a?(Symbol) }.first
return nil unless body_param
source_body_param = example.send(body_param[:name]) if body_param[:name] && example.respond_to?(body_param[:name])
source_body_param ||= body_param[:param_value]
source_body_param ? source_body_param.to_json : nil
end
end
end
end
end

View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'active_support/core_ext/hash/slice'
require 'json-schema'
require 'json'
require 'open_api/rswag/specs/extended_schema'
module OpenApi
module Rswag
module Specs
class ResponseValidator
def initialize(config = ::OpenApi::Rswag::Specs.config)
@config = config
end
def validate!(metadata, response)
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
validate_code!(metadata, response)
validate_headers!(metadata, response.headers)
validate_body!(metadata, swagger_doc, response.body)
end
private
def validate_code!(metadata, response)
expected = metadata[:response][:code].to_s
if response.code != expected
raise UnexpectedResponse,
"Expected response code '#{response.code}' to match '#{expected}'\n" \
"Response body: #{response.body}"
end
end
def validate_headers!(metadata, headers)
expected = (metadata[:response][:headers] || {}).keys
expected.each do |name|
raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil?
end
end
def validate_body!(metadata, swagger_doc, body)
test_schemas = extract_schemas(metadata)
return if test_schemas.nil?
components = swagger_doc[:components] || {}
components_schemas = { components: { schemas: components[:schemas] } }
validation_schema = test_schemas[:schema] # response_schema
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
.merge(components_schemas)
errors = JSON::Validator.fully_validate(validation_schema, body)
raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
end
def extract_schemas(metadata)
metadata[:operation] = {produces: []} if metadata[:operation].nil?
produces = Array(metadata[:operation][:produces])
producer_content = produces.first || 'application/json'
response_content = metadata[:response][:content] || {producer_content => {}}
response_content[producer_content]
end
end
class UnexpectedResponse < StandardError; end
end
end
end

View File

@ -0,0 +1,102 @@
# frozen_string_literal: true
require 'active_support/core_ext/hash/deep_merge'
require 'swagger_helper'
module OpenApi
module Rswag
module Specs
class SwaggerFormatter
# NOTE: rspec 2.x support
if RSPEC_VERSION > 2
::RSpec::Core::Formatters.register self, :example_group_finished, :stop
end
def initialize(output, config = ::OpenApi::Rswag::Specs.config)
@output = output
@config = config
@output.puts 'Generating Swagger docs ...'
end
def example_group_finished(notification)
# NOTE: rspec 2.x support
metadata = if RSPEC_VERSION > 2
notification.group.metadata
else
notification.metadata
end
return unless metadata.key?(:response)
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
swagger_doc.deep_merge!(metadata_to_swagger(metadata))
end
def stop(_notification = nil)
@config.swagger_docs.each do |url_path, doc|
# remove 2.0 parameters
doc[:paths]&.each_pair do |_k, v|
v.each_pair do |_verb, value|
is_hash = value.is_a?(Hash)
if is_hash && value.dig(:parameters)
schema_param = value&.dig(:parameters)&.find{|p| p[:in] == :body && p[:schema] }
if value && schema_param && value&.dig(:requestBody, :content, 'application/json')
value[:requestBody][:content]['application/json'].merge!(schema: schema_param[:schema])
end
value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
value[:parameters].each { |p| p.delete(:type) }
value[:headers].each { |p| p.delete(:type)} if value[:headers]
end
value.delete(:consumes) if is_hash && value.dig(:consumes)
value.delete(:produces) if is_hash && value.dig(:produces)
end
end
file_path = File.join(@config.swagger_root, url_path)
dirname = File.dirname(file_path)
FileUtils.mkdir_p dirname unless File.exist?(dirname)
File.open(file_path, 'w') do |file|
file.write(JSON.pretty_generate(doc))
end
@output.puts "Swagger doc generated at #{file_path}"
end
end
private
def metadata_to_swagger(metadata)
response_code = metadata[:response][:code]
response = metadata[:response].reject { |k, _v| k == :code }
# need to merge in to response
if response[:examples]&.dig('application/json')
example = response[:examples].dig('application/json').dup
schema = response.dig(:content, 'application/json', :schema)
new_hash = {example: example}
new_hash[:schema] = schema if schema
response.merge!(content: { 'application/json' => new_hash })
response.delete(:examples)
end
verb = metadata[:operation][:verb]
operation = metadata[:operation]
.reject { |k, _v| k == :verb }
.merge(responses: { response_code => response })
path_template = metadata[:path_item][:template]
path_item = metadata[:path_item]
.reject { |k, _v| k == :template }
.merge(verb => operation)
{ paths: { path_template => path_item } }
end
end
end
end
end

View File

@ -1,27 +0,0 @@
require 'rspec/core'
require 'rswag/specs/example_group_helpers'
require 'rswag/specs/example_helpers'
require 'rswag/specs/configuration'
require 'rswag/specs/railtie' if defined?(Rails::Railtie)
module Rswag
module Specs
# Extend RSpec with a swagger-based DSL
::RSpec.configure do |c|
c.add_setting :swagger_root
c.add_setting :swagger_docs
c.add_setting :swagger_dry_run
c.extend ExampleGroupHelpers, type: :request
c.include ExampleHelpers, type: :request
end
def self.config
@config ||= Configuration.new(RSpec.configuration)
end
# Support Rails 3+ and RSpec 2+ (sigh!)
RAILS_VERSION = Rails::VERSION::MAJOR
RSPEC_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
end
end

View File

@ -1,46 +0,0 @@
# frozen_string_literal: true
module Rswag
module Specs
class Configuration
def initialize(rspec_config)
@rspec_config = rspec_config
end
def swagger_root
@swagger_root ||= begin
if @rspec_config.swagger_root.nil?
raise ConfigurationError, 'No swagger_root provided. See swagger_helper.rb'
end
@rspec_config.swagger_root
end
end
def swagger_docs
@swagger_docs ||= begin
if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
raise ConfigurationError, 'No swagger_docs defined. See swagger_helper.rb'
end
@rspec_config.swagger_docs
end
end
def swagger_dry_run
@swagger_dry_run ||= begin
@rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
end
end
def get_swagger_doc(name)
return swagger_docs.values.first if name.nil?
raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]
swagger_docs[name]
end
end
class ConfigurationError < StandardError; end
end
end

View File

@ -1,264 +0,0 @@
# frozen_string_literal: true
require 'hashie'
module Rswag
module Specs
module ExampleGroupHelpers
def path(template, metadata = {}, &block)
metadata[:path_item] = { template: template }
describe(template, metadata, &block)
end
%i[get post patch put delete head].each do |verb|
define_method(verb) do |summary, &block|
api_metadata = { operation: { verb: verb, summary: summary } }
describe(verb, api_metadata, &block)
end
end
%i[operationId deprecated security].each do |attr_name|
define_method(attr_name) do |value|
metadata[:operation][attr_name] = value
end
end
# NOTE: 'description' requires special treatment because ExampleGroup already
# defines a method with that name. Provide an override that supports the existing
# functionality while also setting the appropriate metadata if applicable
def description(value = nil)
return super() if value.nil?
metadata[:operation][:description] = value
end
# These are array properties - note the splat operator
%i[tags consumes produces schemes].each do |attr_name|
define_method(attr_name) do |*value|
metadata[:operation][attr_name] = value
end
end
# NICE TO HAVE
# TODO: update generator templates to include 3.0 syntax
# TODO: setup travis CI?
# MUST HAVES
# TODO: *** look at handling different ways schemas can be defined in 3.0 for requestBody and response
# can we handle all of them?
# Then can look at handling different request_body things like $ref, etc
# TODO: look at adding request_body method to handle diffs in Open API 2.0 to 3.0
# TODO: look at adding examples in content request_body
# https://swagger.io/docs/specification/describing-request-body/
# need to make sure we output requestBody in the swagger generator .json
# also need to make sure that it can handle content: , required: true/false, schema: ref
def request_body(attributes)
# can make this generic, and accept any incoming hash (like parameter method)
attributes.compact!
if metadata[:operation][:requestBody].blank?
metadata[:operation][:requestBody] = attributes
elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content]
# merge in
content_hash = metadata[:operation][:requestBody][:content]
incoming_content_hash = attributes[:content]
content_hash.merge!(incoming_content_hash) if incoming_content_hash
end
end
def request_body_json(schema:, required: true, description: nil, examples: nil)
passed_examples = Array(examples)
content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} }
request_body(description: description, required: required, content: content_hash)
if passed_examples.any?
# the request_factory is going to have to resolve the different ways that the example can be given
# it can contain a 'value' key which is a direct hash (easiest)
# it can contain a 'external_value' key which makes an external call to load the json
# it can contain a '$ref' key. Which points to #/components/examples/blog
passed_examples.each do |passed_example|
if passed_example.is_a?(Symbol)
example_key_name = passed_example
# TODO: write more tests around this adding to the parameter
# if symbol try and use save_request_example
param_attributes = { name: example_key_name, in: :body, required: required, param_value: example_key_name, schema: schema }
parameter(param_attributes)
elsif passed_example.is_a?(Hash) && passed_example[:externalValue]
param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example[:externalValue], schema: schema }
parameter(param_attributes)
elsif passed_example.is_a?(Hash) && passed_example['$ref']
param_attributes = { name: passed_example, in: :body, required: required, param_value: passed_example['$ref'], schema: schema }
parameter(param_attributes)
end
end
end
end
def request_body_text_plain(required: false, description: nil, examples: nil)
content_hash = { 'test/plain' => { schema: {type: :string}, examples: examples }.compact! || {} }
request_body(description: description, required: required, content: content_hash)
end
# TODO: add examples to this like we can for json, might be large lift as many assumptions are made on content-type
def request_body_xml(schema:,required: false, description: nil, examples: nil)
passed_examples = Array(examples)
content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} }
request_body(description: description, required: required, content: content_hash)
end
def request_body_multipart(schema:, description: nil)
content_hash = { 'multipart/form-data' => { schema: schema }}
request_body(description: description, content: content_hash)
schema.extend(Hashie::Extensions::DeepLocate)
file_properties = schema.deep_locate -> (_k, v, _obj) { v == :binary }
hash_locator = []
file_properties.each do |match|
hash_match = schema.deep_locate -> (_k, v, _obj) { v == match }
hash_locator.concat(hash_match) unless hash_match.empty?
end
property_hashes = hash_locator.flat_map do |locator|
locator.select { |_k,v| file_properties.include?(v) }
end
property_hashes.each do |property_hash|
file_name = property_hash.keys.first
parameter name: file_name, in: :formData, type: :file, required: true
end
end
def parameter(attributes)
if attributes[:in] && attributes[:in].to_sym == :path
attributes[:required] = true
end
if attributes[:type] && attributes[:schema].nil?
attributes[:schema] = {type: attributes[:type]}
end
if metadata.key?(:operation)
metadata[:operation][:parameters] ||= []
metadata[:operation][:parameters] << attributes
else
metadata[:path_item][:parameters] ||= []
metadata[:path_item][:parameters] << attributes
end
end
def response(code, description, metadata = {}, &block)
metadata[:response] = { code: code, description: description }
context(description, metadata, &block)
end
def schema(value, content_type: 'application/json')
content_hash = {content_type => {schema: value}}
metadata[:response][:content] = content_hash
end
def header(name, attributes)
metadata[:response][:headers] ||= {}
if attributes[:type] && attributes[:schema].nil?
attributes[:schema] = {type: attributes[:type]}
attributes.delete(:type)
end
metadata[:response][:headers][name] = attributes
end
# NOTE: Similar to 'description', 'examples' need to handle the case when
# being invoked with no params to avoid overriding 'examples' method of
# rspec-core ExampleGroup
def examples(example = nil)
return super() if example.nil?
metadata[:response][:examples] = example
end
# checks the examples in the parameters should be able to add $ref and externalValue examples.
# This syntax would look something like this in the integration _spec.rb file
#
# request_body_json schema: { '$ref' => '#/components/schemas/blog' },
# examples: [:blog, {name: :external_blog,
# externalValue: 'http://api.sample.org/myjson_example'},
# {name: :another_example,
# '$ref' => '#/components/examples/flexible_blog_example'}]
# The first value :blog, points to a let param of the same name, and is used to make the request in the
# integration test (it is used to build the request payload)
#
# The second item in the array shows how to add an externalValue for the examples in the requestBody section
# The third item shows how to add a $ref item that points to the components/examples section of the swagger spec.
#
# NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui
# will not show it yet
def merge_other_examples!(example_metadata)
# example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
content_node = example_metadata[:operation][:requestBody][:content]['application/json']
return unless content_node
external_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue] } || {}
ref_example = example_metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref'] } || {}
examples_node = content_node[:examples] ||= {}
nodes_to_add = []
nodes_to_add << external_example unless external_example.empty?
nodes_to_add << ref_example unless ref_example.empty?
nodes_to_add.each do |node|
json_request_examples = examples_node ||= {}
other_name = node[:name][:name]
other_key = node[:name][:externalValue] ? :externalValue : '$ref'
if other_name
json_request_examples.merge!(other_name => {other_key => node[:param_value]})
end
end
end
def run_test!(&block)
# NOTE: rspec 2.x support
if RSPEC_VERSION < 3
before do
submit_request(example.metadata)
end
it "returns a #{metadata[:response][:code]} response" do
assert_response_matches_metadata(metadata)
block.call(response) if block_given?
end
else
before do |example|
submit_request(example.metadata) #
end
it "returns a #{metadata[:response][:code]} response" do |example|
assert_response_matches_metadata(example.metadata, &block)
example.instance_exec(response, &block) if block_given?
end
after do |example|
body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect { |p| p[:in] == :body && p[:required] }
if body_parameter && respond_to?(body_parameter[:name]) && example.metadata[:operation][:requestBody][:content]['application/json']
# save response examples by default
example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } unless response.body.to_s.empty?
# save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test
if response.code.to_s =~ /^2\d{2}$/
example.metadata[:operation][:requestBody][:content]['application/json'] = { examples: {} } unless example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
json_request_examples = example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) }
example.metadata[:operation][:requestBody][:content]['application/json'][:examples] = json_request_examples
end
end
self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody]
end
end
end
end
end
end

View File

@ -1,34 +0,0 @@
# frozen_string_literal: true
require 'rswag/specs/request_factory'
require 'rswag/specs/response_validator'
module Rswag
module Specs
module ExampleHelpers
def submit_request(metadata)
request = RequestFactory.new.build_request(metadata, self)
if RAILS_VERSION < 5
send(
request[:verb],
request[:path],
request[:payload],
request[:headers]
)
else
send(
request[:verb],
request[:path],
params: request[:payload],
headers: request[:headers]
)
end
end
def assert_response_matches_metadata(metadata)
ResponseValidator.new.validate!(metadata, response)
end
end
end
end

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
require 'json-schema'
module Rswag
module Specs
class ExtendedSchema < JSON::Schema::Draft4
def initialize
super
@attributes['type'] = ExtendedTypeAttribute
@uri = URI.parse('http://tempuri.org/rswag/specs/extended_schema')
@names = ['http://tempuri.org/rswag/specs/extended_schema']
end
end
class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
def self.validate(current_schema, data, fragments, processor, validator, options = {})
return if data.nil? && (current_schema.schema['nullable'] == true || current_schema.schema['x-nullable'] == true)
super
end
end
JSON::Validator.register_validator(ExtendedSchema.new)
end
end

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
module Rswag
module Specs
class Railtie < ::Rails::Railtie
rake_tasks do
load File.expand_path('../../tasks/rswag-specs_tasks.rake', __dir__)
end
end
end
end

View File

@ -1,164 +0,0 @@
# frozen_string_literal: true
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/conversions'
require 'json'
module Rswag
module Specs
class RequestFactory
def initialize(config = ::Rswag::Specs.config)
@config = config
end
def build_request(metadata, example)
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
parameters = expand_parameters(metadata, swagger_doc, example)
{}.tap do |request|
add_verb(request, metadata)
add_path(request, metadata, swagger_doc, parameters, example)
add_headers(request, metadata, swagger_doc, parameters, example)
add_payload(request, parameters, example)
end
end
private
def expand_parameters(metadata, swagger_doc, example)
operation_params = metadata[:operation][:parameters] || []
path_item_params = metadata[:path_item][:parameters] || []
security_params = derive_security_params(metadata, swagger_doc)
# NOTE: Use of + instead of concat to avoid mutation of the metadata object
(operation_params + path_item_params + security_params)
.map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p }
.uniq { |p| p[:name] }
.reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
end
def derive_security_params(metadata, swagger_doc)
requirements = metadata[:operation][:security] || swagger_doc[:security] || []
scheme_names = requirements.flat_map(&:keys)
components = swagger_doc[:components] || {}
schemes = (components[:securitySchemes] || {}).slice(*scheme_names).values
schemes.map do |scheme|
param = scheme[:type] == :apiKey ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
param.merge(type: :string, required: requirements.one?)
end
end
def resolve_parameter(ref, swagger_doc)
key = ref.sub('#/parameters/', '').to_sym
definitions = swagger_doc[:parameters]
raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
definitions[key]
end
def add_verb(request, metadata)
request[:verb] = metadata[:operation][:verb]
end
def add_path(request, metadata, swagger_doc, parameters, example)
template = (swagger_doc[:basePath] || '') + metadata[:path_item][:template]
request[:path] = template.tap do |template|
parameters.select { |p| p[:in] == :path }.each do |p|
template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
end
parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
template.concat(i == 0 ? '?' : '&')
template.concat(build_query_string_part(p, example.send(p[:name])))
end
end
end
def build_query_string_part(param, value)
name = param[:name]
return "#{name}=#{value}" unless param[:type].to_sym == :array
case param[:collectionFormat]
when :ssv
"#{name}=#{value.join(' ')}"
when :tsv
"#{name}=#{value.join('\t')}"
when :pipes
"#{name}=#{value.join('|')}"
when :multi
value.map { |v| "#{name}=#{v}" }.join('&')
else
"#{name}=#{value.join(',')}" # csv is default
end
end
def add_headers(request, metadata, swagger_doc, parameters, example)
tuples = parameters
.select { |p| p[:in] == :header }
.map { |p| [p[:name], example.send(p[:name]).to_s] }
# Accept header
produces = metadata[:operation][:produces] || swagger_doc[:produces]
if produces
accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first
tuples << ['Accept', accept]
end
# Content-Type header
consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
if consumes
content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first
tuples << ['Content-Type', content_type]
end
# Rails test infrastructure requires rackified headers
rackified_tuples = tuples.map do |pair|
[
case pair[0]
when 'Accept' then 'HTTP_ACCEPT'
when 'Content-Type' then 'CONTENT_TYPE'
when 'Authorization' then 'HTTP_AUTHORIZATION'
else pair[0]
end,
pair[1]
]
end
request[:headers] = Hash[rackified_tuples]
end
def add_payload(request, parameters, example)
content_type = request[:headers]['CONTENT_TYPE']
return if content_type.nil?
if ['application/x-www-form-urlencoded', 'multipart/form-data'].include?(content_type)
request[:payload] = build_form_payload(parameters, example)
else
request[:payload] = build_json_payload(parameters, example)
end
end
def build_form_payload(parameters, example)
# See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
# Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
# Rails test infrastructure allows us to send the values directly as a hash
# PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
tuples = parameters
.select { |p| p[:in] == :formData }
.map { |p| [p[:name], example.send(p[:name])] }
Hash[tuples]
end
def build_json_payload(parameters, example)
body_param = parameters.select { |p| p[:in] == :body && p[:name].is_a?(Symbol) }.first
return nil unless body_param
source_body_param = example.send(body_param[:name]) if body_param[:name] && example.respond_to?(body_param[:name])
source_body_param ||= body_param[:param_value]
source_body_param ? source_body_param.to_json : nil
end
end
end
end

View File

@ -1,65 +0,0 @@
# frozen_string_literal: true
require 'active_support/core_ext/hash/slice'
require 'json-schema'
require 'json'
require 'rswag/specs/extended_schema'
module Rswag
module Specs
class ResponseValidator
def initialize(config = ::Rswag::Specs.config)
@config = config
end
def validate!(metadata, response)
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
validate_code!(metadata, response)
validate_headers!(metadata, response.headers)
validate_body!(metadata, swagger_doc, response.body)
end
private
def validate_code!(metadata, response)
expected = metadata[:response][:code].to_s
if response.code != expected
raise UnexpectedResponse,
"Expected response code '#{response.code}' to match '#{expected}'\n" \
"Response body: #{response.body}"
end
end
def validate_headers!(metadata, headers)
expected = (metadata[:response][:headers] || {}).keys
expected.each do |name|
raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil?
end
end
def validate_body!(metadata, swagger_doc, body)
test_schemas = extract_schemas(metadata)
return if test_schemas.nil?
components = swagger_doc[:components] || {}
components_schemas = { components: { schemas: components[:schemas] } }
validation_schema = test_schemas[:schema] # response_schema
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
.merge(components_schemas)
errors = JSON::Validator.fully_validate(validation_schema, body)
raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
end
def extract_schemas(metadata)
produces = Array(metadata[:operation][:produces])
response_content = metadata[:response][:content] || {}
producer_content = produces.first || 'application/json'
response_content[producer_content]
end
end
class UnexpectedResponse < StandardError; end
end
end

View File

@ -1,100 +0,0 @@
# frozen_string_literal: true
require 'active_support/core_ext/hash/deep_merge'
require 'swagger_helper'
module Rswag
module Specs
class SwaggerFormatter
# NOTE: rspec 2.x support
if RSPEC_VERSION > 2
::RSpec::Core::Formatters.register self, :example_group_finished, :stop
end
def initialize(output, config = Rswag::Specs.config)
@output = output
@config = config
@output.puts 'Generating Swagger docs ...'
end
def example_group_finished(notification)
# NOTE: rspec 2.x support
metadata = if RSPEC_VERSION > 2
notification.group.metadata
else
notification.metadata
end
return unless metadata.key?(:response)
swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
swagger_doc.deep_merge!(metadata_to_swagger(metadata))
end
def stop(_notification = nil)
@config.swagger_docs.each do |url_path, doc|
# remove 2.0 parameters
doc[:paths]&.each_pair do |_k, v|
v.each_pair do |_verb, value|
is_hash = value.is_a?(Hash)
if is_hash && value.dig(:parameters)
schema_param = value&.dig(:parameters)&.find{|p| p[:in] == :body && p[:schema] }
if value && schema_param && value&.dig(:requestBody, :content, 'application/json')
value[:requestBody][:content]['application/json'].merge!(schema: schema_param[:schema])
end
value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
value[:parameters].each { |p| p.delete(:type) }
value[:headers].each { |p| p.delete(:type)} if value[:headers]
end
value.delete(:consumes) if is_hash && value.dig(:consumes)
value.delete(:produces) if is_hash && value.dig(:produces)
end
end
file_path = File.join(@config.swagger_root, url_path)
dirname = File.dirname(file_path)
FileUtils.mkdir_p dirname unless File.exist?(dirname)
File.open(file_path, 'w') do |file|
file.write(JSON.pretty_generate(doc))
end
@output.puts "Swagger doc generated at #{file_path}"
end
end
private
def metadata_to_swagger(metadata)
response_code = metadata[:response][:code]
response = metadata[:response].reject { |k, _v| k == :code }
# need to merge in to response
if response[:examples]&.dig('application/json')
example = response[:examples].dig('application/json').dup
schema = response.dig(:content, 'application/json', :schema)
new_hash = {example: example}
new_hash[:schema] = schema if schema
response.merge!(content: { 'application/json' => new_hash })
response.delete(:examples)
end
verb = metadata[:operation][:verb]
operation = metadata[:operation]
.reject { |k, _v| k == :verb }
.merge(responses: { response_code => response })
path_template = metadata[:path_item][:template]
path_item = metadata[:path_item]
.reject { |k, _v| k == :template }
.merge(verb => operation)
{ paths: { path_template => path_item } }
end
end
end
end

View File

@ -6,9 +6,9 @@ $LOAD_PATH.push File.expand_path('lib', __dir__)
Gem::Specification.new do |s|
s.name = 'rswag-specs'
s.version = ENV['TRAVIS_TAG'] || '0.0.0'
s.authors = ['Richie Morris']
s.authors = ['Richie Morris', 'Jay Danielian']
s.email = ['domaindrivendev@gmail.com']
s.homepage = 'https://github.com/domaindrivendev/rswag'
s.homepage = 'https://github.com/jdanielian/rswag'
s.summary = 'A Swagger-based DSL for rspec-rails & accompanying rake task for generating Swagger files'
s.description = 'Simplify API integration testing with a succinct rspec DSL and generate Swagger files directly from your rspecs'
s.license = 'MIT'

View File

@ -1,75 +1,77 @@
# frozen_string_literal: true
require 'rswag/specs/configuration'
require 'open_api/rswag/specs/configuration'
module Rswag
module Specs
describe Configuration do
subject { described_class.new(rspec_config) }
module OpenApi
module Rswag
module Specs
describe Configuration do
subject { described_class.new(rspec_config) }
let(:rspec_config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) }
let(:swagger_root) { 'foobar' }
let(:swagger_docs) do
{
'v1/swagger.json' => { info: { title: 'v1' } },
'v2/swagger.json' => { info: { title: 'v2' } }
}
end
describe '#swagger_root' do
let(:response) { subject.swagger_root }
context 'provided in rspec config' do
it { expect(response).to eq('foobar') }
let(:rspec_config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) }
let(:swagger_root) { 'foobar' }
let(:swagger_docs) do
{
'v1/swagger.json' => { info: { title: 'v1' } },
'v2/swagger.json' => { info: { title: 'v2' } }
}
end
context 'not provided' do
let(:swagger_root) { nil }
it { expect { response }.to raise_error ConfigurationError }
end
end
describe '#swagger_root' do
let(:response) { subject.swagger_root }
describe '#swagger_docs' do
let(:response) { subject.swagger_docs }
context 'provided in rspec config' do
it { expect(response).to eq('foobar') }
end
context 'provided in rspec config' do
it { expect(response).to be_an_instance_of(Hash) }
end
context 'not provided' do
let(:swagger_docs) { nil }
it { expect { response }.to raise_error ConfigurationError }
end
context 'provided but empty' do
let(:swagger_docs) { {} }
it { expect { response }.to raise_error ConfigurationError }
end
end
describe '#get_swagger_doc(tag=nil)' do
let(:swagger_doc) { subject.get_swagger_doc(tag) }
context 'no tag provided' do
let(:tag) { nil }
it 'returns the first doc in rspec config' do
expect(swagger_doc).to eq(info: { title: 'v1' })
context 'not provided' do
let(:swagger_root) { nil }
it { expect { response }.to raise_error ConfigurationError }
end
end
context 'tag provided' do
context 'matching doc' do
let(:tag) { 'v2/swagger.json' }
describe '#swagger_docs' do
let(:response) { subject.swagger_docs }
it 'returns the matching doc in rspec config' do
expect(swagger_doc).to eq(info: { title: 'v2' })
context 'provided in rspec config' do
it { expect(response).to be_an_instance_of(Hash) }
end
context 'not provided' do
let(:swagger_docs) { nil }
it { expect { response }.to raise_error ConfigurationError }
end
context 'provided but empty' do
let(:swagger_docs) { {} }
it { expect { response }.to raise_error ConfigurationError }
end
end
describe '#get_swagger_doc(tag=nil)' do
let(:swagger_doc) { subject.get_swagger_doc(tag) }
context 'no tag provided' do
let(:tag) { nil }
it 'returns the first doc in rspec config' do
expect(swagger_doc).to eq(info: { title: 'v1' })
end
end
context 'no matching doc' do
let(:tag) { 'foobar' }
it { expect { swagger_doc }.to raise_error ConfigurationError }
context 'tag provided' do
context 'matching doc' do
let(:tag) { 'v2/swagger.json' }
it 'returns the matching doc in rspec config' do
expect(swagger_doc).to eq(info: { title: 'v2' })
end
end
context 'no matching doc' do
let(:tag) { 'foobar' }
it { expect { swagger_doc }.to raise_error ConfigurationError }
end
end
end
end

View File

@ -1,215 +1,217 @@
# frozen_string_literal: true
require 'rswag/specs/example_group_helpers'
require 'open_api/rswag/specs/example_group_helpers'
module Rswag
module Specs
describe ExampleGroupHelpers do
subject { double('example_group') }
module OpenApi
module Rswag
module Specs
describe ExampleGroupHelpers do
subject { double('example_group') }
before do
subject.extend ExampleGroupHelpers
allow(subject).to receive(:describe)
allow(subject).to receive(:context)
allow(subject).to receive(:metadata).and_return(api_metadata)
end
let(:api_metadata) { {} }
describe '#path(path)' do
before { subject.path('/blogs') }
it "delegates to 'describe' with 'path' metadata" do
expect(subject).to have_received(:describe).with(
'/blogs', path_item: { template: '/blogs' }
)
end
end
describe '#get|post|patch|put|delete|head(verb, summary)' do
before { subject.post('Creates a blog') }
it "delegates to 'describe' with 'operation' metadata" do
expect(subject).to have_received(:describe).with(
:post, operation: { verb: :post, summary: 'Creates a blog' }
)
end
end
describe '#tags|description|operationId|consumes|produces|schemes|deprecated(value)' do
before do
subject.tags('Blogs', 'Admin')
subject.description('Some description')
subject.operationId('createBlog')
subject.consumes('application/json', 'application/xml')
subject.produces('application/json', 'application/xml')
subject.schemes('http', 'https')
subject.deprecated(true)
subject.extend ExampleGroupHelpers
allow(subject).to receive(:describe)
allow(subject).to receive(:context)
allow(subject).to receive(:metadata).and_return(api_metadata)
end
let(:api_metadata) { { operation: {} } }
let(:api_metadata) { {} }
it "adds to the 'operation' metadata" do
expect(api_metadata[:operation]).to match(
tags: %w[Blogs Admin],
description: 'Some description',
operationId: 'createBlog',
consumes: ['application/json', 'application/xml'],
produces: ['application/json', 'application/xml'],
schemes: %w[http https],
deprecated: true
)
end
end
describe '#path(path)' do
before { subject.path('/blogs') }
describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do
before do
subject.tags('Blogs', 'Admin')
subject.description('Some description')
subject.operationId('createBlog')
subject.consumes('application/json', 'application/xml')
subject.produces('application/json', 'application/xml')
subject.schemes('http', 'https')
subject.deprecated(true)
subject.security(api_key: [])
end
let(:api_metadata) { { operation: {} } }
it "adds to the 'operation' metadata" do
expect(api_metadata[:operation]).to match(
tags: %w[Blogs Admin],
description: 'Some description',
operationId: 'createBlog',
consumes: ['application/json', 'application/xml'],
produces: ['application/json', 'application/xml'],
schemes: %w[http https],
deprecated: true,
security: { api_key: [] }
)
end
end
describe '#request_body_json(schema)' do
let(:api_metadata) { { path_item: {}, operation: {} } } # i.e. operation defined
context 'when required is not supplied' do
before { subject.request_body_json(schema: { type: 'object' }) }
it 'adds required true by default' do
expect(api_metadata[:operation][:requestBody]).to match(
required: true, content: { 'application/json' => { schema: { type: 'object' } } }
it "delegates to 'describe' with 'path' metadata" do
expect(subject).to have_received(:describe).with(
'/blogs', path_item: { template: '/blogs' }
)
end
end
context 'when required is supplied' do
before { subject.request_body_json(schema: { type: 'object' }, required: false) }
describe '#get|post|patch|put|delete|head(verb, summary)' do
before { subject.post('Creates a blog') }
it 'adds required false' do
expect(api_metadata[:operation][:requestBody]).to match(
required: false, content: { 'application/json' => { schema: { type: 'object' } } }
it "delegates to 'describe' with 'operation' metadata" do
expect(subject).to have_received(:describe).with(
:post, operation: { verb: :post, summary: 'Creates a blog' }
)
end
end
context 'when required is supplied' do
before { subject.request_body_json(schema: { type: 'object' }, description: 'my description') }
it 'adds description' do
expect(api_metadata[:operation][:requestBody]).to match(
description: 'my description', required: true, content: { 'application/json' => { schema: { type: 'object' } } }
)
describe '#tags|description|operationId|consumes|produces|schemes|deprecated(value)' do
before do
subject.tags('Blogs', 'Admin')
subject.description('Some description')
subject.operationId('createBlog')
subject.consumes('application/json', 'application/xml')
subject.produces('application/json', 'application/xml')
subject.schemes('http', 'https')
subject.deprecated(true)
end
end
end
let(:api_metadata) { { operation: {} } }
describe '#parameter(attributes)' do
context "when called at the 'path' level" do
before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) }
let(:api_metadata) { { path_item: {} } } # i.e. operation not defined yet
it "adds to the 'path_item parameters' metadata" do
expect(api_metadata[:path_item][:parameters]).to match(
[name: :blog, in: :body, schema: { type: 'object' }]
)
it "adds to the 'operation' metadata" do
expect(api_metadata[:operation]).to match(
tags: %w[Blogs Admin],
description: 'Some description',
operationId: 'createBlog',
consumes: ['application/json', 'application/xml'],
produces: ['application/json', 'application/xml'],
schemes: %w[http https],
deprecated: true
)
end
end
context "when called at the 'operation' level" do
before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) }
describe '#tags|description|operationId|consumes|produces|schemes|deprecated|security(value)' do
before do
subject.tags('Blogs', 'Admin')
subject.description('Some description')
subject.operationId('createBlog')
subject.consumes('application/json', 'application/xml')
subject.produces('application/json', 'application/xml')
subject.schemes('http', 'https')
subject.deprecated(true)
subject.security(api_key: [])
end
let(:api_metadata) { { operation: {} } }
it "adds to the 'operation' metadata" do
expect(api_metadata[:operation]).to match(
tags: %w[Blogs Admin],
description: 'Some description',
operationId: 'createBlog',
consumes: ['application/json', 'application/xml'],
produces: ['application/json', 'application/xml'],
schemes: %w[http https],
deprecated: true,
security: { api_key: [] }
)
end
end
describe '#request_body_json(schema)' do
let(:api_metadata) { { path_item: {}, operation: {} } } # i.e. operation defined
context 'when required is not supplied' do
before { subject.request_body_json(schema: { type: 'object' }) }
it "adds to the 'operation parameters' metadata" do
expect(api_metadata[:operation][:parameters]).to match(
[name: :blog, in: :body, schema: { type: 'object' }]
it 'adds required true by default' do
expect(api_metadata[:operation][:requestBody]).to match(
required: true, content: { 'application/json' => { schema: { type: 'object' } } }
)
end
end
context 'when required is supplied' do
before { subject.request_body_json(schema: { type: 'object' }, required: false) }
it 'adds required false' do
expect(api_metadata[:operation][:requestBody]).to match(
required: false, content: { 'application/json' => { schema: { type: 'object' } } }
)
end
end
context 'when required is supplied' do
before { subject.request_body_json(schema: { type: 'object' }, description: 'my description') }
it 'adds description' do
expect(api_metadata[:operation][:requestBody]).to match(
description: 'my description', required: true, content: { 'application/json' => { schema: { type: 'object' } } }
)
end
end
end
describe '#parameter(attributes)' do
context "when called at the 'path' level" do
before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) }
let(:api_metadata) { { path_item: {} } } # i.e. operation not defined yet
it "adds to the 'path_item parameters' metadata" do
expect(api_metadata[:path_item][:parameters]).to match(
[name: :blog, in: :body, schema: { type: 'object' }]
)
end
end
context "when called at the 'operation' level" do
before { subject.parameter(name: :blog, in: :body, schema: { type: 'object' }) }
let(:api_metadata) { { path_item: {}, operation: {} } } # i.e. operation defined
it "adds to the 'operation parameters' metadata" do
expect(api_metadata[:operation][:parameters]).to match(
[name: :blog, in: :body, schema: { type: 'object' }]
)
end
end
context "'path' parameter" do
before { subject.parameter(name: :id, in: :path) }
let(:api_metadata) { { operation: {} } }
it "automatically sets the 'required' flag" do
expect(api_metadata[:operation][:parameters]).to match(
[name: :id, in: :path, required: true]
)
end
end
context "when 'in' parameter key is not defined" do
before { subject.parameter(name: :id) }
let(:api_metadata) { { operation: {} } }
it "does not require the 'in' parameter key" do
expect(api_metadata[:operation][:parameters]).to match([name: :id])
end
end
end
describe '#response(code, description)' do
before { subject.response('201', 'success') }
it "delegates to 'context' with 'response' metadata" do
expect(subject).to have_received(:context).with(
'success', response: { code: '201', description: 'success' }
)
end
end
context "'path' parameter" do
before { subject.parameter(name: :id, in: :path) }
let(:api_metadata) { { operation: {} } }
describe '#schema(value)' do
before { subject.schema(type: 'object') }
let(:api_metadata) { { response: {} } }
it "automatically sets the 'required' flag" do
expect(api_metadata[:operation][:parameters]).to match(
[name: :id, in: :path, required: true]
)
it "adds to the 'response' metadata" do
expect(api_metadata[:response][:content]['application/json'][:schema]).to match(type: 'object')
end
end
context "when 'in' parameter key is not defined" do
before { subject.parameter(name: :id) }
let(:api_metadata) { { operation: {} } }
describe '#header(name, attributes)' do
before { subject.header('Date', type: 'string') }
let(:api_metadata) { { response: {} } }
it "does not require the 'in' parameter key" do
expect(api_metadata[:operation][:parameters]).to match([name: :id])
it "adds to the 'response headers' metadata" do
expect(api_metadata[:response][:headers]).to match(
'Date' => {schema: { type: 'string' }}
)
end
end
end
describe '#response(code, description)' do
before { subject.response('201', 'success') }
it "delegates to 'context' with 'response' metadata" do
expect(subject).to have_received(:context).with(
'success', response: { code: '201', description: 'success' }
)
end
end
describe '#schema(value)' do
before { subject.schema(type: 'object') }
let(:api_metadata) { { response: {} } }
it "adds to the 'response' metadata" do
expect(api_metadata[:response][:content]['application/json'][:schema]).to match(type: 'object')
end
end
describe '#header(name, attributes)' do
before { subject.header('Date', type: 'string') }
let(:api_metadata) { { response: {} } }
it "adds to the 'response headers' metadata" do
expect(api_metadata[:response][:headers]).to match(
'Date' => {schema: { type: 'string' }}
)
end
end
describe '#examples(example)' do
let(:json_example) do
{
'application/json' => {
foo: 'bar'
describe '#examples(example)' do
let(:json_example) do
{
'application/json' => {
foo: 'bar'
}
}
}
end
let(:api_metadata) { { response: {} } }
end
let(:api_metadata) { { response: {} } }
before do
subject.examples(json_example)
end
before do
subject.examples(json_example)
end
it "adds to the 'response examples' metadata" do
expect(api_metadata[:response][:examples]).to eq(json_example)
it "adds to the 'response examples' metadata" do
expect(api_metadata[:response][:examples]).to eq(json_example)
end
end
end
end

View File

@ -1,69 +1,71 @@
# frozen_string_literal: true
require 'rswag/specs/example_helpers'
require 'open_api/rswag/specs/example_helpers'
module Rswag
module Specs
describe ExampleHelpers do
subject { double('example') }
module OpenApi
module Rswag
module Specs
describe ExampleHelpers do
subject { double('example') }
before do
subject.extend(ExampleHelpers)
allow(Rswag::Specs).to receive(:config).and_return(config)
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
stub_const('Rswag::Specs::RAILS_VERSION', 3)
end
let(:config) { double('config') }
let(:swagger_doc) do
{
components: {
securitySchemes: {
api_key: {
type: :apiKey,
name: 'api_key',
in: :query
}
}
}
}
end
let(:metadata) do
{
path_item: { template: '/blogs/{blog_id}/comments/{id}' },
operation: {
verb: :put,
summary: 'Updates a blog',
consumes: ['application/json'],
parameters: [
{ name: :blog_id, in: :path, type: 'integer' },
{ name: 'id', in: :path, type: 'integer' },
{ name: 'q1', in: :query, type: 'string' },
{ name: :blog, in: :body, schema: { type: 'object' } }
],
security: [
{ api_key: [] }
]
}
}
end
describe '#submit_request(metadata)' do
before do
allow(subject).to receive(:blog_id).and_return(1)
allow(subject).to receive(:id).and_return(2)
allow(subject).to receive(:q1).and_return('foo')
allow(subject).to receive(:api_key).and_return('fookey')
allow(subject).to receive(:blog).and_return(text: 'Some comment')
allow(subject).to receive(:put)
subject.submit_request(metadata)
subject.extend(ExampleHelpers)
allow(OpenApi::Rswag::Specs).to receive(:config).and_return(config)
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
stub_const('Rswag::Specs::RAILS_VERSION', 3)
end
let(:config) { double('config') }
let(:swagger_doc) do
{
components: {
securitySchemes: {
api_key: {
type: :apiKey,
name: 'api_key',
in: :query
}
}
}
}
end
let(:metadata) do
{
path_item: { template: '/blogs/{blog_id}/comments/{id}' },
operation: {
verb: :put,
summary: 'Updates a blog',
consumes: ['application/json'],
parameters: [
{ name: :blog_id, in: :path, type: 'integer' },
{ name: 'id', in: :path, type: 'integer' },
{ name: 'q1', in: :query, type: 'string' },
{ name: :blog, in: :body, schema: { type: 'object' } }
],
security: [
{ api_key: [] }
]
}
}
end
it "submits a request built from metadata and 'let' values" do
expect(subject).to have_received(:put).with(
'/blogs/1/comments/2?q1=foo&api_key=fookey',
'{"text":"Some comment"}',
'CONTENT_TYPE' => 'application/json'
)
describe '#submit_request(metadata)' do
before do
allow(subject).to receive(:blog_id).and_return(1)
allow(subject).to receive(:id).and_return(2)
allow(subject).to receive(:q1).and_return('foo')
allow(subject).to receive(:api_key).and_return('fookey')
allow(subject).to receive(:blog).and_return(text: 'Some comment')
allow(subject).to receive(:put)
subject.submit_request(metadata)
end
it "submits a request built from metadata and 'let' values" do
expect(subject).to have_received(:put).with(
'/blogs/1/comments/2?q1=foo&api_key=fookey',
'{"text":"Some comment"}',
'CONTENT_TYPE' => 'application/json'
)
end
end
end
end

View File

@ -1,351 +1,353 @@
# frozen_string_literal: true
require 'rswag/specs/request_factory'
require 'open_api/rswag/specs/request_factory'
module Rswag
module Specs
describe RequestFactory do
subject { RequestFactory.new(config) }
module OpenApi
module Rswag
module Specs
describe RequestFactory do
subject { RequestFactory.new(config) }
before do
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
end
let(:config) { double('config') }
let(:swagger_doc) { {} }
let(:example) { double('example') }
let(:metadata) do
{
path_item: { template: '/blogs' },
operation: { verb: :get }
}
end
describe '#build_request(metadata, example)' do
let(:request) { subject.build_request(metadata, example) }
it 'builds request hash for given example' do
expect(request[:verb]).to eq(:get)
expect(request[:path]).to eq('/blogs')
before do
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
end
let(:config) { double('config') }
let(:swagger_doc) { {} }
let(:example) { double('example') }
let(:metadata) do
{
path_item: { template: '/blogs' },
operation: { verb: :get }
}
end
context "'path' parameters" do
before do
metadata[:path_item][:template] = '/blogs/{blog_id}/comments/{id}'
metadata[:operation][:parameters] = [
{ name: 'blog_id', in: :path, type: :number },
{ name: 'id', in: :path, type: :number }
]
allow(example).to receive(:blog_id).and_return(1)
allow(example).to receive(:id).and_return(2)
end
describe '#build_request(metadata, example)' do
let(:request) { subject.build_request(metadata, example) }
it 'builds the path from example values' do
expect(request[:path]).to eq('/blogs/1/comments/2')
end
end
context "'query' parameters" do
before do
metadata[:operation][:parameters] = [
{ name: 'q1', in: :query, type: :string },
{ name: 'q2', in: :query, type: :string }
]
allow(example).to receive(:q1).and_return('foo')
allow(example).to receive(:q2).and_return('bar')
end
it 'builds the query string from example values' do
expect(request[:path]).to eq('/blogs?q1=foo&q2=bar')
end
end
context "'query' parameters of type 'array'" do
before do
metadata[:operation][:parameters] = [
{ name: 'things', in: :query, type: :array, collectionFormat: collection_format }
]
allow(example).to receive(:things).and_return(%w[foo bar])
end
context 'collectionFormat = csv' do
let(:collection_format) { :csv }
it 'formats as comma separated values' do
expect(request[:path]).to eq('/blogs?things=foo,bar')
end
end
context 'collectionFormat = ssv' do
let(:collection_format) { :ssv }
it 'formats as space separated values' do
expect(request[:path]).to eq('/blogs?things=foo bar')
end
end
context 'collectionFormat = tsv' do
let(:collection_format) { :tsv }
it 'formats as tab separated values' do
expect(request[:path]).to eq('/blogs?things=foo\tbar')
end
end
context 'collectionFormat = pipes' do
let(:collection_format) { :pipes }
it 'formats as pipe separated values' do
expect(request[:path]).to eq('/blogs?things=foo|bar')
end
end
context 'collectionFormat = multi' do
let(:collection_format) { :multi }
it 'formats as multiple parameter instances' do
expect(request[:path]).to eq('/blogs?things=foo&things=bar')
end
end
end
context "'header' parameters" do
before do
metadata[:operation][:parameters] = [{ name: 'Api-Key', in: :header, type: :string }]
allow(example).to receive(:'Api-Key').and_return('foobar')
end
it 'adds names and example values to headers' do
expect(request[:headers]).to eq('Api-Key' => 'foobar')
end
end
context 'optional parameters not provided' do
before do
metadata[:operation][:parameters] = [
{ name: 'q1', in: :query, type: :string, required: false },
{ name: 'Api-Key', in: :header, type: :string, required: false }
]
end
it 'builds request hash without them' do
it 'builds request hash for given example' do
expect(request[:verb]).to eq(:get)
expect(request[:path]).to eq('/blogs')
expect(request[:headers]).to eq({})
end
end
context 'consumes content' do
before do
metadata[:operation][:consumes] = ['application/json', 'application/xml']
end
context "no 'Content-Type' provided" do
it "sets 'CONTENT_TYPE' header to first in list" do
expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/json')
end
end
context "explicit 'Content-Type' provided" do
context "'path' parameters" do
before do
allow(example).to receive(:'Content-Type').and_return('application/xml')
end
it "sets 'CONTENT_TYPE' header to example value" do
expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml')
end
end
context 'JSON payload' do
before do
metadata[:operation][:parameters] = [{ name: 'comment', in: :body, schema: { type: 'object' } }]
allow(example).to receive(:comment).and_return(text: 'Some comment')
end
it "serializes first 'body' parameter to JSON string" do
expect(request[:payload]).to eq('{"text":"Some comment"}')
end
end
context 'form payload' do
before do
metadata[:operation][:consumes] = ['multipart/form-data']
metadata[:path_item][:template] = '/blogs/{blog_id}/comments/{id}'
metadata[:operation][:parameters] = [
{ name: 'f1', in: :formData, type: :string },
{ name: 'f2', in: :formData, type: :string }
{ name: 'blog_id', in: :path, type: :number },
{ name: 'id', in: :path, type: :number }
]
allow(example).to receive(:f1).and_return('foo blah')
allow(example).to receive(:f2).and_return('bar blah')
allow(example).to receive(:blog_id).and_return(1)
allow(example).to receive(:id).and_return(2)
end
it 'sets payload to hash of names and example values' do
expect(request[:payload]).to eq(
'f1' => 'foo blah',
'f2' => 'bar blah'
)
end
end
end
context 'produces content' do
before do
metadata[:operation][:produces] = ['application/json', 'application/xml']
end
context "no 'Accept' value provided" do
it "sets 'HTTP_ACCEPT' header to first in list" do
expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/json')
it 'builds the path from example values' do
expect(request[:path]).to eq('/blogs/1/comments/2')
end
end
context "explicit 'Accept' value provided" do
context "'query' parameters" do
before do
allow(example).to receive(:Accept).and_return('application/xml')
metadata[:operation][:parameters] = [
{ name: 'q1', in: :query, type: :string },
{ name: 'q2', in: :query, type: :string }
]
allow(example).to receive(:q1).and_return('foo')
allow(example).to receive(:q2).and_return('bar')
end
it "sets 'HTTP_ACCEPT' header to example value" do
expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/xml')
it 'builds the query string from example values' do
expect(request[:path]).to eq('/blogs?q1=foo&q2=bar')
end
end
end
context 'basic auth' do
before do
swagger_doc[:components] = { securitySchemes: {
basic: { type: :basic }
context "'query' parameters of type 'array'" do
before do
metadata[:operation][:parameters] = [
{ name: 'things', in: :query, type: :array, collectionFormat: collection_format }
]
allow(example).to receive(:things).and_return(%w[foo bar])
end
context 'collectionFormat = csv' do
let(:collection_format) { :csv }
it 'formats as comma separated values' do
expect(request[:path]).to eq('/blogs?things=foo,bar')
end
end
context 'collectionFormat = ssv' do
let(:collection_format) { :ssv }
it 'formats as space separated values' do
expect(request[:path]).to eq('/blogs?things=foo bar')
end
end
context 'collectionFormat = tsv' do
let(:collection_format) { :tsv }
it 'formats as tab separated values' do
expect(request[:path]).to eq('/blogs?things=foo\tbar')
end
end
context 'collectionFormat = pipes' do
let(:collection_format) { :pipes }
it 'formats as pipe separated values' do
expect(request[:path]).to eq('/blogs?things=foo|bar')
end
end
context 'collectionFormat = multi' do
let(:collection_format) { :multi }
it 'formats as multiple parameter instances' do
expect(request[:path]).to eq('/blogs?things=foo&things=bar')
end
end
end
context "'header' parameters" do
before do
metadata[:operation][:parameters] = [{ name: 'Api-Key', in: :header, type: :string }]
allow(example).to receive(:'Api-Key').and_return('foobar')
end
it 'adds names and example values to headers' do
expect(request[:headers]).to eq('Api-Key' => 'foobar')
end
end
context 'optional parameters not provided' do
before do
metadata[:operation][:parameters] = [
{ name: 'q1', in: :query, type: :string, required: false },
{ name: 'Api-Key', in: :header, type: :string, required: false }
]
end
it 'builds request hash without them' do
expect(request[:path]).to eq('/blogs')
expect(request[:headers]).to eq({})
end
end
context 'consumes content' do
before do
metadata[:operation][:consumes] = ['application/json', 'application/xml']
end
context "no 'Content-Type' provided" do
it "sets 'CONTENT_TYPE' header to first in list" do
expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/json')
end
end
context "explicit 'Content-Type' provided" do
before do
allow(example).to receive(:'Content-Type').and_return('application/xml')
end
it "sets 'CONTENT_TYPE' header to example value" do
expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml')
end
end
context 'JSON payload' do
before do
metadata[:operation][:parameters] = [{ name: :comment, in: :body, schema: { type: 'object' } }]
allow(example).to receive(:comment).and_return(text: 'Some comment')
end
it "serializes first 'body' parameter to JSON string" do
expect(request[:payload]).to eq('{"text":"Some comment"}')
end
end
context 'form payload' do
before do
metadata[:operation][:consumes] = ['multipart/form-data']
metadata[:operation][:parameters] = [
{ name: 'f1', in: :formData, type: :string },
{ name: 'f2', in: :formData, type: :string }
]
allow(example).to receive(:f1).and_return('foo blah')
allow(example).to receive(:f2).and_return('bar blah')
end
it 'sets payload to hash of names and example values' do
expect(request[:payload]).to eq(
'f1' => 'foo blah',
'f2' => 'bar blah'
)
end
end
end
context 'produces content' do
before do
metadata[:operation][:produces] = ['application/json', 'application/xml']
end
context "no 'Accept' value provided" do
it "sets 'HTTP_ACCEPT' header to first in list" do
expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/json')
end
end
context "explicit 'Accept' value provided" do
before do
allow(example).to receive(:Accept).and_return('application/xml')
end
it "sets 'HTTP_ACCEPT' header to example value" do
expect(request[:headers]).to eq('HTTP_ACCEPT' => 'application/xml')
end
end
end
context 'basic auth' do
before do
swagger_doc[:components] = { securitySchemes: {
basic: { type: :basic }
}
}
metadata[:operation][:security] = [basic: []]
allow(example).to receive(:Authorization).and_return('Basic foobar')
end
it "sets 'HTTP_AUTHORIZATION' header to example value" do
expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar')
end
end
context 'apiKey' do
before do
swagger_doc[:components] = { securitySchemes: {
apiKey: { type: :apiKey, name: 'api_key', in: key_location }
}
}
metadata[:operation][:security] = [apiKey: []]
allow(example).to receive(:api_key).and_return('foobar')
metadata[:operation][:security] = [basic: []]
allow(example).to receive(:Authorization).and_return('Basic foobar')
end
it "sets 'HTTP_AUTHORIZATION' header to example value" do
expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar')
end
end
context 'in query' do
let(:key_location) { :query }
context 'apiKey' do
before do
swagger_doc[:components] = { securitySchemes: {
apiKey: { type: :apiKey, name: 'api_key', in: key_location }
}
}
metadata[:operation][:security] = [apiKey: []]
allow(example).to receive(:api_key).and_return('foobar')
end
it 'adds name and example value to the query string' do
context 'in query' do
let(:key_location) { :query }
it 'adds name and example value to the query string' do
expect(request[:path]).to eq('/blogs?api_key=foobar')
end
end
context 'in header' do
let(:key_location) { :header }
it 'adds name and example value to the headers' do
expect(request[:headers]).to eq('api_key' => 'foobar')
end
end
context 'in header with auth param already added' do
let(:key_location) { :header }
before do
metadata[:operation][:parameters] = [
{ name: 'q1', in: :query, type: :string },
{ name: 'api_key', in: :header, type: :string }
]
allow(example).to receive(:q1).and_return('foo')
allow(example).to receive(:api_key).and_return('foobar')
end
it 'adds authorization parameter only once' do
expect(request[:headers]).to eq('api_key' => 'foobar')
expect(metadata[:operation][:parameters].size).to eq 2
end
end
end
context 'oauth2' do
before do
swagger_doc[:components] = { securitySchemes: {
oauth2: { type: :oauth2, scopes: ['read:blogs'] }
}
}
metadata[:operation][:security] = [oauth2: ['read:blogs']]
allow(example).to receive(:Authorization).and_return('Bearer foobar')
end
it "sets 'HTTP_AUTHORIZATION' header to example value" do
expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Bearer foobar')
end
end
context 'paired security requirements' do
before do
swagger_doc[:components] = { securitySchemes: {
basic: { type: :basic },
api_key: { type: :apiKey, name: 'api_key', in: :query }
}
}
metadata[:operation][:security] = [{ basic: [], api_key: [] }]
allow(example).to receive(:Authorization).and_return('Basic foobar')
allow(example).to receive(:api_key).and_return('foobar')
end
it 'sets both params to example values' do
expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar')
expect(request[:path]).to eq('/blogs?api_key=foobar')
end
end
context 'in header' do
let(:key_location) { :header }
context 'path-level parameters' do
before do
metadata[:operation][:parameters] = [{ name: 'q1', in: :query, type: :string }]
metadata[:path_item][:parameters] = [{ name: 'q2', in: :query, type: :string }]
allow(example).to receive(:q1).and_return('foo')
allow(example).to receive(:q2).and_return('bar')
end
it 'adds name and example value to the headers' do
expect(request[:headers]).to eq('api_key' => 'foobar')
it 'populates operation and path level parameters ' do
expect(request[:path]).to eq('/blogs?q1=foo&q2=bar')
end
end
context 'in header with auth param already added' do
let(:key_location) { :header }
context 'referenced parameters' do
before do
metadata[:operation][:parameters] = [
{ name: 'q1', in: :query, type: :string },
{ name: 'api_key', in: :header, type: :string }
]
swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } }
metadata[:operation][:parameters] = [{ '$ref' => '#/parameters/q1' }]
allow(example).to receive(:q1).and_return('foo')
end
it 'uses the referenced metadata to build the request' do
expect(request[:path]).to eq('/blogs?q1=foo')
end
end
context 'global basePath' do
before { swagger_doc[:basePath] = '/api' }
it 'prepends to the path' do
expect(request[:path]).to eq('/api/blogs')
end
end
context 'global consumes' do
before { swagger_doc[:consumes] = ['application/xml'] }
it "defaults 'CONTENT_TYPE' to global value(s)" do
expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml')
end
end
context 'global security requirements' do
before do
swagger_doc[:components] = {securitySchemes: { apiKey: { type: :apiKey, name: 'api_key', in: :query } }}
swagger_doc[:security] = [apiKey: []]
allow(example).to receive(:api_key).and_return('foobar')
end
it 'adds authorization parameter only once' do
expect(request[:headers]).to eq('api_key' => 'foobar')
expect(metadata[:operation][:parameters].size).to eq 2
it 'applieds the scheme by default' do
expect(request[:path]).to eq('/blogs?api_key=foobar')
end
end
end
context 'oauth2' do
before do
swagger_doc[:components] = { securitySchemes: {
oauth2: { type: :oauth2, scopes: ['read:blogs'] }
}
}
metadata[:operation][:security] = [oauth2: ['read:blogs']]
allow(example).to receive(:Authorization).and_return('Bearer foobar')
end
it "sets 'HTTP_AUTHORIZATION' header to example value" do
expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Bearer foobar')
end
end
context 'paired security requirements' do
before do
swagger_doc[:components] = { securitySchemes: {
basic: { type: :basic },
api_key: { type: :apiKey, name: 'api_key', in: :query }
}
}
metadata[:operation][:security] = [{ basic: [], api_key: [] }]
allow(example).to receive(:Authorization).and_return('Basic foobar')
allow(example).to receive(:api_key).and_return('foobar')
end
it 'sets both params to example values' do
expect(request[:headers]).to eq('HTTP_AUTHORIZATION' => 'Basic foobar')
expect(request[:path]).to eq('/blogs?api_key=foobar')
end
end
context 'path-level parameters' do
before do
metadata[:operation][:parameters] = [{ name: 'q1', in: :query, type: :string }]
metadata[:path_item][:parameters] = [{ name: 'q2', in: :query, type: :string }]
allow(example).to receive(:q1).and_return('foo')
allow(example).to receive(:q2).and_return('bar')
end
it 'populates operation and path level parameters ' do
expect(request[:path]).to eq('/blogs?q1=foo&q2=bar')
end
end
context 'referenced parameters' do
before do
swagger_doc[:parameters] = { q1: { name: 'q1', in: :query, type: :string } }
metadata[:operation][:parameters] = [{ '$ref' => '#/parameters/q1' }]
allow(example).to receive(:q1).and_return('foo')
end
it 'uses the referenced metadata to build the request' do
expect(request[:path]).to eq('/blogs?q1=foo')
end
end
context 'global basePath' do
before { swagger_doc[:basePath] = '/api' }
it 'prepends to the path' do
expect(request[:path]).to eq('/api/blogs')
end
end
context 'global consumes' do
before { swagger_doc[:consumes] = ['application/xml'] }
it "defaults 'CONTENT_TYPE' to global value(s)" do
expect(request[:headers]).to eq('CONTENT_TYPE' => 'application/xml')
end
end
context 'global security requirements' do
before do
swagger_doc[:components] = {securitySchemes: { apiKey: { type: :apiKey, name: 'api_key', in: :query } }}
swagger_doc[:security] = [apiKey: []]
allow(example).to receive(:api_key).and_return('foobar')
end
it 'applieds the scheme by default' do
expect(request[:path]).to eq('/blogs?api_key=foobar')
end
end
end
end
end

View File

@ -1,76 +1,82 @@
# frozen_string_literal: true
require 'rswag/specs/response_validator'
require 'open_api/rswag/specs/response_validator'
module Rswag
module Specs
describe ResponseValidator do
subject { ResponseValidator.new(config) }
module OpenApi
module Rswag
module Specs
describe ResponseValidator do
subject { ResponseValidator.new(config) }
before do
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
end
let(:config) { double('config') }
let(:swagger_doc) {{}}
let(:example) { double('example') }
let(:metadata) do
{
response: {
code: 200,
headers: { 'X-Rate-Limit-Limit' => { type: :integer } },
schema: {
type: :object,
properties: { text: { type: :string } },
required: ['text']
}
before do
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
end
let(:config) { double('config') }
let(:swagger_doc) {{}}
let(:example) { double('example') }
let(:metadata) do
{
response: {
code: 200,
headers: { 'X-Rate-Limit-Limit' => { type: :integer } },
content:
{'application/json' => {
schema: {
type: :object,
properties: { text: { type: :string } },
required: ['text']
}
}
}
}
}
}
end
describe '#validate!(metadata, response)' do
let(:call) { subject.validate!(metadata, response) }
let(:response) do
OpenStruct.new(
code: '200',
headers: { 'X-Rate-Limit-Limit' => '10' },
body: '{"text":"Some comment"}'
)
end
context 'response matches metadata' do
it { expect { call }.to_not raise_error }
end
context 'response code differs from metadata' do
before { response.code = '400' }
it { expect { call }.to raise_error /Expected response code/ }
end
context 'response headers differ from metadata' do
before { response.headers = {} }
it { expect { call }.to raise_error /Expected response header/ }
end
context 'response body differs from metadata' do
before { response.body = '{"foo":"Some comment"}' }
it { expect { call }.to raise_error /Expected response body/ }
end
context 'referenced schemas' do
before do
swagger_doc[:components] = {}
swagger_doc[:components][:schemas] = {
'blog' => {
type: :object,
properties: { foo: { type: :string } },
required: ['foo']
}
}
metadata[:response][:schema] = { '$ref' => '#/components/schemas/blog' }
describe '#validate!(metadata, response)' do
let(:call) { subject.validate!(metadata, response) }
let(:response) do
OpenStruct.new(
code: '200',
headers: { 'X-Rate-Limit-Limit' => '10' },
body: '{"text":"Some comment"}'
)
end
it 'uses the referenced schema to validate the response body' do
expect { call }.to raise_error /Expected response body/
context 'response matches metadata' do
it { expect { call }.to_not raise_error }
end
context 'response code differs from metadata' do
before { response.code = '400' }
it { expect { call }.to raise_error /Expected response code/ }
end
context 'response headers differ from metadata' do
before { response.headers = {} }
it { expect { call }.to raise_error /Expected response header/ }
end
context 'response body differs from metadata' do
before { response.body = '{"foo":"Some comment"}' }
it { expect { call }.to raise_error /Expected response body/ }
end
context 'referenced schemas' do
before do
swagger_doc[:components] = {}
swagger_doc[:components][:schemas] = {
'blog' => {
type: :object,
properties: { foo: { type: :string } },
required: ['foo']
}
}
metadata[:response][:content]['application/json'][:schema] = { '$ref' => '#/components/schemas/blog' }
end
it 'uses the referenced schema to validate the response body' do
expect { call }.to raise_error /Expected response body/
end
end
end
end

View File

@ -1,71 +1,73 @@
# frozen_string_literal: true
require 'rswag/specs/swagger_formatter'
require 'open_api/rswag/specs/swagger_formatter'
require 'ostruct'
module Rswag
module Specs
describe SwaggerFormatter do
subject { described_class.new(output, config) }
module OpenApi
module Rswag
module Specs
describe SwaggerFormatter do
subject { described_class.new(output, config) }
# Mock out some infrastructure
before do
allow(config).to receive(:swagger_root).and_return(swagger_root)
end
let(:config) { double('config') }
let(:output) { double('output').as_null_object }
let(:swagger_root) { File.expand_path('tmp/swagger', __dir__) }
describe '#example_group_finished(notification)' do
# Mock out some infrastructure
before do
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
subject.example_group_finished(notification)
end
let(:swagger_doc) { {} }
let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) }
let(:api_metadata) do
{
path_item: { template: '/blogs' },
operation: { verb: :post, summary: 'Creates a blog' },
response: { code: '201', description: 'blog created' }
}
allow(config).to receive(:swagger_root).and_return(swagger_root)
end
let(:config) { double('config') }
let(:output) { double('output').as_null_object }
let(:swagger_root) { File.expand_path('tmp/swagger', __dir__) }
it 'converts to swagger and merges into the corresponding swagger doc' do
expect(swagger_doc).to match(
paths: {
'/blogs' => {
post: {
summary: 'Creates a blog',
responses: {
'201' => { description: 'blog created' }
}
}
}
describe '#example_group_finished(notification)' do
before do
allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
subject.example_group_finished(notification)
end
let(:swagger_doc) { {} }
let(:notification) { OpenStruct.new(group: OpenStruct.new(metadata: api_metadata)) }
let(:api_metadata) do
{
path_item: { template: '/blogs' },
operation: { verb: :post, summary: 'Creates a blog' },
response: { code: '201', description: 'blog created' }
}
)
end
end
end
describe '#stop' do
before do
FileUtils.rm_r(swagger_root) if File.exist?(swagger_root)
allow(config).to receive(:swagger_docs).and_return(
'v1/swagger.json' => { info: { version: 'v1' } },
'v2/swagger.json' => { info: { version: 'v2' } }
)
subject.stop(notification)
it 'converts to swagger and merges into the corresponding swagger doc' do
expect(swagger_doc).to match(
paths: {
'/blogs' => {
post: {
summary: 'Creates a blog',
responses: {
'201' => { description: 'blog created' }
}
}
}
}
)
end
end
let(:notification) { double('notification') }
describe '#stop' do
before do
FileUtils.rm_r(swagger_root) if File.exist?(swagger_root)
allow(config).to receive(:swagger_docs).and_return(
'v1/swagger.json' => { info: { version: 'v1' } },
'v2/swagger.json' => { info: { version: 'v2' } }
)
subject.stop(notification)
end
it 'writes the swagger_doc(s) to file' do
expect(File).to exist("#{swagger_root}/v1/swagger.json")
expect(File).to exist("#{swagger_root}/v2/swagger.json")
end
let(:notification) { double('notification') }
after do
FileUtils.rm_r(swagger_root) if File.exist?(swagger_root)
it 'writes the swagger_doc(s) to file' do
expect(File).to exist("#{swagger_root}/v1/swagger.json")
expect(File).to exist("#{swagger_root}/v2/swagger.json")
end
after do
FileUtils.rm_r(swagger_root) if File.exist?(swagger_root)
end
end
end
end

View File

@ -4,4 +4,4 @@ module Rails
end
end
require 'rswag/specs'
require 'open_api/rswag/specs'