committee 5.6.3 → 5.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/committee/middleware/options/response_validation.rb +4 -2
- data/lib/committee/middleware/response_validation.rb +18 -5
- data/lib/committee/schema_validator/hyper_schema/response_validator.rb +1 -1
- data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +37 -26
- data/lib/committee/schema_validator/open_api_3/response_validator.rb +2 -1
- data/lib/committee/schema_validator/open_api_3.rb +4 -0
- data/lib/committee/schema_validator/option.rb +2 -1
- data/lib/committee/test/methods.rb +1 -1
- data/lib/committee/version.rb +1 -1
- data/test/middleware/options/response_validation_test.rb +15 -0
- data/test/middleware/request_validation_open_api_3_test.rb +2 -2
- data/test/middleware/response_validation_open_api_3_test.rb +85 -0
- data/test/schema_validator/hyper_schema/response_validator_test.rb +16 -0
- data/test/schema_validator/open_api_3/operation_wrapper_test.rb +95 -0
- data/test/schema_validator/open_api_3_test.rb +27 -0
- data/test/test/methods_test.rb +12 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59078886ccd7f7a330d9249a0804394504f31e4db513a71f1b0226c4445c5873
|
|
4
|
+
data.tar.gz: 1029ac7aa696d256bf8c9e588c24aa4c39e2a8ac906481a78d89f6d9490a5222
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce35b10aa816fd3514674a710e2ac6b71164bb00a483de716efa66bc5a3fef64e1c566e9535372fd3971c0ec00989cc95b746cdd04892344f375e181b7c35e05
|
|
7
|
+
data.tar.gz: 7f4adcf8fd1e0c8113b49b9f09f67425219f796f8ea4f34f2e94704b23d7b9e2c932fce33d9d40f261b1b1183b441fce527871940adacb7d26643d78cf615c7d
|
|
@@ -10,9 +10,10 @@ module Committee
|
|
|
10
10
|
attr_reader :parse_response_by_content_type
|
|
11
11
|
attr_reader :coerce_response_values
|
|
12
12
|
attr_reader :streaming_content_parsers
|
|
13
|
+
attr_reader :strict_response_content_type
|
|
13
14
|
|
|
14
15
|
# Default values
|
|
15
|
-
DEFAULTS = { strict: false, validate_success_only: true, parse_response_by_content_type: true, coerce_response_values: false }.freeze
|
|
16
|
+
DEFAULTS = { strict: false, validate_success_only: true, parse_response_by_content_type: true, coerce_response_values: false, strict_response_content_type: false }.freeze
|
|
16
17
|
|
|
17
18
|
def initialize(options = {})
|
|
18
19
|
super(options)
|
|
@@ -22,6 +23,7 @@ module Committee
|
|
|
22
23
|
@validate_success_only = options.fetch(:validate_success_only, DEFAULTS[:validate_success_only])
|
|
23
24
|
@parse_response_by_content_type = options.fetch(:parse_response_by_content_type, DEFAULTS[:parse_response_by_content_type])
|
|
24
25
|
@coerce_response_values = options.fetch(:coerce_response_values, DEFAULTS[:coerce_response_values])
|
|
26
|
+
@strict_response_content_type = options.fetch(:strict_response_content_type, DEFAULTS[:strict_response_content_type])
|
|
25
27
|
|
|
26
28
|
# Streaming
|
|
27
29
|
@streaming_content_parsers = options[:streaming_content_parsers] || {}
|
|
@@ -38,7 +40,7 @@ module Committee
|
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
def build_hash
|
|
41
|
-
super.merge(strict: @strict, validate_success_only: @validate_success_only, parse_response_by_content_type: @parse_response_by_content_type, coerce_response_values: @coerce_response_values, streaming_content_parsers: @streaming_content_parsers)
|
|
43
|
+
super.merge(strict: @strict, validate_success_only: @validate_success_only, parse_response_by_content_type: @parse_response_by_content_type, coerce_response_values: @coerce_response_values, streaming_content_parsers: @streaming_content_parsers, strict_response_content_type: @strict_response_content_type)
|
|
42
44
|
end
|
|
43
45
|
end
|
|
44
46
|
end
|
|
@@ -28,8 +28,19 @@ module Committee
|
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
else
|
|
31
|
+
validator = response_validator(request, status)
|
|
32
|
+
if validator
|
|
33
|
+
original_response = response
|
|
34
|
+
response = []
|
|
35
|
+
begin
|
|
36
|
+
original_response.each { |chunk| response << chunk }
|
|
37
|
+
ensure
|
|
38
|
+
original_response.close if original_response.respond_to?(:close)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
31
42
|
begin
|
|
32
|
-
|
|
43
|
+
validator&.response_validate(status, headers, response, @strict)
|
|
33
44
|
rescue Committee::InvalidResponse
|
|
34
45
|
handle_exception($!, request.env)
|
|
35
46
|
|
|
@@ -68,10 +79,12 @@ module Committee
|
|
|
68
79
|
end
|
|
69
80
|
|
|
70
81
|
def validate(request, status, headers, response, streaming_content_parser = nil)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
response_validator(request, status)&.response_validate(status, headers, response, @strict, streaming_content_parser)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def response_validator(request, status)
|
|
86
|
+
validator = build_schema_validator(request)
|
|
87
|
+
validator if validator.link_exist? && self.class.validate?(status, validate_success_only)
|
|
75
88
|
end
|
|
76
89
|
|
|
77
90
|
def retrieve_streaming_content_parser(headers)
|
|
@@ -15,7 +15,7 @@ module Committee
|
|
|
15
15
|
@validators = {}
|
|
16
16
|
if link.is_a? Drivers::OpenAPI2::Link
|
|
17
17
|
link.target_schemas.each do |status, schema|
|
|
18
|
-
@validators[status] = JsonSchema::Validator.new(
|
|
18
|
+
@validators[status] = JsonSchema::Validator.new(schema)
|
|
19
19
|
end
|
|
20
20
|
else
|
|
21
21
|
@validator = JsonSchema::Validator.new(target_schema(link))
|
|
@@ -32,20 +32,34 @@ module Committee
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# @param [Boolean] strict when not content_type or status code definition, raise error
|
|
35
|
-
def validate_response_params(status_code, headers, response_data, strict, check_header, validator_options: {})
|
|
35
|
+
def validate_response_params(status_code, headers, response_data, strict, check_header, strict_response_content_type: false, validator_options: {})
|
|
36
36
|
response_body = OpenAPIParser::RequestOperation::ValidatableResponseBody.new(status_code, response_data, headers)
|
|
37
37
|
|
|
38
|
+
# When strict_response_content_type is enabled, reject responses whose Content-Type is not
|
|
39
|
+
# declared in the spec's content map for this status code. Responses with no content map
|
|
40
|
+
# (e.g. bare 204s) are skipped — openapi_parser's validate_response_body handles those.
|
|
41
|
+
if strict_response_content_type
|
|
42
|
+
response_object = find_response_object_for_status(request_operation.operation_object&.responses, status_code)
|
|
43
|
+
if response_object
|
|
44
|
+
content_type = Rack::MediaType.type(response_body.content_type)
|
|
45
|
+
matched = response_object.select_media_type(content_type)
|
|
46
|
+
if matched.nil? && response_object.content && !response_object.content.empty?
|
|
47
|
+
raise Committee::InvalidResponse, "Response Content-Type '#{content_type}' is not declared in the OpenAPI spec for this operation. Declared types: #{response_object.content.keys.join(', ')}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
38
52
|
return request_operation.validate_response_body(response_body, response_validate_options(strict, check_header, validator_options: validator_options))
|
|
39
53
|
rescue OpenAPIParser::OpenAPIError => e
|
|
40
54
|
raise Committee::InvalidResponse.new(e.message, original_error: e)
|
|
41
55
|
end
|
|
42
56
|
|
|
43
|
-
def validate_request_params(
|
|
57
|
+
def validate_request_params(_path_params, query_params, body_params, headers, validator_option)
|
|
44
58
|
ret, err = case request_operation.http_method
|
|
45
59
|
when 'get', 'delete', 'head'
|
|
46
|
-
validate_get_request_params(
|
|
60
|
+
validate_get_request_params(query_params, headers, validator_option)
|
|
47
61
|
when 'post', 'put', 'patch', 'options'
|
|
48
|
-
validate_post_request_params(
|
|
62
|
+
validate_post_request_params(query_params, body_params, headers, validator_option)
|
|
49
63
|
else
|
|
50
64
|
raise "Committee OpenAPI3 not support #{request_operation.http_method} method"
|
|
51
65
|
end
|
|
@@ -112,42 +126,27 @@ module Committee
|
|
|
112
126
|
OpenAPIParser::SchemaValidator::Options.new(**parser_options)
|
|
113
127
|
end
|
|
114
128
|
|
|
115
|
-
def validate_get_request_params(
|
|
116
|
-
|
|
117
|
-
validate_path_and_query_params(path_params, query_params, headers, validator_option)
|
|
129
|
+
def validate_get_request_params(query_params, headers, validator_option)
|
|
130
|
+
validate_query_params(query_params, headers, validator_option)
|
|
118
131
|
rescue OpenAPIParser::OpenAPIError => e
|
|
119
132
|
raise Committee::InvalidRequest.new(e.message, original_error: e)
|
|
120
133
|
end
|
|
121
134
|
|
|
122
|
-
def validate_post_request_params(
|
|
135
|
+
def validate_post_request_params(query_params, body_params, headers, validator_option)
|
|
123
136
|
content_type_key = headers.keys.detect { |k| k.casecmp?('Content-Type') }
|
|
124
137
|
content_type = Rack::MediaType.type(headers[content_type_key])
|
|
125
138
|
|
|
126
|
-
|
|
127
|
-
validate_path_and_query_params(path_params, query_params, headers, validator_option)
|
|
139
|
+
validate_query_params(query_params, headers, validator_option)
|
|
128
140
|
request_operation.validate_request_body(content_type, body_params, build_openapi_parser_body_option(validator_option))
|
|
129
141
|
rescue => e
|
|
130
142
|
raise Committee::InvalidRequest.new(e.message, original_error: e)
|
|
131
143
|
end
|
|
132
144
|
|
|
133
|
-
def
|
|
134
|
-
path_params ||= {}
|
|
145
|
+
def validate_query_params(query_params, headers, validator_option)
|
|
135
146
|
query_params ||= {}
|
|
136
147
|
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
path_keys = path_params.keys.to_set
|
|
141
|
-
query_keys = query_params.keys.to_set
|
|
142
|
-
|
|
143
|
-
merged_params = query_params.merge(path_params)
|
|
144
|
-
|
|
145
|
-
request_operation.validate_request_parameter(merged_params, headers, build_openapi_parser_request_parameter_option(validator_option))
|
|
146
|
-
|
|
147
|
-
merged_params.each do |k, v|
|
|
148
|
-
path_params[k] = v if path_keys.include?(k)
|
|
149
|
-
query_params[k] = v if query_keys.include?(k)
|
|
150
|
-
end
|
|
148
|
+
# Path parameters have already been validated by coerce_path_parameter.
|
|
149
|
+
request_operation.validate_request_parameter(query_params, headers, build_openapi_parser_request_parameter_option(validator_option))
|
|
151
150
|
|
|
152
151
|
validate_no_unknown_query_params(query_params) if validator_option.strict_query_params
|
|
153
152
|
end
|
|
@@ -163,6 +162,18 @@ module Committee
|
|
|
163
162
|
raise Committee::InvalidRequest.new("Unknown query parameter(s): #{unknown_params.join(', ')}")
|
|
164
163
|
end
|
|
165
164
|
|
|
165
|
+
def find_response_object_for_status(responses, status_code)
|
|
166
|
+
return nil unless responses&.response
|
|
167
|
+
|
|
168
|
+
response_hash = responses.response
|
|
169
|
+
return response_hash[status_code.to_s] if response_hash[status_code.to_s]
|
|
170
|
+
|
|
171
|
+
wild_card = "#{status_code.to_i / 100}XX"
|
|
172
|
+
return response_hash[wild_card] if response_hash[wild_card]
|
|
173
|
+
|
|
174
|
+
responses.default
|
|
175
|
+
end
|
|
176
|
+
|
|
166
177
|
def response_validate_options(strict, check_header, validator_options: {})
|
|
167
178
|
options = { strict: strict, validate_header: check_header }
|
|
168
179
|
|
|
@@ -14,6 +14,7 @@ module Committee
|
|
|
14
14
|
@check_header = validator_option.check_header
|
|
15
15
|
@allow_empty_date_and_datetime = validator_option.allow_empty_date_and_datetime
|
|
16
16
|
@coerce_response_values = validator_option.coerce_response_values
|
|
17
|
+
@strict_response_content_type = validator_option.strict_response_content_type
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def call(status, headers, response_data, strict)
|
|
@@ -21,7 +22,7 @@ module Committee
|
|
|
21
22
|
|
|
22
23
|
validator_options = { allow_empty_date_and_datetime: @allow_empty_date_and_datetime, coerce_value: @coerce_response_values }
|
|
23
24
|
|
|
24
|
-
operation_wrapper.validate_response_params(status, headers, response_data, strict, check_header, validator_options: validator_options)
|
|
25
|
+
operation_wrapper.validate_response_params(status, headers, response_data, strict, check_header, strict_response_content_type: @strict_response_content_type, validator_options: validator_options)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def validate?(status)
|
|
@@ -50,6 +50,10 @@ module Committee
|
|
|
50
50
|
!@operation_object.nil?
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# Expose the operation matched for the request, or nil if none matched
|
|
54
|
+
# @return [Committee::SchemaValidator::OpenAPI3::OperationWrapper, nil]
|
|
55
|
+
attr_reader :operation_object
|
|
56
|
+
|
|
53
57
|
private
|
|
54
58
|
|
|
55
59
|
attr_reader :validator_option
|
|
@@ -4,7 +4,7 @@ module Committee
|
|
|
4
4
|
module SchemaValidator
|
|
5
5
|
class Option
|
|
6
6
|
# Boolean Options
|
|
7
|
-
attr_reader :allow_blank_structures, :allow_empty_date_and_datetime, :allow_form_params, :allow_get_body, :allow_query_params, :allow_non_get_query_params, :check_content_type, :check_header, :coerce_date_times, :coerce_form_params, :coerce_path_params, :coerce_query_params, :coerce_recursive, :coerce_response_values, :deserialize_parameters, :optimistic_json, :validate_success_only, :parse_response_by_content_type, :parameter_overwrite_by_rails_rule, :strict_query_params
|
|
7
|
+
attr_reader :allow_blank_structures, :allow_empty_date_and_datetime, :allow_form_params, :allow_get_body, :allow_query_params, :allow_non_get_query_params, :check_content_type, :check_header, :coerce_date_times, :coerce_form_params, :coerce_path_params, :coerce_query_params, :coerce_recursive, :coerce_response_values, :deserialize_parameters, :optimistic_json, :validate_success_only, :parse_response_by_content_type, :parameter_overwrite_by_rails_rule, :strict_query_params, :strict_response_content_type
|
|
8
8
|
|
|
9
9
|
# Non-boolean options:
|
|
10
10
|
attr_reader :headers_key, :params_key, :query_hash_key, :request_body_hash_key, :path_hash_key, :prefix
|
|
@@ -32,6 +32,7 @@ module Committee
|
|
|
32
32
|
@optimistic_json = options.fetch(:optimistic_json, false)
|
|
33
33
|
@parse_response_by_content_type = options.fetch(:parse_response_by_content_type, true)
|
|
34
34
|
@strict_query_params = options.fetch(:strict_query_params, false)
|
|
35
|
+
@strict_response_content_type = options.fetch(:strict_response_content_type, false)
|
|
35
36
|
|
|
36
37
|
@parameter_overwrite_by_rails_rule =
|
|
37
38
|
if options.key?(:parameter_overwite_by_rails_rule)
|
data/lib/committee/version.rb
CHANGED
|
@@ -94,6 +94,16 @@ describe Committee::Middleware::Options::ResponseValidation do
|
|
|
94
94
|
end
|
|
95
95
|
assert_equal "streaming_content_parsers must be a Hash", e.message
|
|
96
96
|
end
|
|
97
|
+
|
|
98
|
+
it "sets strict_response_content_type option with default false" do
|
|
99
|
+
options = Committee::Middleware::Options::ResponseValidation.new(schema: hyper_schema)
|
|
100
|
+
assert_equal false, options.strict_response_content_type
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "sets strict_response_content_type option when provided" do
|
|
104
|
+
options = Committee::Middleware::Options::ResponseValidation.new(schema: hyper_schema, strict_response_content_type: true)
|
|
105
|
+
assert_equal true, options.strict_response_content_type
|
|
106
|
+
end
|
|
97
107
|
end
|
|
98
108
|
|
|
99
109
|
describe "#to_h" do
|
|
@@ -103,6 +113,11 @@ describe Committee::Middleware::Options::ResponseValidation do
|
|
|
103
113
|
assert_equal true, hash[:strict]
|
|
104
114
|
assert_equal false, hash[:validate_success_only]
|
|
105
115
|
end
|
|
116
|
+
|
|
117
|
+
it "includes strict_response_content_type in hash" do
|
|
118
|
+
options = Committee::Middleware::Options::ResponseValidation.new(schema: hyper_schema, strict_response_content_type: true)
|
|
119
|
+
assert_equal true, options.to_h[:strict_response_content_type]
|
|
120
|
+
end
|
|
106
121
|
end
|
|
107
122
|
|
|
108
123
|
describe ".from" do
|
|
@@ -424,7 +424,7 @@ describe Committee::Middleware::RequestValidation do
|
|
|
424
424
|
assert_equal env['committee.path_hash'][:integer], 84
|
|
425
425
|
assert_equal env['committee.request_body_hash']['integer'], 21
|
|
426
426
|
assert_equal env['committee.request_body_hash'][:integer], 21
|
|
427
|
-
assert_equal env['committee.query_hash']['integer'],
|
|
427
|
+
assert_equal env['committee.query_hash']['integer'], 42
|
|
428
428
|
# assert_equal env['rack.request.query_hash'][:integer], 21 # this isn't hash indifferent hash because we use rack.request.query_hash
|
|
429
429
|
[204, {}, []]
|
|
430
430
|
end, schema: open_api_3_schema, parameter_overwrite_by_rails_rule: false)
|
|
@@ -478,7 +478,7 @@ describe Committee::Middleware::RequestValidation do
|
|
|
478
478
|
assert_equal env['committee.params'][:integer], 84
|
|
479
479
|
assert_equal env['committee.request_body_hash']['integer'], 21
|
|
480
480
|
assert_equal env['committee.request_body_hash'][:integer], 21
|
|
481
|
-
assert_equal env['committee.query_hash']['integer'],
|
|
481
|
+
assert_equal env['committee.query_hash']['integer'], 42
|
|
482
482
|
assert_equal env['committee.path_hash']['integer'], 84
|
|
483
483
|
assert_equal env['committee.path_hash'][:integer], 84
|
|
484
484
|
[204, {}, []]
|
|
@@ -17,6 +17,29 @@ describe Committee::Middleware::ResponseValidation do
|
|
|
17
17
|
assert_equal 200, last_response.status
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
it "preserves a one-shot response body after validation" do
|
|
21
|
+
content = JSON.generate(CHARACTERS_RESPONSE)
|
|
22
|
+
chunks = [content]
|
|
23
|
+
each_calls = 0
|
|
24
|
+
body = Rack::BodyProxy.new(Enumerator.new do |yielder|
|
|
25
|
+
each_calls += 1
|
|
26
|
+
chunks.each { |chunk| yielder << chunk }
|
|
27
|
+
chunks.clear
|
|
28
|
+
end) {}
|
|
29
|
+
@app = Rack::Builder.new {
|
|
30
|
+
use Committee::Middleware::ResponseValidation, { schema: open_api_3_schema }
|
|
31
|
+
run ->(_) { [200, { "Content-Type" => "application/json" }, body] }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
status, _headers, response_body = @app.call(Rack::MockRequest.env_for("/characters"))
|
|
35
|
+
|
|
36
|
+
assert_equal 200, status
|
|
37
|
+
assert_equal content, response_body.each.to_a.join
|
|
38
|
+
assert_equal 1, each_calls
|
|
39
|
+
response_body.close if response_body.respond_to?(:close)
|
|
40
|
+
assert body.closed?
|
|
41
|
+
end
|
|
42
|
+
|
|
20
43
|
it "passes through a valid response with content-type (lower-case)" do
|
|
21
44
|
status = 200
|
|
22
45
|
headers = { "content-type" => "application/json" }
|
|
@@ -303,6 +326,68 @@ describe Committee::Middleware::ResponseValidation do
|
|
|
303
326
|
end
|
|
304
327
|
end
|
|
305
328
|
|
|
329
|
+
describe "strict_response_content_type option" do
|
|
330
|
+
it "passes through with undeclared content type when strict_response_content_type: false (default)" do
|
|
331
|
+
@app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), { "Content-Type" => "application/vnd.api+json" }, schema: open_api_3_schema)
|
|
332
|
+
get "/characters"
|
|
333
|
+
assert_equal 200, last_response.status
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
it "raises with undeclared content type when strict_response_content_type: true" do
|
|
337
|
+
@app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), { "Content-Type" => "application/vnd.api+json" }, schema: open_api_3_schema, strict_response_content_type: true, raise: true)
|
|
338
|
+
assert_raises(Committee::InvalidResponse) do
|
|
339
|
+
get "/characters"
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it "returns 500 with undeclared content type when strict_response_content_type: true without raise" do
|
|
344
|
+
@app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), { "Content-Type" => "application/vnd.api+json" }, schema: open_api_3_schema, strict_response_content_type: true)
|
|
345
|
+
get "/characters"
|
|
346
|
+
assert_equal 500, last_response.status
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
it "passes through when response has no content schema with strict_response_content_type: true" do
|
|
350
|
+
@app = new_response_rack("", { "Content-Type" => "application/vnd.api+json" }, schema: open_api_3_schema, strict_response_content_type: true)
|
|
351
|
+
get "/no_content_schema"
|
|
352
|
+
assert_equal 200, last_response.status
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it "passes through with declared content type and strict_response_content_type: true" do
|
|
356
|
+
@app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), { "Content-Type" => "application/json" }, schema: open_api_3_schema, strict_response_content_type: true)
|
|
357
|
+
get "/characters"
|
|
358
|
+
assert_equal 200, last_response.status
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it "passes through with declared content type and charset param with strict_response_content_type: true" do
|
|
362
|
+
@app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), { "Content-Type" => "application/json; charset=utf-8" }, schema: open_api_3_schema, strict_response_content_type: true)
|
|
363
|
+
get "/characters"
|
|
364
|
+
assert_equal 200, last_response.status
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
it "raises with undeclared content type matched via wildcard status code with strict_response_content_type: true" do
|
|
368
|
+
@app = new_response_rack("{}", { "Content-Type" => "application/vnd.api+json" }, { schema: open_api_3_schema, strict_response_content_type: true, raise: true, validate_success_only: false }, { status: 400 })
|
|
369
|
+
assert_raises(Committee::InvalidResponse) do
|
|
370
|
+
get "/wildcard_response"
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
it "raises with undeclared content type matched via default response with strict_response_content_type: true" do
|
|
375
|
+
@app = new_response_rack("{}", { "Content-Type" => "application/vnd.api+json" }, { schema: open_api_3_schema, strict_response_content_type: true, raise: true, validate_success_only: false }, { status: 500 })
|
|
376
|
+
assert_raises(Committee::InvalidResponse) do
|
|
377
|
+
get "/default_response"
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
it "calls error_handler with strict_response_content_type: true and raise: false" do
|
|
382
|
+
called_err = nil
|
|
383
|
+
pr = ->(e, _env) { called_err = e }
|
|
384
|
+
@app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), { "Content-Type" => "application/vnd.api+json" }, schema: open_api_3_schema, strict_response_content_type: true, error_handler: pr)
|
|
385
|
+
get "/characters"
|
|
386
|
+
assert_equal 500, last_response.status
|
|
387
|
+
assert_kind_of Committee::InvalidResponse, called_err
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
306
391
|
private
|
|
307
392
|
|
|
308
393
|
def new_response_rack(response, headers = {}, options = {}, rack_options = {})
|
|
@@ -112,6 +112,22 @@ describe Committee::SchemaValidator::HyperSchema::ResponseValidator do
|
|
|
112
112
|
assert_equal message, e.message
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
it "validates OpenAPI 2 responses against the schema for their status" do
|
|
116
|
+
link = Committee::Drivers::OpenAPI2::Link.new
|
|
117
|
+
link.href = "/apps"
|
|
118
|
+
link.media_type = "application/json"
|
|
119
|
+
link.status_success = 200
|
|
120
|
+
success_schema = JsonSchema.parse!({ "type" => "object", "required" => ["id"], "properties" => { "id" => { "type" => "integer" } } })
|
|
121
|
+
error_schema = JsonSchema.parse!({ "type" => "object", "required" => ["error"], "properties" => { "error" => { "type" => "string" } } })
|
|
122
|
+
link.target_schemas = { 200 => success_schema, 400 => error_schema }
|
|
123
|
+
validator = Committee::SchemaValidator::HyperSchema::ResponseValidator.new(link, validate_success_only: false)
|
|
124
|
+
|
|
125
|
+
validator.call(200, @headers, { "id" => 1 })
|
|
126
|
+
validator.call(400, @headers, { "error" => "invalid request" })
|
|
127
|
+
assert_raises(Committee::InvalidResponse) { validator.call(200, @headers, { "error" => "invalid request" }) }
|
|
128
|
+
assert_raises(Committee::InvalidResponse) { validator.call(400, @headers, { "id" => 1, "error" => 1 }) }
|
|
129
|
+
end
|
|
130
|
+
|
|
115
131
|
private
|
|
116
132
|
|
|
117
133
|
def call
|
|
@@ -156,6 +156,63 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
|
|
|
156
156
|
assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
+
describe 'same-named path and query parameters' do
|
|
160
|
+
%w[get post].each do |method|
|
|
161
|
+
describe method do
|
|
162
|
+
before do
|
|
163
|
+
@path = '/overwrite_same_parameter/123'
|
|
164
|
+
@method = method
|
|
165
|
+
data = open_api_3_data
|
|
166
|
+
path_item = data['paths']['/overwrite_same_parameter/{integer}']
|
|
167
|
+
path_item[method] = path_item['post']
|
|
168
|
+
@open_api_3_schema = Committee::Drivers.load_from_data(data, open_api_3_schema_path, parser_options: { strict_reference_validation: true })
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it 'rejects an invalid query value without overwriting it with the path value' do
|
|
172
|
+
path_params = { 'integer' => 123 }
|
|
173
|
+
query_params = { 'integer' => 'not-an-integer' }
|
|
174
|
+
|
|
175
|
+
error = assert_raises(Committee::InvalidRequest) do
|
|
176
|
+
operation_object.validate_request_params(path_params, query_params, {}, HEADER, @validator_option)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
assert_match(/expected integer, but received String: "not-an-integer"/i, error.message)
|
|
180
|
+
assert_kind_of(OpenAPIParser::OpenAPIError, error.original_error)
|
|
181
|
+
assert_equal({ 'integer' => 'not-an-integer' }, query_params)
|
|
182
|
+
assert_equal({ 'integer' => 123 }, path_params)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'rejects a missing required query parameter even when the path parameter is present' do
|
|
186
|
+
error = assert_raises(Committee::InvalidRequest) do
|
|
187
|
+
operation_object.validate_request_params({ 'integer' => 123 }, {}, {}, HEADER, @validator_option)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
assert_match(/missing required parameters: integer/i, error.message)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'coerces the query value independently of the path value' do
|
|
194
|
+
path_params = { 'integer' => 123 }
|
|
195
|
+
query_params = { 'integer' => '456' }
|
|
196
|
+
|
|
197
|
+
operation_object.validate_request_params(path_params, query_params, {}, HEADER, @validator_option)
|
|
198
|
+
|
|
199
|
+
assert_equal({ 'integer' => 123 }, path_params)
|
|
200
|
+
assert_equal({ 'integer' => 456 }, query_params)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'honors disabled query coercion even when the path value is already an integer' do
|
|
204
|
+
options = Committee::SchemaValidator::Option.new({ coerce_query_params: false }, open_api_3_schema, :open_api_3)
|
|
205
|
+
|
|
206
|
+
error = assert_raises(Committee::InvalidRequest) do
|
|
207
|
+
operation_object.validate_request_params({ 'integer' => 123 }, { 'integer' => '456' }, {}, HEADER, options)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
assert_match(/expected integer, but received String: "456"/i, error.message)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
159
216
|
describe '#content_types' do
|
|
160
217
|
it 'returns supported content types' do
|
|
161
218
|
@path = '/validate_content_types'
|
|
@@ -223,5 +280,43 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
|
|
|
223
280
|
assert_kind_of(Integer, body_params["integer"])
|
|
224
281
|
end
|
|
225
282
|
end
|
|
283
|
+
|
|
284
|
+
describe '#find_response_object_for_status' do
|
|
285
|
+
def responses_for(path, method = 'get')
|
|
286
|
+
open_api_3_schema.operation_object(path, method).request_operation.operation_object.responses
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def find(path, status, method = 'get')
|
|
290
|
+
wrapper = open_api_3_schema.operation_object(path, method)
|
|
291
|
+
wrapper.send(:find_response_object_for_status, responses_for(path, method), status)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it 'returns nil when responses is nil' do
|
|
295
|
+
wrapper = open_api_3_schema.operation_object('/characters', 'get')
|
|
296
|
+
assert_nil wrapper.send(:find_response_object_for_status, nil, 200)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it 'returns exact status code match' do
|
|
300
|
+
result = find('/characters', 200)
|
|
301
|
+
assert_kind_of OpenAPIParser::Schemas::Response, result
|
|
302
|
+
assert result.content.key?('application/json')
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it 'returns wildcard match when exact status not defined' do
|
|
306
|
+
result = find('/wildcard_response', 400)
|
|
307
|
+
assert_kind_of OpenAPIParser::Schemas::Response, result
|
|
308
|
+
assert result.content.key?('application/json')
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it 'returns default when no exact or wildcard match' do
|
|
312
|
+
result = find('/default_response', 500)
|
|
313
|
+
assert_kind_of OpenAPIParser::Schemas::Response, result
|
|
314
|
+
assert result.content.key?('application/json')
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
it 'returns nil when no exact, wildcard, or default match' do
|
|
318
|
+
assert_nil find('/characters', 999)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
226
321
|
end
|
|
227
322
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
describe Committee::SchemaValidator::OpenAPI3 do
|
|
6
|
+
before do
|
|
7
|
+
@router = open_api_3_schema.build_router(schema: open_api_3_schema)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def validator_for(path, method)
|
|
11
|
+
request = Rack::Request.new({ "REQUEST_METHOD" => method, "PATH_INFO" => path, "rack.input" => StringIO.new("") })
|
|
12
|
+
@router.build_schema_validator(request)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe "#operation_object" do
|
|
16
|
+
it "returns the matched operation wrapper" do
|
|
17
|
+
operation_object = validator_for("/validate", "POST").operation_object
|
|
18
|
+
|
|
19
|
+
assert_kind_of Committee::SchemaValidator::OpenAPI3::OperationWrapper, operation_object
|
|
20
|
+
assert_equal "/validate", operation_object.original_path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "returns nil when the request matches no operation" do
|
|
24
|
+
assert_nil validator_for("/no-such-path", "GET").operation_object
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/test/test/methods_test.rb
CHANGED
|
@@ -123,6 +123,18 @@ describe Committee::Test::Methods do
|
|
|
123
123
|
assert_schema_conform(200)
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
it "uses each request's operation for consecutive responses" do
|
|
127
|
+
responses = { "/characters" => JSON.generate(@correct_response), "/validate_response_array" => JSON.generate(["honoka"]), }
|
|
128
|
+
@app = Rack::Builder.new {
|
|
129
|
+
run ->(env) { [200, { "Content-Type" => "application/json" }, [responses.fetch(env["PATH_INFO"])]] }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get "/characters"
|
|
133
|
+
assert_schema_conform(200)
|
|
134
|
+
get "/validate_response_array"
|
|
135
|
+
assert_schema_conform(200)
|
|
136
|
+
end
|
|
137
|
+
|
|
126
138
|
it "detects an invalid response Content-Type" do
|
|
127
139
|
@app = new_rack_app(JSON.generate([@correct_response]), {})
|
|
128
140
|
get "/characters"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: committee
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.6.
|
|
4
|
+
version: 5.6.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brandur
|
|
@@ -249,6 +249,7 @@ files:
|
|
|
249
249
|
- test/schema_validator/open_api_3/parameter_deserializer_test.rb
|
|
250
250
|
- test/schema_validator/open_api_3/request_validator_test.rb
|
|
251
251
|
- test/schema_validator/open_api_3/response_validator_test.rb
|
|
252
|
+
- test/schema_validator/open_api_3_test.rb
|
|
252
253
|
- test/schema_validator_test.rb
|
|
253
254
|
- test/test/methods_new_version_test.rb
|
|
254
255
|
- test/test/methods_test.rb
|
|
@@ -277,7 +278,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
277
278
|
- !ruby/object:Gem::Version
|
|
278
279
|
version: '0'
|
|
279
280
|
requirements: []
|
|
280
|
-
rubygems_version:
|
|
281
|
+
rubygems_version: 4.0.3
|
|
281
282
|
specification_version: 4
|
|
282
283
|
summary: A collection of Rack middleware to support JSON Schema.
|
|
283
284
|
test_files: []
|