mirror of
https://github.com/ditkrg/rswag.git
synced 2026-01-25 07:16:40 +00:00
Merge branch 'master' into feature/request_spec_generator
This commit is contained in:
@@ -3,11 +3,11 @@ require 'rails_helper'
|
||||
RSpec.configure do |config|
|
||||
# Specify a root folder where Swagger JSON files are generated
|
||||
# NOTE: If you're using the rswag-api to serve API descriptions, you'll need
|
||||
# to ensure that it's confiugred to server Swagger from the same folder
|
||||
config.swagger_root = Rails.root.to_s + '/swagger'
|
||||
# to ensure that it's configured to serve Swagger from the same folder
|
||||
config.swagger_root = Rails.root.join('swagger').to_s
|
||||
|
||||
# Define one or more Swagger documents and provide global metadata for each one
|
||||
# When you run the 'rswag:specs:to_swagger' rake task, the complete Swagger will
|
||||
# When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will
|
||||
# be generated at the provided relative path under swagger_root
|
||||
# By default, the operations defined in spec files are added to the first
|
||||
# document below. You can override this behavior by adding a swagger_doc tag to the
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'rspec/core'
|
||||
require 'rswag/specs/version'
|
||||
require 'rswag/specs/example_group_helpers'
|
||||
require 'rswag/specs/example_helpers'
|
||||
require 'rswag/specs/configuration'
|
||||
|
||||
@@ -2,12 +2,12 @@ module Rswag
|
||||
module Specs
|
||||
module ExampleGroupHelpers
|
||||
|
||||
def path(template, &block)
|
||||
api_metadata = { path_item: { template: template } }
|
||||
describe(template, api_metadata, &block)
|
||||
def path(template, metadata={}, &block)
|
||||
metadata[:path_item] = { template: template }
|
||||
describe(template, metadata, &block)
|
||||
end
|
||||
|
||||
[ :get, :post, :patch, :put, :delete, :head ].each do |verb|
|
||||
[ :get, :post, :patch, :put, :delete, :head, :options, :trace ].each do |verb|
|
||||
define_method(verb) do |summary, &block|
|
||||
api_metadata = { operation: { verb: verb, summary: summary } }
|
||||
describe(verb, api_metadata, &block)
|
||||
@@ -36,7 +36,9 @@ module Rswag
|
||||
end
|
||||
|
||||
def parameter(attributes)
|
||||
attributes[:required] = true if attributes[:in].to_sym == :path
|
||||
if attributes[:in] && attributes[:in].to_sym == :path
|
||||
attributes[:required] = true
|
||||
end
|
||||
|
||||
if metadata.has_key?(:operation)
|
||||
metadata[:operation][:parameters] ||= []
|
||||
@@ -47,9 +49,9 @@ module Rswag
|
||||
end
|
||||
end
|
||||
|
||||
def response(code, description, &block)
|
||||
api_metadata = { response: { code: code, description: description } }
|
||||
context(description, api_metadata, &block)
|
||||
def response(code, description, metadata={}, &block)
|
||||
metadata[:response] = { code: code, description: description }
|
||||
context(description, metadata, &block)
|
||||
end
|
||||
|
||||
def schema(value)
|
||||
@@ -77,7 +79,8 @@ module Rswag
|
||||
end
|
||||
|
||||
it "returns a #{metadata[:response][:code]} response" do
|
||||
assert_response_matches_metadata(example.metadata, &block)
|
||||
assert_response_matches_metadata(metadata)
|
||||
block.call(response) if block_given?
|
||||
end
|
||||
else
|
||||
before do |example|
|
||||
@@ -86,6 +89,7 @@ module Rswag
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,55 +5,30 @@ module Rswag
|
||||
module Specs
|
||||
module ExampleHelpers
|
||||
|
||||
def submit_request(api_metadata)
|
||||
global_metadata = rswag_config.get_swagger_doc(api_metadata[:swagger_doc])
|
||||
factory = RequestFactory.new(api_metadata, global_metadata)
|
||||
def submit_request(metadata)
|
||||
request = RequestFactory.new.build_request(metadata, self)
|
||||
|
||||
if RAILS_VERSION < 5
|
||||
send(
|
||||
api_metadata[:operation][:verb],
|
||||
factory.build_fullpath(self),
|
||||
factory.build_body(self),
|
||||
rackify_headers(factory.build_headers(self)) # Rails test infrastructure requires Rack headers
|
||||
request[:verb],
|
||||
request[:path],
|
||||
request[:payload],
|
||||
request[:headers]
|
||||
)
|
||||
else
|
||||
send(
|
||||
api_metadata[:operation][:verb],
|
||||
factory.build_fullpath(self),
|
||||
request[:verb],
|
||||
request[:path],
|
||||
{
|
||||
params: factory.build_body(self),
|
||||
headers: factory.build_headers(self)
|
||||
params: request[:payload],
|
||||
headers: request[:headers]
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def assert_response_matches_metadata(api_metadata, &block)
|
||||
global_metadata = rswag_config.get_swagger_doc(api_metadata[:swagger_doc])
|
||||
validator = ResponseValidator.new(api_metadata, global_metadata)
|
||||
validator.validate!(response, &block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rackify_headers(headers)
|
||||
name_value_pairs = headers.map do |name, value|
|
||||
[
|
||||
case name
|
||||
when 'Accept' then 'HTTP_ACCEPT'
|
||||
when 'Content-Type' then 'CONTENT_TYPE'
|
||||
when 'Authorization' then 'HTTP_AUTHORIZATION'
|
||||
else name
|
||||
end,
|
||||
value
|
||||
]
|
||||
end
|
||||
|
||||
Hash[ name_value_pairs ]
|
||||
end
|
||||
|
||||
def rswag_config
|
||||
::Rswag::Specs.config
|
||||
def assert_response_matches_metadata(metadata)
|
||||
ResponseValidator.new.validate!(metadata, response)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,98 +1,82 @@
|
||||
require 'active_support/core_ext/hash/slice'
|
||||
require 'active_support/core_ext/hash/conversions'
|
||||
require 'json'
|
||||
|
||||
module Rswag
|
||||
module Specs
|
||||
class RequestFactory
|
||||
|
||||
def initialize(api_metadata, global_metadata)
|
||||
@api_metadata = api_metadata
|
||||
@global_metadata = global_metadata
|
||||
def initialize(config = ::Rswag::Specs.config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def build_fullpath(example)
|
||||
@api_metadata[:path_item][:template].dup.tap do |t|
|
||||
t.prepend(@global_metadata[:basePath] || '')
|
||||
parameters_in(:path).each { |p| t.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) }
|
||||
t.concat(build_query_string(example))
|
||||
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
|
||||
|
||||
def build_query_string(example)
|
||||
query_string = parameters_in(:query)
|
||||
.map { |p| build_query_string_part(p, example.send(p[:name])) }
|
||||
.join('&')
|
||||
|
||||
query_string.empty? ? '' : "?#{query_string}"
|
||||
end
|
||||
|
||||
def build_body(example)
|
||||
body_parameter = parameters_in(:body).first
|
||||
body_parameter.nil? ? '' : example.send(body_parameter[:name]).to_json
|
||||
end
|
||||
|
||||
def build_headers(example)
|
||||
name_value_pairs = parameters_in(:header).map do |param|
|
||||
[
|
||||
param[:name],
|
||||
example.send(param[:name]).to_s
|
||||
]
|
||||
end
|
||||
|
||||
# Add MIME type headers based on produces/consumes metadata
|
||||
produces = @api_metadata[:operation][:produces] || @global_metadata[:produces]
|
||||
consumes = @api_metadata[:operation][:consumes] || @global_metadata[:consumes]
|
||||
name_value_pairs << [ 'Accept', produces.join(';') ] unless produces.nil?
|
||||
name_value_pairs << [ 'Content-Type', consumes.join(';') ] unless consumes.nil?
|
||||
|
||||
Hash[ name_value_pairs ]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parameters_in(location)
|
||||
path_item_params = @api_metadata[:path_item][:parameters] || []
|
||||
operation_params = @api_metadata[:operation][:parameters] || []
|
||||
applicable_params = operation_params
|
||||
.concat(path_item_params)
|
||||
.uniq { |p| p[:name] } # operation params should override path_item params
|
||||
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)
|
||||
|
||||
applicable_params
|
||||
.map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references
|
||||
.concat(security_parameters)
|
||||
.select { |p| p[:in] == location }
|
||||
# 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 resolve_parameter(ref)
|
||||
defined_params = @global_metadata[:parameters]
|
||||
key = ref.sub('#/parameters/', '')
|
||||
raise "Referenced parameter '#{ref}' must be defined" unless defined_params && defined_params[key]
|
||||
defined_params[key]
|
||||
def derive_security_params(metadata, swagger_doc)
|
||||
requirements = metadata[:operation][:security] || swagger_doc[:security] || []
|
||||
scheme_names = requirements.flat_map { |r| r.keys }
|
||||
schemes = (swagger_doc[:securityDefinitions] || {}).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 security_parameters
|
||||
applicable_security_schemes.map do |scheme|
|
||||
if scheme[:type] == :apiKey
|
||||
{ name: scheme[:name], type: :string, in: scheme[:in] }
|
||||
else
|
||||
{ name: 'Authorization', type: :string, in: :header } # use auth header for basic & oauth2
|
||||
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 applicable_security_schemes
|
||||
# First figure out the security requirement applicable to the operation
|
||||
requirements = @api_metadata[:operation][:security] || @global_metadata[:security]
|
||||
scheme_names = requirements ? requirements.map { |r| r.keys.first } : []
|
||||
|
||||
# Then obtain the scheme definitions for those requirements
|
||||
(@global_metadata[:securityDefinitions] || {}).slice(*scheme_names).values
|
||||
end
|
||||
|
||||
def build_query_string_part(param, value)
|
||||
return "#{param[:name]}=#{value.to_s}" unless param[:type].to_sym == :array
|
||||
|
||||
name = param[:name]
|
||||
return "#{name}=#{value.to_s}" unless param[:type].to_sym == :array
|
||||
|
||||
case param[:collectionFormat]
|
||||
when :ssv
|
||||
"#{name}=#{value.join(' ')}"
|
||||
@@ -106,6 +90,68 @@ module Rswag
|
||||
"#{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 }.first
|
||||
body_param ? example.send(body_param[:name]).to_json : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,47 +7,45 @@ module Rswag
|
||||
module Specs
|
||||
class ResponseValidator
|
||||
|
||||
def initialize(api_metadata, global_metadata)
|
||||
@api_metadata = api_metadata
|
||||
@global_metadata = global_metadata
|
||||
def initialize(config = ::Rswag::Specs.config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def validate!(response, &block)
|
||||
validate_code!(response.code)
|
||||
validate_headers!(response.headers)
|
||||
validate_body!(response.body, &block)
|
||||
block.call(response) if block_given?
|
||||
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!(code)
|
||||
if code.to_s != @api_metadata[:response][:code].to_s
|
||||
raise UnexpectedResponse, "Expected response code '#{code}' to match '#{@api_metadata[:response][:code]}'"
|
||||
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!(headers)
|
||||
header_schema = @api_metadata[:response][:headers]
|
||||
return if header_schema.nil?
|
||||
|
||||
header_schema.keys.each do |header_name|
|
||||
raise UnexpectedResponse, "Expected response header #{header_name} to be present" if headers[header_name.to_s].nil?
|
||||
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!(body)
|
||||
response_schema = @api_metadata[:response][:schema]
|
||||
def validate_body!(metadata, swagger_doc, body)
|
||||
response_schema = metadata[:response][:schema]
|
||||
return if response_schema.nil?
|
||||
|
||||
begin
|
||||
validation_schema = response_schema
|
||||
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
|
||||
.merge(@global_metadata.slice(:definitions))
|
||||
JSON::Validator.validate!(validation_schema, body)
|
||||
rescue JSON::Schema::ValidationError => ex
|
||||
raise UnexpectedResponse, "Expected response body to match schema: #{ex.message}"
|
||||
end
|
||||
validation_schema = response_schema
|
||||
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
|
||||
.merge(swagger_doc.slice(:definitions))
|
||||
errors = JSON::Validator.fully_validate(validation_schema, body)
|
||||
raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module Rswag
|
||||
module Specs
|
||||
VERSION = '1.3.0'
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user