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

Add option to permit validation when schema and data are empty #417

Merged
merged 4 commits into from
May 13, 2024
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `allow_blank_structures` option #417
- Allow Empty Response Body. supported on Hyper-schema parser but will default to true in next major version.

## [5.2.0] - 2024-05-04
- Error explicitly that OpenAPI 3.1+ isn't supported #418

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This piece of middleware validates the parameters of incoming requests to make s

| name | Hyper-Schema | OpenAPI 3 | Description |
|-----------:|------------:|------------:| :------------ |
|allow_blank_structures | false | always true | Allow Empty Response Body. supported on Hyper-schema parser but will default to true in next major version. |
|allow_form_params | true | true | Specifies that input can alternatively be specified as `application/x-www-form-urlencoded` parameters when possible. This won't work for more complex schema validations. |
|allow_get_body | true | false | Allow GET request body, which merge to request parameter. See (#211) |
|allow_query_params | true | true | Specifies that query string parameters will be taken into consideration when doing validation. |
Expand Down
2 changes: 1 addition & 1 deletion lib/committee/schema_validator/hyper_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def response_validate(status, headers, response, _test_method = false)
data = JSON.parse(full_body) if parse_to_json
end

Committee::SchemaValidator::HyperSchema::ResponseValidator.new(link, validate_success_only: validator_option.validate_success_only).call(status, headers, data)
Committee::SchemaValidator::HyperSchema::ResponseValidator.new(link, validate_success_only: validator_option.validate_success_only, allow_blank_structures: validator_option.allow_blank_structures).call(status, headers, data)
end

def link_exist?
Expand Down
18 changes: 14 additions & 4 deletions lib/committee/schema_validator/hyper_schema/response_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ module Committee
module SchemaValidator
class HyperSchema
class ResponseValidator
attr_reader :validate_success_only
attr_reader :allow_blank_structures, :validate_success_only

def initialize(link, options = {})
@link = link
@validate_success_only = options[:validate_success_only]
@allow_blank_structures = options[:allow_blank_structures]

@validator = JsonSchema::Validator.new(target_schema(link))
end
Expand Down Expand Up @@ -39,9 +40,18 @@ def call(status, headers, data)
return if data == nil
end

if Committee::Middleware::ResponseValidation.validate?(status, validate_success_only) && !@validator.validate(data)
errors = JsonSchema::SchemaError.aggregate(@validator.errors).join("\n")
raise InvalidResponse, "Invalid response.\n\n#{errors}"
if allow_blank_structures && @link.is_a?(Committee::Drivers::OpenAPI2::Link) && !@link.target_schema
return if data.nil?
end

begin
if Committee::Middleware::ResponseValidation.validate?(status, validate_success_only) && !@validator.validate(data)
errors = JsonSchema::SchemaError.aggregate(@validator.errors).join("\n")
raise InvalidResponse, "Invalid response.\n\n#{errors}"
end
rescue => e
raise InvalidResponse, "Invalid response.\n\nschema is undefined" if /undefined method .all_of. for nil/ =~ e.message
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ruby 3.2.2

NoMethodError: undefined method `all_of' for nil:NilClass

ruby 3.3.0

NoMethodError: undefined method `all_of' for nil

ruby head

NoMethodError: undefined method 'all_of' for nil

raise e
end
end

Expand Down
4 changes: 3 additions & 1 deletion lib/committee/schema_validator/option.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ module Committee
module SchemaValidator
class Option
# Boolean Options
attr_reader :allow_form_params,
attr_reader :allow_blank_structures,
:allow_form_params,
:allow_get_body,
:allow_query_params,
:check_content_type,
Expand Down Expand Up @@ -38,6 +39,7 @@ def initialize(options, schema, schema_type)
@prefix = options[:prefix]

# Boolean options and have a common value by default
@allow_blank_structures = options.fetch(:allow_blank_structures, false)
@allow_form_params = options.fetch(:allow_form_params, true)
@allow_query_params = options.fetch(:allow_query_params, true)
@check_content_type = options.fetch(:check_content_type, true)
Expand Down
11 changes: 11 additions & 0 deletions test/data/openapi2/petstore-expanded.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@
}
}
}
},
"/pets/cat": {
"get": {
"description": "Returns pets which are cats",
"operationId": "find pets which are cats",
"responses": {
"200": {
"description": "empty schema"
Comment on lines +192 to +194
Copy link
Member

Choose a reason for hiding this comment

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

imo) 204 No Content I think the response body is empty as a specification. So why not make the test data follow the common use case?

Suggested change
"responses": {
"200": {
"description": "empty schema"
"responses": {
"204": {
"description": "The resource was deleted successfully."

Refs: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204#compatibility_notes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In my recent experiences, I've noticed the following scenarios:

  1. status_code=200 && empty response
    => This PR addresses this case. While it's technically correct to use status_code=204, there are instances where APIs are operated with 200, and ideally, I'd like to accommodate this case as well.

  2. status_code=204 && empty response
    => While it's being skipped here for 204, it seemed like a better approach to modify it to skip within Committee::SchemaValidator::HyperSchema::ResponseValidator. If it's not an inconvenience, I'm considering addressing this in a separate PR. Could you please share your thoughts on this?

Copy link
Member

Choose a reason for hiding this comment

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

Thank you for your detailed explanation!
I understood the intent and thought it make sense. So I agree with your opinion.

}
}
}
}
},
"definitions": {
Expand Down
23 changes: 23 additions & 0 deletions test/middleware/response_validation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,29 @@ def app
assert_equal 200, last_response.status
end

it "passes through a valid response for OpenAPI when data=nil, target_schema=empty, allow_blank_structures=true" do
@app = new_rack_app("null", {},
allow_blank_structures: true, schema: open_api_2_schema)
get "/api/pets/cat"
assert_equal 200, last_response.status
end

it "invalid responses for OpenAPI when data=nil, target_schema=empty, allow_blank_structures=false" do
@app = new_rack_app("null", {},
allow_blank_structures: false, schema: open_api_2_schema)
get "/api/pets/cat"
assert_equal 500, last_response.status
assert_match(/Invalid response/i, last_response.body)
end

it "passes through a valid response for OpenAPI when data=nil, target_schema=present, allow_blank_structures=true" do
@app = new_rack_app("null", {},
allow_blank_structures: true, schema: open_api_2_schema)
get "/api/pets/dog"
assert_equal 500, last_response.status
assert_match(/nil is not an array/i, last_response.body)
end

it "detects an invalid response for OpenAPI" do
@app = new_rack_app("{_}", {}, schema: open_api_2_schema)
get "/api/pets"
Expand Down