Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support response validation #619

Merged
merged 6 commits into from
May 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,77 @@ The concern needs to be included to the controller after the methods are defined
(either at the end of the class, or by using
``Controller.send(:include, Concerns::OauthConcern)``.


Response validation
-------------------

The swagger definitions created by Apipie can be used to auto-generate clients that access the
described APIs. Those clients will break if the responses returned from the API do not match
the declarations. As such, it is very important to include unit tests that validate the actual
responses against the swagger definitions.

The implemented mechanism provides two ways to include such validations in RSpec unit tests:
manual (using an RSpec matcher) and automated (by injecting a test into the http operations 'get', 'post',
raising an error if there is no match).

Example of the manual mechanism:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code:: ruby

require 'apipie/rspec/response_validation_helper'

RSpec.describe MyController, :type => :controller, :show_in_doc => true do

describe "GET stuff with response validation" do
render_views # this makes sure the 'get' operation will actually
# return the rendered view even though this is a Controller spec

it "does something" do
response = get :index, {format: :json}

# the following expectation will fail if the returned object
# does not match the 'returns' declaration in the Controller,
# or if there is no 'returns' declaration for the returned
# HTTP status code
expect(response).to match_declared_responses
end
end
end


Example of the automated mechanism:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code:: ruby

require 'apipie/rspec/response_validation_helper'

RSpec.describe MyController, :type => :controller, :show_in_doc => true do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing indentation and final end for this block.


describe "GET stuff with response validation" do
render_views
auto_validate_rendered_views

it "does something" do
get :index, {format: :json}
end
it "does something else" do
get :another_index, {format: :json}
end
end

describe "GET stuff without response validation" do
it "does something" do
get :index, {format: :json}
end
it "does something else" do
get :another_index, {format: :json}
end
end
end


=========================
Configuration Reference
=========================
Expand Down
11 changes: 11 additions & 0 deletions lib/apipie/apipie_module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ def self.to_swagger_json(version = nil, resource_name = nil, method_name = nil,
app.to_swagger_json(version, resource_name, method_name, lang, clear_warnings)
end

def self.json_schema_for_method_response(controller_name, method_name, return_code, allow_nulls)
# note: this does not support versions (only the default version is queried)!
version ||= Apipie.configuration.default_version
app.json_schema_for_method_response(version, controller_name, method_name, return_code, allow_nulls)
end

def self.json_schema_for_self_describing_class(cls, allow_nulls=true)
app.json_schema_for_self_describing_class(cls, allow_nulls)
end


# all calls delegated to Apipie::Application instance
def self.method_missing(method, *args, &block)
app.respond_to?(method) ? app.send(method, *args, &block) : super
Expand Down
10 changes: 10 additions & 0 deletions lib/apipie/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,16 @@ def reload_examples
@recorded_examples = nil
end

def json_schema_for_method_response(version, controller_name, method_name, return_code, allow_nulls)
method = @resource_descriptions[version][controller_name].method_description(method_name)
raise NoDocumentedMethod.new(controller_name, method_name) if method.nil?
@swagger_generator.json_schema_for_method_response(method, return_code, allow_nulls)
end

def json_schema_for_self_describing_class(cls, allow_nulls)
@swagger_generator.json_schema_for_self_describing_class(cls, allow_nulls)
end

def to_swagger_json(version, resource_name, method_name, lang, clear_warnings=false)
return unless valid_search_args?(version, resource_name, method_name)

Expand Down
4 changes: 3 additions & 1 deletion lib/apipie/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ class Configuration
:persist_show_in_doc, :authorize,
:swagger_include_warning_tags, :swagger_content_type_input, :swagger_json_input_uses_refs,
:swagger_suppress_warnings, :swagger_api_host, :swagger_generate_x_computed_id_field,
:swagger_allow_additional_properties_in_response
:swagger_allow_additional_properties_in_response, :swagger_responses_use_refs

alias_method :validate?, :validate
alias_method :required_by_default?, :required_by_default
alias_method :namespaced_resources?, :namespaced_resources
alias_method :swagger_include_warning_tags?, :swagger_include_warning_tags
alias_method :swagger_json_input_uses_refs?, :swagger_json_input_uses_refs
alias_method :swagger_responses_use_refs?, :swagger_responses_use_refs
alias_method :swagger_generate_x_computed_id_field?, :swagger_generate_x_computed_id_field

# matcher to be used in Dir.glob to find controllers to be reloaded e.g.
Expand Down Expand Up @@ -179,6 +180,7 @@ def initialize
@swagger_api_host = "localhost:3000"
@swagger_generate_x_computed_id_field = false
@swagger_allow_additional_properties_in_response = false
@swagger_responses_use_refs = true
end
end
end
26 changes: 26 additions & 0 deletions lib/apipie/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,30 @@ def to_s
"Invalid parameter '#{@param}' value #{@value.inspect}: #{@error}"
end
end

class ResponseDoesNotMatchSwaggerSchema < Error
def initialize(controller_name, method_name, response_code, error_messages, schema, returned_object)
@controller_name = controller_name
@method_name = method_name
@response_code = response_code
@error_messages = error_messages
@schema = schema
@returned_object = returned_object
end

def to_s
"Response does not match swagger schema (#{@controller_name}##{@method_name} #{@response_code}): #{@error_messages}\nSchema: #{JSON(@schema)}\nReturned object: #{@returned_object}"
end
end

class NoDocumentedMethod < Error
def initialize(controller_name, method_name)
@method_name = method_name
@controller_name = controller_name
end

def to_s
"There is no documented method #{@controller_name}##{@method_name}"
end
end
end
12 changes: 9 additions & 3 deletions lib/apipie/response_description.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ class ResponseObject
include Apipie::DSL::Base
include Apipie::DSL::Param

attr_accessor :additional_properties
attr_accessor :additional_properties, :typename

def initialize(method_description, scope, block)
def initialize(method_description, scope, block, typename)
@method_description = method_description
@scope = scope
@param_group = {scope: scope}
@additional_properties = false
@typename = typename

self.instance_exec(&block) if block

Expand Down Expand Up @@ -67,6 +68,11 @@ def is_array?
@is_array_of != false
end

def typename
@response_object.typename
end


def initialize(method_description, code, options, scope, block, adapter)

@type_ref = options[:param_group]
Expand All @@ -93,7 +99,7 @@ def initialize(method_description, code, options, scope, block, adapter)
if adapter
@response_object = adapter
else
@response_object = ResponseObject.new(method_description, scope, block)
@response_object = ResponseObject.new(method_description, scope, block, @type_ref)
end

@response_object.additional_properties ||= options[:additional_properties]
Expand Down
7 changes: 4 additions & 3 deletions lib/apipie/response_description_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,19 @@ def validator
class ResponseDescriptionAdapter

def self.from_self_describing_class(cls)
adapter = ResponseDescriptionAdapter.new
adapter = ResponseDescriptionAdapter.new(cls.to_s)
props = cls.describe_own_properties
adapter.add_property_descriptions(props)
adapter
end

def initialize
def initialize(typename)
@property_descs = []
@additional_properties = false
@typename = typename
end

attr_accessor :additional_properties
attr_accessor :additional_properties, :typename

def allow_additional_properties
additional_properties
Expand Down
Loading