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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 694da9f92c8670bbe39fa64e209c43da029c58262ebbc249cff4512dee7e7aed
4
- data.tar.gz: 2c3ffa46d5c827471545b585421a45e5c7781b9d1ddb1d861ba9bfd12a1fddd8
3
+ metadata.gz: 59078886ccd7f7a330d9249a0804394504f31e4db513a71f1b0226c4445c5873
4
+ data.tar.gz: 1029ac7aa696d256bf8c9e588c24aa4c39e2a8ac906481a78d89f6d9490a5222
5
5
  SHA512:
6
- metadata.gz: fc906473afe3beef06fe6f34b96ad3c29566761a1806dff3ed5fabe5d5b477bdbebb49216b5039e533316e0271957a30812f791c5e4ea8a306cf8613abdb8772
7
- data.tar.gz: ffa5ad6a180270f703d13fa2dacebe534c0a9c76bc5be9d4a27eac1f07e9592813bfa81d74712b63d5ff9502ebfde97cd171cca3abb2376f4f2a9865d4f11614
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
- validate(request, status, headers, response)
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
- v = build_schema_validator(request)
72
- if v.link_exist? && self.class.validate?(status, validate_success_only)
73
- v.response_validate(status, headers, response, @strict, streaming_content_parser)
74
- end
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(target_schema(link))
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(path_params, query_params, body_params, headers, validator_option)
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(path_params, query_params, headers, validator_option)
60
+ validate_get_request_params(query_params, headers, validator_option)
47
61
  when 'post', 'put', 'patch', 'options'
48
- validate_post_request_params(path_params, query_params, body_params, headers, validator_option)
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(path_params, query_params, headers, validator_option)
116
- # bad performance because when we coerce value, same check
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(path_params, query_params, body_params, headers, validator_option)
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
- # bad performance because when we coerce value, same check
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 validate_path_and_query_params(path_params, query_params, headers, validator_option)
134
- path_params ||= {}
145
+ def validate_query_params(query_params, headers, validator_option)
135
146
  query_params ||= {}
136
147
 
137
- # it's currently impossible to validate path params and query params separately
138
- # so we have to resort to this workaround
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)
@@ -77,7 +77,7 @@ module Committee
77
77
  end
78
78
 
79
79
  def schema_validator
80
- @schema_validator ||= router.build_schema_validator(request_object)
80
+ router.build_schema_validator(request_object)
81
81
  end
82
82
 
83
83
  def schema_coverage
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Committee
4
- VERSION = '5.6.3'.freeze
4
+ VERSION = '5.6.4'.freeze
5
5
  end
@@ -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'], 84 # we can't use query_parameter :(
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'], 84 # we can't use query_parameter :(
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
@@ -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.3
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: 3.6.9
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: []