diff --git a/README.md b/README.md index 7e45f4d..63ed64a 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,11 @@ Once you have an API that can describe itself in Swagger, you've opened the trea let(:id) { 'invalid' } run_test! end + + response '406', 'unsupported accept header' do + let(:'Accept') { 'application/foo' } + run_test! + end end end end diff --git a/rswag-specs/lib/rswag/specs/example_helpers.rb b/rswag-specs/lib/rswag/specs/example_helpers.rb index d669c28..302a84a 100644 --- a/rswag-specs/lib/rswag/specs/example_helpers.rb +++ b/rswag-specs/lib/rswag/specs/example_helpers.rb @@ -5,33 +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[:body], + rackify_headers(request[:headers]) # Rails test infrastructure requires Rack 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[:body], + 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) + def assert_response_matches_metadata(metadata, &block) + ResponseValidator.new.validate!(metadata, response, &block) end private @@ -51,10 +48,6 @@ module Rswag Hash[ name_value_pairs ] end - - def rswag_config - ::Rswag::Specs.config - end end end end diff --git a/rswag-specs/lib/rswag/specs/request_factory.rb b/rswag-specs/lib/rswag/specs/request_factory.rb index 197238e..ac5a6b3 100644 --- a/rswag-specs/lib/rswag/specs/request_factory.rb +++ b/rswag-specs/lib/rswag/specs/request_factory.rb @@ -5,96 +5,78 @@ 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, parameters, example) + add_content(request, metadata, swagger_doc, parameters, example) end end - def build_query_string(example) - query_string = parameters_in(:query) - .select { |p| p.fetch(:required, true) || - example.respond_to?(p[:name]) } - .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 + 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) + + operation_params .concat(path_item_params) - .uniq { |p| p[:name] } # operation params should override path_item params - - applicable_params - .map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references - .concat(security_parameters) - .select { |p| p[:in] == location } + .concat(security_params) + .map { |p| p['$ref'] ? resolve_parameter(p['$ref'], swagger_doc) : p } + .uniq { |p| "#{p[:name]}_#{p[:in]}" } + .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) } end - def resolve_parameter(ref) - defined_params = @global_metadata[:parameters] + def derive_security_params(metadata, swagger_doc) + 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/', '') - raise "Referenced parameter '#{ref}' must be defined" unless defined_params && defined_params[key] - defined_params[key] + raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key] + definitions[key] 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 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(' ')}" @@ -108,6 +90,36 @@ module Rswag "#{name}=#{value.join(',')}" # csv is default end end + + def add_headers(request, parameters, example) + name_value_pairs = parameters + .select { |p| p[:in] == :header } + .map { |p| [ p[:name], example.send(p[:name]).to_s ] } + + request[:headers] = Hash[ name_value_pairs ] + end + + def add_content(request, metadata, swagger_doc, parameters, example) + # Accept header + produces = metadata[:operation][:produces] || swagger_doc[:produces] + if produces + accept = example.respond_to?(:'Accept') ? example.send(:'Accept') : produces.first + request[:headers]['Accept'] = accept + end + + # Content-Type and body + consumes = metadata[:operation][:consumes] || swagger_doc[:consumes] + return if consumes.nil? + + content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first + request[:headers]['Content-Type'] = content_type + + if content_type.include?('json') + body_param = parameters.select { |p| p[:in] == :body }.first + return if body_param.nil? + request[:body] = example.send(body_param[:name]).to_json + end + end end end end diff --git a/rswag-specs/lib/rswag/specs/response_validator.rb b/rswag-specs/lib/rswag/specs/response_validator.rb index 30a8ec8..f5c4cf7 100644 --- a/rswag-specs/lib/rswag/specs/response_validator.rb +++ b/rswag-specs/lib/rswag/specs/response_validator.rb @@ -7,46 +7,44 @@ 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) + def validate!(metadata, response, &block) + swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) + + validate_code!(metadata, response.code) + validate_headers!(metadata, response.headers) + validate_body!(metadata, swagger_doc, response.body, &block) block.call(response) if block_given? 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, code) + expected = metadata[:response][:code].to_s + if code != expected + raise UnexpectedResponse, "Expected response code '#{code}' to match '#{expected}'" 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? validation_schema = response_schema .merge('$schema' => 'http://tempuri.org/rswag/specs/extended_schema') - .merge(@global_metadata.slice(:definitions)) - error_messages = JSON::Validator.fully_validate(validation_schema, body) - if error_messages.any? - raise UnexpectedResponse, "Expected response body to match schema: #{error_messages[0]}" - end + .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 diff --git a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb index 6b389d4..3b22af4 100644 --- a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb @@ -7,19 +7,30 @@ module Rswag subject { double('example') } before do - subject.extend ExampleHelpers - # Mock out some infrastructure - stub_const('Rails::VERSION::MAJOR', 3) - rswag_config = double('rswag_config') - allow(rswag_config).to receive(:get_swagger_doc).and_return(global_metadata) - allow(subject).to receive(:rswag_config).and_return(rswag_config) + 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(: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}' }, operation: { verb: :put, summary: 'Updates a blog', + consumes: [ 'application/json' ], parameters: [ { name: :blog_id, in: :path, type: 'integer' }, { name: 'id', in: :path, type: 'integer' }, @@ -32,19 +43,8 @@ module Rswag } } 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 allow(subject).to receive(:blog_id).and_return(1) 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(:blog).and_return(text: 'Some comment') allow(subject).to receive(:put) - subject.submit_request(api_metadata) + 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 diff --git a/rswag-specs/spec/rswag/specs/request_factory_spec.rb b/rswag-specs/spec/rswag/specs/request_factory_spec.rb index f93c715..423272f 100644 --- a/rswag-specs/spec/rswag/specs/request_factory_spec.rb +++ b/rswag-specs/spec/rswag/specs/request_factory_spec.rb @@ -4,265 +4,282 @@ module Rswag module Specs describe RequestFactory do - subject { RequestFactory.new(api_metadata, global_metadata) } + subject { RequestFactory.new(config) } before do - allow(example).to receive(:blog_id).and_return(1) - allow(example).to receive(:id).and_return('2') + allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) 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}' }, - operation: { - verb: :put, - summary: 'Updates a blog', - parameters: [ - { name: :blog_id, in: :path, type: 'integer' }, - { name: 'id', in: :path, type: 'integer' } - ] - } + path_item: { template: '/blogs' }, + operation: { verb: :get } } end - let(:global_metadata) { {} } - let(:example) { double('example') } - describe '#build_fullpath(example)' do - let(:path) { subject.build_fullpath(example) } + describe '#build_request(metadata, example)' do + let(:request) { subject.build_request(metadata, example) } - context 'always' do - it "builds a path using metadata and example values" do - expect(path).to eq('/blogs/1/comments/2') + it 'builds request hash for given example' do + expect(request[:verb]).to eq(:get) + 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 context "'query' parameters" do before do - api_metadata[:operation][:parameters] << { name: 'q1', in: :query, type: 'string' } - api_metadata[:operation][:parameters] << { name: 'q2', in: :query, type: 'string' } + 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 "appends a query string using metadata and example values" do - expect(path).to eq('/blogs/1/comments/2?q1=foo&q2=bar') + it "builds the query string from example values" do + expect(request[:path]).to eq('/blogs?q1=foo&q2=bar') end end - context "optional 'query' parameters" do + context "'query' parameters of type 'array'" do before do - api_metadata[:operation][:parameters] << { name: 'q1', in: :query, type: 'string' } - api_metadata[:operation][:parameters] << { name: 'q2', in: :query, type: 'string', required: true } - api_metadata[:operation][:parameters] << { name: 'q3', in: :query, type: 'string', required: false } - api_metadata[:operation][:parameters] << { name: 'q4', in: :query, type: 'string', required: false } - allow(example).to receive(:q1).and_return('foo') - allow(example).to receive(:q2).and_return('bar') - allow(example).to receive(:q3).and_return('baz') - end - - it "appends a query string using metadata and example values" do - expect(path).to eq('/blogs/1/comments/2?q1=foo&q2=bar&q3=baz') - end - end - - context "'query' parameter of type 'array'" do - before do - api_metadata[:operation][:parameters] << { - name: 'things', - in: :query, - type: :array, - collectionFormat: collectionFormat - } + metadata[:operation][:parameters] = [ + { name: 'things', in: :query, type: :array, collectionFormat: collection_format } + ] allow(example).to receive(:things).and_return([ 'foo', 'bar' ]) end context 'collectionFormat = csv' do - let(:collectionFormat) { :csv } + let(:collection_format) { :csv } 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 context 'collectionFormat = ssv' do - let(:collectionFormat) { :ssv } + let(:collection_format) { :ssv } 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 context 'collectionFormat = tsv' do - let(:collectionFormat) { :tsv } + let(:collection_format) { :tsv } 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 context 'collectionFormat = pipes' do - let(:collectionFormat) { :pipes } + let(:collection_format) { :pipes } 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 context 'collectionFormat = multi' do - let(:collectionFormat) { :multi } + let(:collection_format) { :multi } 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 - context "global definition for 'api_key in query'" do + context "'header' parameters" do before do - global_metadata[:securityDefinitions] = { api_key: { type: :apiKey, name: 'api_key', in: :query } } - allow(example).to receive(:api_key).and_return('fookey') + metadata[:operation][:parameters] = [ { name: 'Api-Key', in: :header, type: :string } ] + allow(example).to receive(:'Api-Key').and_return('foobar') end - context 'global requirement' do - before { global_metadata[:security] = [ { api_key: [] } ] } + it 'adds names and example values to headers' do + expect(request[:headers]).to eq({ 'Api-Key' => 'foobar' }) + end + end - it "appends the api_key using metadata and example value" do - expect(path).to eq('/blogs/1/comments/2?api_key=fookey') + 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 'operation-specific requirement' do - before { api_metadata[:operation][:security] = [ { api_key: [] } ] } - - it "appends the api_key using metadata and example value" do - expect(path).to eq('/blogs/1/comments/2?api_key=fookey') + 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 body' 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 'sets body to example value as JSON string' do + expect(request[:body]).to eq("{\"text\":\"Some comment\"}") + 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 'Accept' header to first in list" do + expect(request[:headers]).to eq('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 'Accept' header to example value" do + expect(request[:headers]).to eq('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 'Authorization' header to example value" do + expect(request[:headers]).to eq('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 'Authorization' header to example value" do + expect(request[:headers]).to eq('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 context 'global basePath' do - before { global_metadata[:basePath] = '/foobar' } + before { swagger_doc[:basePath] = '/api' } - it 'prepends the basePath' do - expect(path).to eq('/foobar/blogs/1/comments/2') + it 'prepends to the path' do + expect(request[:path]).to eq('/api/blogs') 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 - api_metadata[:path_item][:parameters] = [ { name: :blog_id, in: :path } ] - api_metadata[:operation][:parameters] = [ { name: :id, in: :path } ] + swagger_doc[:securityDefinitions] = { apiKey: { type: :apiKey, name: 'api_key', in: :query } } + swagger_doc[:security] = [ apiKey: [] ] + allow(example).to receive(:api_key).and_return('foobar') end - it "builds path from parameters defined at path and operation levels" do - expect(path).to eq('/blogs/1/comments/2') - 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' - ) + it 'applieds the scheme by default' do + expect(request[:path]).to eq('/blogs?api_key=foobar') end end end diff --git a/rswag-specs/spec/rswag/specs/response_validator_spec.rb b/rswag-specs/spec/rswag/specs/response_validator_spec.rb index cb95996..b93350b 100644 --- a/rswag-specs/spec/rswag/specs/response_validator_spec.rb +++ b/rswag-specs/spec/rswag/specs/response_validator_spec.rb @@ -4,121 +4,85 @@ module Rswag module Specs describe ResponseValidator do - subject { ResponseValidator.new(api_metadata, global_metadata) } + subject { ResponseValidator.new(config) } - let(:api_metadata) { { response: { code: 200 } } } - let(:global_metadata) { {} } - - describe '#validate!(response)' do - let(:call) { subject.validate!(response) } - - context "no 'schema' provided" do - context 'response code matches' do - let(:response) { OpenStruct.new(code: 200, body: '') } - it { expect { call }.to_not raise_error } - end - - context 'response code does not match' do - let(:response) { OpenStruct.new(code: 201, body: '') } - it { expect { call }.to raise_error UnexpectedResponse } - end - end - - context "'schema' provided" do - before do - api_metadata[:response][: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 } }, + schema: { + type: :object, + properties: { text: { type: :string } }, + required: [ 'text' ] } - end + } + } + end - context 'response code & body matches' do - let(:response) { OpenStruct.new(code: 200, body: "{\"text\":\"Some comment\"}") } + 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 'validation block provided' do + let(:call) { subject.validate!(metadata, response, &block) } + + context 'block passes' do + let(:block) { Proc.new { |response| expect(response.code).to eq('200') } } 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 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 + context 'block fails' do + let(:block) { Proc.new { |response| expect(response.code).to eq('201') } } + it { expect { call }.to raise_error RSpec::Expectations::ExpectationNotMetError } end end - context "referenced 'schema' provided" do + context 'referenced schemas' do before do - api_metadata[:response][:schema] = { '$ref' => '#/definitions/author' } - global_metadata[:definitions] = { - author: { - type: 'object', - properties: { name: { type: 'string' } }, - required: ['name'] + swagger_doc[:definitions] = { + 'blog' => { + type: :object, + properties: { foo: { type: :string } }, + required: [ 'foo' ] } } + metadata[:response][:schema] = { '$ref' => '#/definitions/blog' } end - context 'response code & body matches' do - let(:response) { OpenStruct.new(code: 200, body: "{\"name\":\"Some name\"}") } - 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 } + it 'uses the referenced schema to validate the response body' do + expect { call }.to raise_error /Expected response body/ end end end diff --git a/test-app/app/controllers/application_controller.rb b/test-app/app/controllers/application_controller.rb index 4879440..c31bf0e 100644 --- a/test-app/app/controllers/application_controller.rb +++ b/test-app/app/controllers/application_controller.rb @@ -4,4 +4,9 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :null_session wrap_parameters format: [ :json ] + + respond_to :json + rescue_from 'ActionController::UnknownFormat' do |ex| + head :not_acceptable + end end diff --git a/test-app/app/controllers/auth_tests_controller.rb b/test-app/app/controllers/auth_tests_controller.rb index 4f90b67..162ef2f 100644 --- a/test-app/app/controllers/auth_tests_controller.rb +++ b/test-app/app/controllers/auth_tests_controller.rb @@ -1,6 +1,4 @@ class AuthTestsController < ApplicationController - wrap_parameters Blog - respond_to :json # POST /auth-tests/basic def basic diff --git a/test-app/app/controllers/blogs_controller.rb b/test-app/app/controllers/blogs_controller.rb index 261d718..765576b 100644 --- a/test-app/app/controllers/blogs_controller.rb +++ b/test-app/app/controllers/blogs_controller.rb @@ -1,6 +1,4 @@ class BlogsController < ApplicationController - wrap_parameters Blog - respond_to :json # POST /blogs def create diff --git a/test-app/config/routes.rb b/test-app/config/routes.rb index d8c676e..2974a48 100644 --- a/test-app/config/routes.rb +++ b/test-app/config/routes.rb @@ -1,5 +1,5 @@ TestApp::Application.routes.draw do - resources :blogs, defaults: { :format => :json } + resources :blogs post 'auth-tests/basic', to: 'auth_tests#basic' diff --git a/test-app/spec/integration/blogs_spec.rb b/test-app/spec/integration/blogs_spec.rb index 0ae884c..1949fa9 100644 --- a/test-app/spec/integration/blogs_spec.rb +++ b/test-app/spec/integration/blogs_spec.rb @@ -9,10 +9,12 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do description 'Creates a new blog from provided data' operationId 'createBlog' consumes 'application/json' + produces 'application/json' parameter name: :blog, :in => :body, schema: { '$ref' => '#/definitions/blog' } + let(:blog) { { title: 'foo', content: 'bar' } } + response '201', 'blog created' do - let(:blog) { { title: 'foo', content: 'bar' } } run_test! end @@ -22,6 +24,11 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do let(:blog) { { title: 'foo' } } run_test! end + + response '406', 'unsupported accept header' do + let(:'Accept') { 'application/foo' } + run_test! + end end get 'Searches blogs' do @@ -31,10 +38,15 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do produces 'application/json' parameter name: :keywords, in: :query, type: 'string' + let(:keywords) { 'foo bar' } + response '200', 'success' do schema type: 'array', items: { '$ref' => '#/definitions/blog' } + run_test! + end - let(:keywords) { 'foo bar' } + response '406', 'unsupported accept header' do + let(:'Accept') { 'application/foo' } run_test! end end @@ -43,6 +55,9 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do path '/blogs/{id}' do parameter name: :id, :in => :path, :type => :string + let(:id) { blog.id } + let(:blog) { Blog.create(title: 'foo', content: 'bar') } + get 'Retrieves a blog' do tags 'Blogs' description 'Retrieves a specific blog by id' @@ -62,8 +77,6 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do content: 'Hello world and hello universe. Thank you all very much!!!' } - let(:blog) { Blog.create(title: 'foo', content: 'bar') } - let(:id) { blog.id } run_test! end @@ -71,6 +84,11 @@ describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do let(:id) { 'invalid' } run_test! end + + response '406', 'unsupported accept header' do + let(:'Accept') { 'application/foo' } + run_test! + end end end end diff --git a/test-app/swagger/v1/swagger.json b/test-app/swagger/v1/swagger.json index 0318f94..60f3ca8 100644 --- a/test-app/swagger/v1/swagger.json +++ b/test-app/swagger/v1/swagger.json @@ -40,6 +40,9 @@ "consumes": [ "application/json" ], + "produces": [ + "application/json" + ], "parameters": [ { "name": "blog", @@ -58,6 +61,9 @@ "schema": { "$ref": "#/definitions/errors_object" } + }, + "406": { + "description": "unsupported accept header" } } }, @@ -87,6 +93,9 @@ "$ref": "#/definitions/blog" } } + }, + "406": { + "description": "unsupported accept header" } } } @@ -137,6 +146,9 @@ }, "404": { "description": "blog not found" + }, + "406": { + "description": "unsupported accept header" } } }