diff --git a/CHANGELOG.md b/CHANGELOG.md index 7178ca8a..81c9f954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 68855ddc..0a56b4e7 100644 --- a/README.md +++ b/README.md @@ -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. | diff --git a/lib/committee/schema_validator/hyper_schema.rb b/lib/committee/schema_validator/hyper_schema.rb index af8bd0b1..1419b01e 100644 --- a/lib/committee/schema_validator/hyper_schema.rb +++ b/lib/committee/schema_validator/hyper_schema.rb @@ -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? diff --git a/lib/committee/schema_validator/hyper_schema/response_validator.rb b/lib/committee/schema_validator/hyper_schema/response_validator.rb index 44a3d03d..d396219c 100644 --- a/lib/committee/schema_validator/hyper_schema/response_validator.rb +++ b/lib/committee/schema_validator/hyper_schema/response_validator.rb @@ -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 @@ -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 + raise e end end diff --git a/lib/committee/schema_validator/option.rb b/lib/committee/schema_validator/option.rb index 72c773d9..8b2fae77 100644 --- a/lib/committee/schema_validator/option.rb +++ b/lib/committee/schema_validator/option.rb @@ -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, @@ -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) diff --git a/test/data/openapi2/petstore-expanded.json b/test/data/openapi2/petstore-expanded.json index 06bea91e..fbb34170 100644 --- a/test/data/openapi2/petstore-expanded.json +++ b/test/data/openapi2/petstore-expanded.json @@ -184,6 +184,17 @@ } } } + }, + "/pets/cat": { + "get": { + "description": "Returns pets which are cats", + "operationId": "find pets which are cats", + "responses": { + "200": { + "description": "empty schema" + } + } + } } }, "definitions": { diff --git a/test/middleware/response_validation_test.rb b/test/middleware/response_validation_test.rb index 3b0bde52..c5d743df 100644 --- a/test/middleware/response_validation_test.rb +++ b/test/middleware/response_validation_test.rb @@ -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"