17 Commits
1.4.0 ... 1.5.1

Author SHA1 Message Date
domaindrivendev
d91601b02c Merge pull request #87 from BC-THooper/allow_for_undefined_in_parameter_key
Allows for parameters to be defined without the 'in' key defined.
2017-07-31 17:54:43 -07:00
Travis Hooper
037c0e374a Allows for parameters to be defined without the 'in' key defined to allow for parameter 2017-07-31 17:44:43 -05:00
domaindrivendev
8f16492462 Merge pull request #82 from domaindrivendev/per-response-metadata
Allow arbitrary metadata for path/response blocks
2017-07-22 10:33:05 -07:00
domaindrivendev
732cab994c simplify validation blocks and use correct scope 2017-07-21 22:26:14 -07:00
domaindrivendev
452d9176cc Allow arbitrary metadata for path/response blocks 2017-07-21 21:19:25 -07:00
domaindrivendev
7f0e437f8b For rswag, param names MUST be unique 2017-07-21 21:04:46 -07:00
domaindrivendev
700b227231 Merge branch 'thg303-add-formData-support' 2017-07-21 14:25:51 -07:00
domaindrivendev
b16198377b Merge branch 'add-formData-support' of https://github.com/thg303/rswag into thg303-add-formData-support 2017-07-21 14:25:29 -07:00
domaindrivendev
e18a001e9b Move rackify headers to RequestFactory + other minor refactors 2017-07-21 13:49:01 -07:00
domaindrivendev
6ee53b08ae Merge branch 'refactor-request-factory' 2017-07-20 23:42:49 -07:00
domaindrivendev
97c2a39cfa Refactor request_factory & response_validator 2017-07-20 23:42:40 -07:00
domaindrivendev
f7c29267fe Merge pull request #78 from bertha-carl/fully-validate
Can we use JSON::Validator.fully_validate() to get better error messages?
2017-07-20 10:09:01 -07:00
Hannes Probst
8ede7f78f1 use fully_validate to get more specific error msg 2017-07-18 15:45:36 +02:00
domaindrivendev
8b937e3585 Merge pull request #76 from ahobson/bugfix-optional-query-params
Support optional query parameters
2017-07-13 16:35:48 -07:00
Andrew Hobson
1515ce4fcb Support optional query parameters 2017-07-11 16:28:45 -04:00
Ali Qanavatian
44840ab836 Merge branch 'master' into add-formData-support 2017-07-04 09:45:33 +04:30
ali.q
182ee093f4 add formData support 2017-05-11 05:06:46 +04:30
21 changed files with 600 additions and 466 deletions

View File

@@ -82,6 +82,11 @@ Once you have an API that can describe itself in Swagger, you've opened the trea
let(:id) { 'invalid' } let(:id) { 'invalid' }
run_test! run_test!
end end
response '406', 'unsupported accept header' do
let(:'Accept') { 'application/foo' }
run_test!
end
end end
end end
end end

View File

@@ -2,9 +2,9 @@ module Rswag
module Specs module Specs
module ExampleGroupHelpers module ExampleGroupHelpers
def path(template, &block) def path(template, metadata={}, &block)
api_metadata = { path_item: { template: template } } metadata[:path_item] = { template: template }
describe(template, api_metadata, &block) describe(template, metadata, &block)
end end
[ :get, :post, :patch, :put, :delete, :head ].each do |verb| [ :get, :post, :patch, :put, :delete, :head ].each do |verb|
@@ -36,7 +36,9 @@ module Rswag
end end
def parameter(attributes) 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) if metadata.has_key?(:operation)
metadata[:operation][:parameters] ||= [] metadata[:operation][:parameters] ||= []
@@ -47,9 +49,9 @@ module Rswag
end end
end end
def response(code, description, &block) def response(code, description, metadata={}, &block)
api_metadata = { response: { code: code, description: description } } metadata[:response] = { code: code, description: description }
context(description, api_metadata, &block) context(description, metadata, &block)
end end
def schema(value) def schema(value)
@@ -77,7 +79,8 @@ module Rswag
end end
it "returns a #{metadata[:response][:code]} response" do 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 end
else else
before do |example| before do |example|
@@ -86,6 +89,7 @@ module Rswag
it "returns a #{metadata[:response][:code]} response" do |example| it "returns a #{metadata[:response][:code]} response" do |example|
assert_response_matches_metadata(example.metadata, &block) assert_response_matches_metadata(example.metadata, &block)
example.instance_exec(response, &block) if block_given?
end end
end end
end end

View File

@@ -5,55 +5,30 @@ module Rswag
module Specs module Specs
module ExampleHelpers module ExampleHelpers
def submit_request(api_metadata) def submit_request(metadata)
global_metadata = rswag_config.get_swagger_doc(api_metadata[:swagger_doc]) request = RequestFactory.new.build_request(metadata, self)
factory = RequestFactory.new(api_metadata, global_metadata)
if RAILS_VERSION < 5 if RAILS_VERSION < 5
send( send(
api_metadata[:operation][:verb], request[:verb],
factory.build_fullpath(self), request[:path],
factory.build_body(self), request[:payload],
rackify_headers(factory.build_headers(self)) # Rails test infrastructure requires Rack headers request[:headers]
) )
else else
send( send(
api_metadata[:operation][:verb], request[:verb],
factory.build_fullpath(self), request[:path],
{ {
params: factory.build_body(self), params: request[:payload],
headers: factory.build_headers(self) headers: request[:headers]
} }
) )
end end
end end
def assert_response_matches_metadata(api_metadata, &block) def assert_response_matches_metadata(metadata)
global_metadata = rswag_config.get_swagger_doc(api_metadata[:swagger_doc]) ResponseValidator.new.validate!(metadata, response)
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
end end
end end
end end

View File

@@ -1,98 +1,83 @@
require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/conversions'
require 'json' require 'json'
module Rswag module Rswag
module Specs module Specs
class RequestFactory class RequestFactory
def initialize(api_metadata, global_metadata) def initialize(config = ::Rswag::Specs.config)
@api_metadata = api_metadata @config = config
@global_metadata = global_metadata
end end
def build_fullpath(example) def build_request(metadata, example)
@api_metadata[:path_item][:template].dup.tap do |t| swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
t.prepend(@global_metadata[:basePath] || '') parameters = expand_parameters(metadata, swagger_doc, example)
parameters_in(:path).each { |p| t.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s) }
t.concat(build_query_string(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
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 private
def parameters_in(location) def expand_parameters(metadata, swagger_doc, example)
path_item_params = @api_metadata[:path_item][:parameters] || [] operation_params = metadata[:operation][:parameters] || []
operation_params = @api_metadata[:operation][:parameters] || [] path_item_params = metadata[:path_item][:parameters] || []
applicable_params = operation_params security_params = derive_security_params(metadata, swagger_doc)
operation_params
.concat(path_item_params) .concat(path_item_params)
.uniq { |p| p[:name] } # operation params should override path_item params .concat(security_params)
.map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p }
applicable_params .uniq { |p| p[:name] }
.map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
.concat(security_parameters)
.select { |p| p[:in] == location }
end end
def resolve_parameter(ref) def derive_security_params(metadata, swagger_doc)
defined_params = @global_metadata[:parameters] requirements = metadata[:operation][:security] || swagger_doc[:security]
scheme_names = requirements ? requirements.map { |r| r.keys.first } : []
applicable_schemes = (swagger_doc[:securityDefinitions] || {}).slice(*scheme_names).values
applicable_schemes.map do |scheme|
param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: 'Authorization', in: :header }
param.merge(type: :string)
end
end
def resolve_parameter(ref, swagger_doc)
definitions = swagger_doc[:parameters]
key = ref.sub('#/parameters/', '') key = ref.sub('#/parameters/', '')
raise "Referenced parameter '#{ref}' must be defined" unless defined_params && defined_params[key] raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
defined_params[key] definitions[key]
end end
def security_parameters def add_verb(request, metadata)
applicable_security_schemes.map do |scheme| request[:verb] = metadata[:operation][:verb]
if scheme[:type] == :apiKey end
{ name: scheme[:name], type: :string, in: scheme[:in] }
else def add_path(request, metadata, swagger_doc, parameters, example)
{ name: 'Authorization', type: :string, in: :header } # use auth header for basic & oauth2 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 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) def build_query_string_part(param, value)
return "#{param[:name]}=#{value.to_s}" unless param[:type].to_sym == :array
name = param[:name] name = param[:name]
return "#{name}=#{value.to_s}" unless param[:type].to_sym == :array
case param[:collectionFormat] case param[:collectionFormat]
when :ssv when :ssv
"#{name}=#{value.join(' ')}" "#{name}=#{value.join(' ')}"
@@ -106,6 +91,68 @@ module Rswag
"#{name}=#{value.join(',')}" # csv is default "#{name}=#{value.join(',')}" # csv is default
end end
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 end
end end

View File

@@ -7,47 +7,43 @@ module Rswag
module Specs module Specs
class ResponseValidator class ResponseValidator
def initialize(api_metadata, global_metadata) def initialize(config = ::Rswag::Specs.config)
@api_metadata = api_metadata @config = config
@global_metadata = global_metadata
end end
def validate!(response, &block) def validate!(metadata, response)
validate_code!(response.code) swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
validate_headers!(response.headers)
validate_body!(response.body, &block) validate_code!(metadata, response.code)
block.call(response) if block_given? validate_headers!(metadata, response.headers)
validate_body!(metadata, swagger_doc, response.body)
end end
private private
def validate_code!(code) def validate_code!(metadata, code)
if code.to_s != @api_metadata[:response][:code].to_s expected = metadata[:response][:code].to_s
raise UnexpectedResponse, "Expected response code '#{code}' to match '#{@api_metadata[:response][:code]}'" if code != expected
raise UnexpectedResponse, "Expected response code '#{code}' to match '#{expected}'"
end end
end end
def validate_headers!(headers) def validate_headers!(metadata, headers)
header_schema = @api_metadata[:response][:headers] expected = (metadata[:response][:headers] || {}).keys
return if header_schema.nil? expected.each do |name|
raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].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?
end end
end end
def validate_body!(body) def validate_body!(metadata, swagger_doc, body)
response_schema = @api_metadata[:response][:schema] response_schema = metadata[:response][:schema]
return if response_schema.nil? return if response_schema.nil?
begin validation_schema = response_schema
validation_schema = response_schema .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema')
.merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema') .merge(swagger_doc.slice(:definitions))
.merge(@global_metadata.slice(:definitions)) errors = JSON::Validator.fully_validate(validation_schema, body)
JSON::Validator.validate!(validation_schema, body) raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
rescue JSON::Schema::ValidationError => ex
raise UnexpectedResponse, "Expected response body to match schema: #{ex.message}"
end
end end
end end

View File

@@ -120,6 +120,15 @@ module Rswag
) )
end end
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 end
describe '#response(code, description)' do describe '#response(code, description)' do

View File

@@ -7,19 +7,30 @@ module Rswag
subject { double('example') } subject { double('example') }
before do before do
subject.extend ExampleHelpers subject.extend(ExampleHelpers)
# Mock out some infrastructure allow(Rswag::Specs).to receive(:config).and_return(config)
stub_const('Rails::VERSION::MAJOR', 3) allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
rswag_config = double('rswag_config') stub_const('Rswag::Specs::RAILS_VERSION', 3)
allow(rswag_config).to receive(:get_swagger_doc).and_return(global_metadata)
allow(subject).to receive(:rswag_config).and_return(rswag_config)
end end
let(:api_metadata) do let(:config) { double('config') }
let(:swagger_doc) do
{
securityDefinitions: {
api_key: {
type: :apiKey,
name: 'api_key',
in: :query
}
}
}
end
let(:metadata) do
{ {
path_item: { template: '/blogs/{blog_id}/comments/{id}' }, path_item: { template: '/blogs/{blog_id}/comments/{id}' },
operation: { operation: {
verb: :put, verb: :put,
summary: 'Updates a blog', summary: 'Updates a blog',
consumes: [ 'application/json' ],
parameters: [ parameters: [
{ name: :blog_id, in: :path, type: 'integer' }, { name: :blog_id, in: :path, type: 'integer' },
{ name: 'id', in: :path, type: 'integer' }, { name: 'id', in: :path, type: 'integer' },
@@ -32,19 +43,8 @@ module Rswag
} }
} }
end end
let(:global_metadata) do
{
securityDefinitions: {
api_key: {
type: :apiKey,
name: 'api_key',
in: :query
}
}
}
end
describe '#submit_request(api_metadata)' do describe '#submit_request(metadata)' do
before do before do
allow(subject).to receive(:blog_id).and_return(1) allow(subject).to receive(:blog_id).and_return(1)
allow(subject).to receive(:id).and_return(2) allow(subject).to receive(:id).and_return(2)
@@ -52,14 +52,14 @@ module Rswag
allow(subject).to receive(:api_key).and_return('fookey') allow(subject).to receive(:api_key).and_return('fookey')
allow(subject).to receive(:blog).and_return(text: 'Some comment') allow(subject).to receive(:blog).and_return(text: 'Some comment')
allow(subject).to receive(:put) allow(subject).to receive(:put)
subject.submit_request(api_metadata) subject.submit_request(metadata)
end end
it "submits a request built from metadata and 'let' values" do it "submits a request built from metadata and 'let' values" do
expect(subject).to have_received(:put).with( expect(subject).to have_received(:put).with(
'/blogs/1/comments/2?q1=foo&api_key=fookey', '/blogs/1/comments/2?q1=foo&api_key=fookey',
"{\"text\":\"Some comment\"}", "{\"text\":\"Some comment\"}",
{} { 'CONTENT_TYPE' => 'application/json' }
) )
end end
end end

View File

@@ -4,249 +4,301 @@ module Rswag
module Specs module Specs
describe RequestFactory do describe RequestFactory do
subject { RequestFactory.new(api_metadata, global_metadata) } subject { RequestFactory.new(config) }
before do before do
allow(example).to receive(:blog_id).and_return(1) allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
allow(example).to receive(:id).and_return('2')
end end
let(:api_metadata) do let(:config) { double('config') }
let(:swagger_doc) { {} }
let(:example) { double('example') }
let(:metadata) do
{ {
path_item: { template: '/blogs/{blog_id}/comments/{id}' }, path_item: { template: '/blogs' },
operation: { operation: { verb: :get }
verb: :put,
summary: 'Updates a blog',
parameters: [
{ name: :blog_id, in: :path, type: 'integer' },
{ name: 'id', in: :path, type: 'integer' }
]
}
} }
end end
let(:global_metadata) { {} }
let(:example) { double('example') }
describe '#build_fullpath(example)' do describe '#build_request(metadata, example)' do
let(:path) { subject.build_fullpath(example) } let(:request) { subject.build_request(metadata, example) }
context 'always' do it 'builds request hash for given example' do
it "builds a path using metadata and example values" do expect(request[:verb]).to eq(:get)
expect(path).to eq('/blogs/1/comments/2') expect(request[:path]).to eq('/blogs')
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
it 'builds the path from example values' do
expect(request[:path]).to eq('/blogs/1/comments/2')
end end
end end
context "'query' parameters" do context "'query' parameters" do
before do before do
api_metadata[:operation][:parameters] << { name: 'q1', in: :query, type: 'string' } metadata[:operation][:parameters] = [
api_metadata[:operation][:parameters] << { name: 'q2', in: :query, type: 'string' } { name: 'q1', in: :query, type: :string },
{ name: 'q2', in: :query, type: :string }
]
allow(example).to receive(:q1).and_return('foo') allow(example).to receive(:q1).and_return('foo')
allow(example).to receive(:q2).and_return('bar') allow(example).to receive(:q2).and_return('bar')
end end
it "appends a query string using metadata and example values" do it "builds the query string from example values" do
expect(path).to eq('/blogs/1/comments/2?q1=foo&q2=bar') expect(request[:path]).to eq('/blogs?q1=foo&q2=bar')
end end
end end
context "'query' parameter of type 'array'" do context "'query' parameters of type 'array'" do
before do before do
api_metadata[:operation][:parameters] << { metadata[:operation][:parameters] = [
name: 'things', { name: 'things', in: :query, type: :array, collectionFormat: collection_format }
in: :query, ]
type: :array,
collectionFormat: collectionFormat
}
allow(example).to receive(:things).and_return([ 'foo', 'bar' ]) allow(example).to receive(:things).and_return([ 'foo', 'bar' ])
end end
context 'collectionFormat = csv' do context 'collectionFormat = csv' do
let(:collectionFormat) { :csv } let(:collection_format) { :csv }
it "formats as comma separated values" do it "formats as comma separated values" do
expect(path).to eq('/blogs/1/comments/2?things=foo,bar') expect(request[:path]).to eq('/blogs?things=foo,bar')
end end
end end
context 'collectionFormat = ssv' do context 'collectionFormat = ssv' do
let(:collectionFormat) { :ssv } let(:collection_format) { :ssv }
it "formats as space separated values" do it "formats as space separated values" do
expect(path).to eq('/blogs/1/comments/2?things=foo bar') expect(request[:path]).to eq('/blogs?things=foo bar')
end end
end end
context 'collectionFormat = tsv' do context 'collectionFormat = tsv' do
let(:collectionFormat) { :tsv } let(:collection_format) { :tsv }
it "formats as tab separated values" do it "formats as tab separated values" do
expect(path).to eq('/blogs/1/comments/2?things=foo\tbar') expect(request[:path]).to eq('/blogs?things=foo\tbar')
end end
end end
context 'collectionFormat = pipes' do context 'collectionFormat = pipes' do
let(:collectionFormat) { :pipes } let(:collection_format) { :pipes }
it "formats as pipe separated values" do it "formats as pipe separated values" do
expect(path).to eq('/blogs/1/comments/2?things=foo|bar') expect(request[:path]).to eq('/blogs?things=foo|bar')
end end
end end
context 'collectionFormat = multi' do context 'collectionFormat = multi' do
let(:collectionFormat) { :multi } let(:collection_format) { :multi }
it "formats as multiple parameter instances" do it "formats as multiple parameter instances" do
expect(path).to eq('/blogs/1/comments/2?things=foo&things=bar') expect(request[:path]).to eq('/blogs?things=foo&things=bar')
end end
end end
end end
context "global definition for 'api_key in query'" do context "'header' parameters" do
before do before do
global_metadata[:securityDefinitions] = { api_key: { type: :apiKey, name: 'api_key', in: :query } } metadata[:operation][:parameters] = [ { name: 'Api-Key', in: :header, type: :string } ]
allow(example).to receive(:api_key).and_return('fookey') allow(example).to receive(:'Api-Key').and_return('foobar')
end end
context 'global requirement' do it 'adds names and example values to headers' do
before { global_metadata[:security] = [ { api_key: [] } ] } expect(request[:headers]).to eq({ 'Api-Key' => 'foobar' })
end
end
it "appends the api_key using metadata and example value" do context 'optional parameters not provided' do
expect(path).to eq('/blogs/1/comments/2?api_key=fookey') 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
end end
context 'operation-specific requirement' do context "explicit 'Content-Type' provided" do
before { api_metadata[:operation][:security] = [ { api_key: [] } ] } before do
allow(example).to receive(:'Content-Type').and_return('application/xml')
it "appends the api_key using metadata and example value" do
expect(path).to eq('/blogs/1/comments/2?api_key=fookey')
end 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 'apiKey' do
before do
swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: key_location } }
metadata[:operation][:security] = [ apiKey: [] ]
allow(example).to receive(:api_key).and_return('foobar')
end
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
end
context 'basic auth' do
before do
swagger_doc[:securityDefinitions] = { 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 'oauth2' do
before do
swagger_doc[:securityDefinitions] = { 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 "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
end end
context 'global basePath' do context 'global basePath' do
before { global_metadata[:basePath] = '/foobar' } before { swagger_doc[:basePath] = '/api' }
it 'prepends the basePath' do it 'prepends to the path' do
expect(path).to eq('/foobar/blogs/1/comments/2') expect(request[:path]).to eq('/api/blogs')
end end
end end
context "defined at the 'path' level" do 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 before do
api_metadata[:path_item][:parameters] = [ { name: :blog_id, in: :path } ] swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: :query } }
api_metadata[:operation][:parameters] = [ { name: :id, in: :path } ] swagger_doc[:security] = [ apiKey: [] ]
allow(example).to receive(:api_key).and_return('foobar')
end end
it "builds path from parameters defined at path and operation levels" do it 'applieds the scheme by default' do
expect(path).to eq('/blogs/1/comments/2') expect(request[:path]).to eq('/blogs?api_key=foobar')
end
end
end
describe '#build_body(example)' do
let(:body) { subject.build_body(example) }
context "no 'body' parameter" do
it "returns ''" do
expect(body).to eq('')
end
end
context "'body' parameter" do
before do
api_metadata[:operation][:parameters] << { name: 'comment', in: :body, schema: { type: 'object' } }
allow(example).to receive(:comment).and_return(text: 'Some comment')
end
it 'returns the example value as a json string' do
expect(body).to eq("{\"text\":\"Some comment\"}")
end
end
context "referenced 'body' parameter" do
before do
api_metadata[:operation][:parameters] << { '$ref' => '#/parameters/comment' }
global_metadata[:parameters] = {
'comment' => { name: 'comment', in: :body, schema: { type: 'object' } }
}
allow(example).to receive(:comment).and_return(text: 'Some comment')
end
it 'returns the example value as a json string' do
expect(body).to eq("{\"text\":\"Some comment\"}")
end
end
end
describe '#build_headers' do
let(:headers) { subject.build_headers(example) }
context "no 'header' params" do
it 'returns an empty hash' do
expect(headers).to eq({})
end
end
context "'header' params" do
before do
api_metadata[:operation][:parameters] << { name: 'Api-Key', in: :header, type: 'string' }
allow(example).to receive(:'Api-Key').and_return('foobar')
end
it 'returns a hash of names with example values' do
expect(headers).to eq({ 'Api-Key' => 'foobar' })
end
end
context "global definition for 'basic auth'" do
before do
global_metadata[:securityDefinitions] = { basic_auth: { type: :basic} }
allow(example).to receive(:'Authorization').and_return('Basic foobar')
end
context 'global requirement' do
before { global_metadata[:security] = [ { basic_auth: [] } ] }
it "includes a corresponding Authorization header" do
expect(headers).to match(
'Authorization' => 'Basic foobar'
)
end
end
context 'operation-specific requirement' do
before { api_metadata[:operation][:security] = [ { basic_auth: [] } ] }
it "includes a corresponding Authorization header" do
expect(headers).to match(
'Authorization' => 'Basic foobar'
)
end
end
end
context 'consumes & produces' do
before do
api_metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ]
api_metadata[:operation][:produces] = [ 'application/json', 'application/xml' ]
end
it "includes corresponding 'Accept' & 'Content-Type' headers" do
expect(headers).to match(
'Accept' => 'application/json;application/xml',
'Content-Type' => 'application/json;application/xml'
)
end
end
context 'global consumes & produces' do
let(:global_metadata) do
{
consumes: [ 'application/json', 'application/xml' ],
produces: [ 'application/json', 'application/xml' ]
}
end
it "includes corresponding 'Accept' & 'Content-Type' headers" do
expect(headers).to match(
'Accept' => 'application/json;application/xml',
'Content-Type' => 'application/json;application/xml'
)
end end
end end
end end

View File

@@ -4,121 +4,71 @@ module Rswag
module Specs module Specs
describe ResponseValidator do describe ResponseValidator do
subject { ResponseValidator.new(api_metadata, global_metadata) } subject { ResponseValidator.new(config) }
let(:api_metadata) { { response: { code: 200 } } } before do
let(:global_metadata) { {} } allow(config).to receive(:get_swagger_doc).and_return(swagger_doc)
end
describe '#validate!(response)' do let(:config) { double('config') }
let(:call) { subject.validate!(response) } let(:swagger_doc) { {} }
let(:example) { double('example') }
context "no 'schema' provided" do let(:metadata) do
context 'response code matches' do {
let(:response) { OpenStruct.new(code: 200, body: '') } response: {
it { expect { call }.to_not raise_error } code: 200,
end headers: { 'X-Rate-Limit-Limit' => { type: :integer } },
schema: {
context 'response code does not match' do type: :object,
let(:response) { OpenStruct.new(code: 201, body: '') } properties: { text: { type: :string } },
it { expect { call }.to raise_error UnexpectedResponse } required: [ 'text' ]
end
end
context "'schema' provided" do
before do
api_metadata[:response][:schema] = {
type: 'object',
properties: { text: { type: 'string' } },
required: ['text']
} }
end }
}
end
context 'response code & body matches' do describe '#validate!(metadata, response)' do
let(:response) { OpenStruct.new(code: 200, body: "{\"text\":\"Some comment\"}") } let(:call) { subject.validate!(metadata, response) }
it { expect { call }.to_not raise_error } let(:response) do
end OpenStruct.new(
code: '200',
context 'response code matches & body does not' do headers: { 'X-Rate-Limit-Limit' => '10' },
let(:response) { OpenStruct.new(code: 200, body: "{\"foo\":\"Some comment\"}") } body: "{\"text\":\"Some comment\"}"
it { expect { call }.to raise_error UnexpectedResponse } )
end
context "'block' provided" do
let(:call) do
subject.validate!(response) do |response|
data = JSON.parse(response.body)
expect(data['text']).to eq('Some comment')
end
end
context 'the block validation passes' do
let(:response) { OpenStruct.new(code: 200, body: "{\"text\":\"Some comment\"}") }
it { expect { call }.to_not raise_error }
end
context 'the block validation fails' do
let(:response) { OpenStruct.new(code: 200, body: "{\"text\":\"Some other comment\"}") }
it { expect { call }.to raise_error(RSpec::Expectations::ExpectationNotMetError) }
end
end
end end
context "referenced 'schema' provided" do 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 before do
api_metadata[:response][:schema] = { '$ref' => '#/definitions/author' } swagger_doc[:definitions] = {
global_metadata[:definitions] = { 'blog' => {
author: { type: :object,
type: 'object', properties: { foo: { type: :string } },
properties: { name: { type: 'string' } }, required: [ 'foo' ]
required: ['name']
} }
} }
metadata[:response][:schema] = { '$ref' => '#/definitions/blog' }
end end
context 'response code & body matches' do it 'uses the referenced schema to validate the response body' do
let(:response) { OpenStruct.new(code: 200, body: "{\"name\":\"Some name\"}") } expect { call }.to raise_error /Expected response body/
it { expect { call }.to_not raise_error }
end
context 'response code matches & body does not' do
let(:response) { OpenStruct.new(code: 200, body: "{\"foo\":\"Some name\"}") }
it { expect { call }.to raise_error UnexpectedResponse }
end
end
context "'headers' provided" do
before do
api_metadata[:response][:headers] = {
'X-Rate-Limit-Limit' => {
description: 'The number of allowed requests in the current period',
type: 'integer'
},
'X-Rate-Limit-Remaining' => {
description: 'The number of remaining requests in the current period',
type: 'integer'
},
'X-Rate-Limit-Reset' => {
description: 'The number of seconds left in the current period',
type: 'integer'
}
}
end
context 'response code & body matches' do
let(:response) { OpenStruct.new(code: 200, body: '{}', headers: {
'X-Rate-Limit-Limit' => 1,
'X-Rate-Limit-Remaining' => 1,
'X-Rate-Limit-Reset' => 1
}) }
it { expect { call }.to_not raise_error }
end
context 'response code matches & body does not' do
let(:response) { OpenStruct.new(code: 200, body: '{}', headers: {
'X-Rate-Limit-Limit' => 1,
'X-Rate-Limit-Remaining' => 1
}) }
it { expect { call }.to raise_error UnexpectedResponse }
end end
end end
end end

View File

@@ -4,4 +4,9 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
wrap_parameters format: [ :json ] wrap_parameters format: [ :json ]
respond_to :json
rescue_from 'ActionController::UnknownFormat' do |ex|
head :not_acceptable
end
end end

View File

@@ -1,6 +1,4 @@
class AuthTestsController < ApplicationController class AuthTestsController < ApplicationController
wrap_parameters Blog
respond_to :json
# POST /auth-tests/basic # POST /auth-tests/basic
def basic def basic

View File

@@ -1,6 +1,6 @@
require 'fileutils'
class BlogsController < ApplicationController class BlogsController < ApplicationController
wrap_parameters Blog
respond_to :json
# POST /blogs # POST /blogs
def create def create
@@ -8,6 +8,14 @@ class BlogsController < ApplicationController
respond_with @blog respond_with @blog
end end
# Put /blogs/1
def upload
@blog = Blog.find_by_id(params[:id])
return head :not_found if @blog.nil?
@blog.thumbnail = save_uploaded_file params[:file]
head @blog.save ? :ok : :unprocsessible_entity
end
# GET /blogs # GET /blogs
def index def index
@blogs = Blog.all @blogs = Blog.all
@@ -24,4 +32,13 @@ class BlogsController < ApplicationController
respond_with @blog, status: :not_found and return unless @blog respond_with @blog, status: :not_found and return unless @blog
respond_with @blog respond_with @blog
end end
private
def save_uploaded_file(field)
return if field.nil?
file = File.join('tmp', field.original_filename)
FileUtils.cp field.tempfile.path, file
field.original_filename
end
end end

View File

@@ -5,7 +5,8 @@ class Blog < ActiveRecord::Base
{ {
id: id, id: id,
title: title, title: title,
content: nil content: nil,
thumbnail: thumbnail
} }
end end
end end

View File

@@ -1,5 +1,6 @@
TestApp::Application.routes.draw do TestApp::Application.routes.draw do
resources :blogs, defaults: { :format => :json } resources :blogs
put '/blogs/:id/upload', to: 'blogs#upload'
post 'auth-tests/basic', to: 'auth_tests#basic' post 'auth-tests/basic', to: 'auth_tests#basic'

View File

@@ -9,6 +9,7 @@ class CreateBlogs < migration_class
create_table :blogs do |t| create_table :blogs do |t|
t.string :title t.string :title
t.text :content t.text :content
t.string :thumbnail
t.timestamps t.timestamps
end end

View File

@@ -15,6 +15,7 @@ ActiveRecord::Schema.define(version: 20160218212104) do
create_table "blogs", force: :cascade do |t| create_table "blogs", force: :cascade do |t|
t.string "title" t.string "title"
t.text "content" t.text "content"
t.string "thumbnail"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
end end

BIN
test-app/spec/fixtures/thumbnail.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -9,10 +9,12 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
description 'Creates a new blog from provided data' description 'Creates a new blog from provided data'
operationId 'createBlog' operationId 'createBlog'
consumes 'application/json' consumes 'application/json'
parameter name: :blog, :in => :body, schema: { '$ref' => '#/definitions/blog' } produces 'application/json'
parameter name: :blog, in: :body, schema: { '$ref' => '#/definitions/blog' }
let(:blog) { { title: 'foo', content: 'bar' } }
response '201', 'blog created' do response '201', 'blog created' do
let(:blog) { { title: 'foo', content: 'bar' } }
run_test! run_test!
end end
@@ -20,7 +22,9 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
schema '$ref' => '#/definitions/errors_object' schema '$ref' => '#/definitions/errors_object'
let(:blog) { { title: 'foo' } } let(:blog) { { title: 'foo' } }
run_test! run_test! do |response|
expect(response.body).to include("can't be blank")
end
end end
end end
@@ -31,17 +35,24 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
produces 'application/json' produces 'application/json'
parameter name: :keywords, in: :query, type: 'string' parameter name: :keywords, in: :query, type: 'string'
let(:keywords) { 'foo bar' }
response '200', 'success' do response '200', 'success' do
schema type: 'array', items: { '$ref' => '#/definitions/blog' } schema type: 'array', items: { '$ref' => '#/definitions/blog' }
end
let(:keywords) { 'foo bar' } response '406', 'unsupported accept header' do
let(:'Accept') { 'application/foo' }
run_test! run_test!
end end
end end
end end
path '/blogs/{id}' do path '/blogs/{id}' do
parameter name: :id, :in => :path, :type => :string parameter name: :id, in: :path, type: :string
let(:id) { blog.id }
let(:blog) { Blog.create(title: 'foo', content: 'bar', thumbnail: 'thumbnail.png') }
get 'Retrieves a blog' do get 'Retrieves a blog' do
tags 'Blogs' tags 'Blogs'
@@ -59,10 +70,10 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
examples 'application/json' => { examples 'application/json' => {
id: 1, id: 1,
title: 'Hello world!', title: 'Hello world!',
content: 'Hello world and hello universe. Thank you all very much!!!' content: 'Hello world and hello universe. Thank you all very much!!!',
thumbnail: "thumbnail.png"
} }
let(:blog) { Blog.create(title: 'foo', content: 'bar') }
let(:id) { blog.id } let(:id) { blog.id }
run_test! run_test!
end end
@@ -73,4 +84,24 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do
end end
end end
end end
path '/blogs/{id}/upload' do
parameter name: :id, in: :path, type: :string
let(:id) { blog.id }
let(:blog) { Blog.create(title: 'foo', content: 'bar') }
put 'upload a blog thumbnail' do
tags 'Blogs'
description 'Upload a thumbnail for specific blog by id'
operationId 'uploadThumbnailBlog'
consumes 'multipart/form-data'
parameter name: :file, :in => :formData, :type => :file, required: true
response '200', 'blog updated' do
let(:file) { Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures/thumbnail.png")) }
run_test!
end
end
end
end end

View File

@@ -47,6 +47,10 @@ RSpec.configure do |config|
# triggering implicit auto-inclusion in groups with matching metadata. # triggering implicit auto-inclusion in groups with matching metadata.
config.shared_context_metadata_behavior = :apply_to_host_groups config.shared_context_metadata_behavior = :apply_to_host_groups
config.after(:suite) do
File.delete("#{Rails.root}/tmp/thumbnail.png") if File.file?("#{Rails.root}/tmp/thumbnail.png")
end
# The settings below are suggested to provide a good initial experience # The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content. # with RSpec, but feel free to customize to your heart's content.
=begin =begin

View File

@@ -39,9 +39,10 @@ RSpec.configure do |config|
properties: { properties: {
id: { type: 'integer' }, id: { type: 'integer' },
title: { type: 'string' }, title: { type: 'string' },
content: { type: 'string', 'x-nullable': true } content: { type: 'string', 'x-nullable': true },
thumbnail: { type: 'string'}
}, },
required: [ 'id', 'title', 'content' ] required: [ 'id', 'title', 'content', 'thumbnail' ]
} }
}, },
securityDefinitions: { securityDefinitions: {

View File

@@ -40,6 +40,9 @@
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"produces": [
"application/json"
],
"parameters": [ "parameters": [
{ {
"name": "blog", "name": "blog",
@@ -79,14 +82,8 @@
} }
], ],
"responses": { "responses": {
"200": { "406": {
"description": "success", "description": "unsupported accept header"
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/blog"
}
}
} }
} }
} }
@@ -131,7 +128,8 @@
"application/json": { "application/json": {
"id": 1, "id": 1,
"title": "Hello world!", "title": "Hello world!",
"content": "Hello world and hello universe. Thank you all very much!!!" "content": "Hello world and hello universe. Thank you all very much!!!",
"thumbnail": "thumbnail.png"
} }
} }
}, },
@@ -140,6 +138,40 @@
} }
} }
} }
},
"/blogs/{id}/upload": {
"parameters": [
{
"name": "id",
"in": "path",
"type": "string",
"required": true
}
],
"put": {
"summary": "upload a blog thumbnail",
"tags": [
"Blogs"
],
"description": "Upload a thumbnail for specific blog by id",
"operationId": "uploadThumbnailBlog",
"consumes": [
"multipart/form-data"
],
"parameters": [
{
"name": "file",
"in": "formData",
"type": "file",
"required": true
}
],
"responses": {
"200": {
"description": "blog updated"
}
}
}
} }
}, },
"definitions": { "definitions": {
@@ -172,12 +204,16 @@
"content": { "content": {
"type": "string", "type": "string",
"x-nullable": true "x-nullable": true
},
"thumbnail": {
"type": "string"
} }
}, },
"required": [ "required": [
"id", "id",
"title", "title",
"content" "content",
"thumbnail"
] ]
} }
}, },