committee 3.1.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 169dfa8f079626447ca5086429c0bd025d8180f3ef5b7a7194d1333bdeff8fab
4
- data.tar.gz: 2c8eeece5b411266f482808b7caf08d2de285f64e78e705990a4b7b9606902d3
3
+ metadata.gz: 9bef22bebc103d1bee78ba294078cd56e34e37da3e92b5cb89aba683a2361c74
4
+ data.tar.gz: 76f012e3f28bd5275af941a372563f00b3bf68e1c0332d0e78affc18cbac9f2e
5
5
  SHA512:
6
- metadata.gz: b891ea4d1498b2fc16f0d241520486736b0b707bf401e96bdab09795ff165c23d86344a2b29cfcf1dbca83d8a773b85a08e59dc09220c5acca2738e485c3994f
7
- data.tar.gz: ca657dfb18edaf157cb01ca48b522882c165a8c06216c84deda9a88a7718fb61da4ea3a3b017e4ac8594e83f97f904d4f0f1ad1947ee9418b977ddf4884e8c52
6
+ metadata.gz: da601220eca1ea05bb829a61a65bf6928262be27c73647de756f44b07329752efe6818be1a79b95bb51ad2e00025d08ff5fbb5410acfdc670d43f6816c6614f1
7
+ data.tar.gz: c0fad32bef922020708c194065d0de90fccc9d33d00354b4c8a4f3208e18b4720030e01810e798107894e6f503e8145fef11fd12afa1ba1f0b5b017f3b58eeda
@@ -21,14 +21,14 @@ module Committee
21
21
  # @param [String] schema_path
22
22
  # @return [Committee::Driver]
23
23
  def self.load_from_json(schema_path)
24
- load_from_data(JSON.parse(File.read(schema_path)))
24
+ load_from_data(JSON.parse(File.read(schema_path)), schema_path)
25
25
  end
26
26
 
27
27
  # load and build drive from YAML file
28
28
  # @param [String] schema_path
29
29
  # @return [Committee::Driver]
30
30
  def self.load_from_yaml(schema_path)
31
- load_from_data(YAML.load_file(schema_path))
31
+ load_from_data(YAML.load_file(schema_path), schema_path)
32
32
  end
33
33
 
34
34
  # load and build drive from file
@@ -48,10 +48,10 @@ module Committee
48
48
  # load and build drive from Hash object
49
49
  # @param [Hash] hash
50
50
  # @return [Committee::Driver]
51
- def self.load_from_data(hash)
51
+ def self.load_from_data(hash, schema_path = nil)
52
52
  if hash['openapi']&.start_with?('3.0.')
53
- parser = OpenAPIParser.parse(hash)
54
- return Committee::Drivers::OpenAPI3::Driver.new.parse(parser)
53
+ openapi = OpenAPIParser.parse_with_filepath(hash, schema_path)
54
+ return Committee::Drivers::OpenAPI3::Driver.new.parse(openapi)
55
55
  end
56
56
 
57
57
  driver = if hash['swagger'] == '2.0'
@@ -8,17 +8,19 @@ module Committee
8
8
 
9
9
  @error_class = options.fetch(:error_class, Committee::ValidationError)
10
10
  @error_handler = options[:error_handler]
11
+ @ignore_error = options.fetch(:ignore_error, false)
11
12
 
12
13
  @raise = options[:raise]
13
14
  @schema = self.class.get_schema(options)
14
15
 
15
16
  @router = @schema.build_router(options)
17
+ @accept_request_filter = options[:accept_request_filter] || -> (_) { true }
16
18
  end
17
19
 
18
20
  def call(env)
19
21
  request = Rack::Request.new(env)
20
22
 
21
- if @router.includes_request?(request)
23
+ if @router.includes_request?(request) && @accept_request_filter.call(request)
22
24
  handle(request)
23
25
  else
24
26
  @app.call(request.env)
@@ -13,27 +13,25 @@ module Committee
13
13
  end
14
14
 
15
15
  def handle(request)
16
- schema_validator = build_schema_validator(request)
17
- schema_validator.request_validate(request)
18
-
19
- raise Committee::NotFound, "That request method and path combination isn't defined." if !schema_validator.link_exist? && @strict
16
+ begin
17
+ schema_validator = build_schema_validator(request)
18
+ schema_validator.request_validate(request)
19
+
20
+ raise Committee::NotFound, "That request method and path combination isn't defined." if !schema_validator.link_exist? && @strict
21
+ rescue Committee::BadRequest, Committee::InvalidRequest
22
+ handle_exception($!, request.env)
23
+ raise if @raise
24
+ return @error_class.new(400, :bad_request, $!.message).render unless @ignore_error
25
+ rescue Committee::NotFound => e
26
+ raise if @raise
27
+ return @error_class.new(404, :not_found, e.message).render unless @ignore_error
28
+ rescue JSON::ParserError
29
+ handle_exception($!, request.env)
30
+ raise Committee::InvalidRequest if @raise
31
+ return @error_class.new(400, :bad_request, "Request body wasn't valid JSON.").render unless @ignore_error
32
+ end
20
33
 
21
34
  @app.call(request.env)
22
- rescue Committee::BadRequest, Committee::InvalidRequest
23
- handle_exception($!, request.env)
24
- raise if @raise
25
- @error_class.new(400, :bad_request, $!.message).render
26
- rescue Committee::NotFound => e
27
- raise if @raise
28
- @error_class.new(
29
- 404,
30
- :not_found,
31
- e.message
32
- ).render
33
- rescue JSON::ParserError
34
- handle_exception($!, request.env)
35
- raise Committee::InvalidRequest if @raise
36
- @error_class.new(400, :bad_request, "Request body wasn't valid JSON.").render
37
35
  end
38
36
 
39
37
  private
@@ -11,22 +11,25 @@ module Committee
11
11
  end
12
12
 
13
13
  def handle(request)
14
- status, headers, response = @app.call(request.env)
14
+ begin
15
+ status, headers, response = @app.call(request.env)
15
16
 
16
- v = build_schema_validator(request)
17
- v.response_validate(status, headers, response) if v.link_exist? && self.class.validate?(status, validate_success_only)
17
+ v = build_schema_validator(request)
18
+ v.response_validate(status, headers, response) if v.link_exist? && self.class.validate?(status, validate_success_only)
18
19
 
19
- [status, headers, response]
20
- rescue Committee::InvalidResponse
21
- handle_exception($!, request.env)
20
+ rescue Committee::InvalidResponse
21
+ handle_exception($!, request.env)
22
+
23
+ raise if @raise
24
+ return @error_class.new(500, :invalid_response, $!.message).render unless @ignore_error
25
+ rescue JSON::ParserError
26
+ handle_exception($!, request.env)
22
27
 
23
- raise if @raise
24
- @error_class.new(500, :invalid_response, $!.message).render
25
- rescue JSON::ParserError
26
- handle_exception($!, request.env)
28
+ raise Committee::InvalidResponse if @raise
29
+ return @error_class.new(500, :invalid_response, "Response wasn't valid JSON.").render unless @ignore_error
30
+ end
27
31
 
28
- raise Committee::InvalidResponse if @raise
29
- @error_class.new(500, :invalid_response, "Response wasn't valid JSON.").render
32
+ [status, headers, response]
30
33
  end
31
34
 
32
35
  class << self
@@ -35,7 +35,14 @@ module Committee
35
35
  response.each do |chunk|
36
36
  full_body << chunk
37
37
  end
38
- data = full_body.empty? ? {} : JSON.parse(full_body)
38
+
39
+ data = {}
40
+ unless full_body.empty?
41
+ parse_to_json = !validator_option.parse_response_by_content_type ||
42
+ headers.fetch('Content-Type', nil)&.start_with?('application/json')
43
+ data = JSON.parse(full_body) if parse_to_json
44
+ end
45
+
39
46
  Committee::SchemaValidator::HyperSchema::ResponseValidator.new(link, validate_success_only: validator_option.validate_success_only).call(status, headers, data)
40
47
  end
41
48
 
@@ -30,7 +30,14 @@ module Committee
30
30
  response.each do |chunk|
31
31
  full_body << chunk
32
32
  end
33
- data = full_body.empty? ? {} : JSON.parse(full_body)
33
+
34
+ parse_to_json = !validator_option.parse_response_by_content_type ||
35
+ headers.fetch('Content-Type', nil)&.start_with?('application/json')
36
+ data = if parse_to_json
37
+ full_body.empty? ? {} : JSON.parse(full_body)
38
+ else
39
+ full_body
40
+ end
34
41
 
35
42
  strict = test_method
36
43
  Committee::SchemaValidator::OpenAPI3::ResponseValidator.
@@ -64,6 +64,10 @@ module Committee
64
64
  end
65
65
  end
66
66
 
67
+ def request_content_types
68
+ request_operation.operation_object&.request_body&.content&.keys || []
69
+ end
70
+
67
71
  private
68
72
 
69
73
  attr_reader :request_operation
@@ -112,7 +116,9 @@ module Committee
112
116
  content_type = headers['Content-Type'].to_s.split(";").first.to_s
113
117
 
114
118
  # bad performance because when we coerce value, same check
115
- return request_operation.validate_request_body(content_type, params, build_openapi_parser_post_option(validator_option))
119
+ schema_validator_options = build_openapi_parser_post_option(validator_option)
120
+ request_operation.validate_request_parameter(params, headers, schema_validator_options)
121
+ request_operation.validate_request_body(content_type, params, schema_validator_options)
116
122
  rescue => e
117
123
  raise Committee::InvalidRequest.new(e.message)
118
124
  end
@@ -25,7 +25,17 @@ module Committee
25
25
  return true unless request.post? || request.put? || request.patch?
26
26
  return true if @operation_object.valid_request_content_type?(content_type)
27
27
 
28
- raise Committee::InvalidRequest, %{"Content-Type" request header must be set to "#{@operation_object}".}
28
+ message = if valid_content_types.size > 1
29
+ types = valid_content_types.map {|x| %{"#{x}"} }.join(', ')
30
+ %{"Content-Type" request header must be set to any of the following: [#{types}].}
31
+ else
32
+ %{"Content-Type" request header must be set to "#{valid_content_types.first}".}
33
+ end
34
+ raise Committee::InvalidRequest, message
35
+ end
36
+
37
+ def valid_content_types
38
+ @operation_object&.request_content_types
29
39
  end
30
40
  end
31
41
  end
@@ -17,7 +17,6 @@ module Committee
17
17
  def call(status, headers, response_data, strict)
18
18
  return unless Committee::Middleware::ResponseValidation.validate?(status, validate_success_only)
19
19
 
20
- #content_type = headers['Content-Type'].to_s.split(";").first.to_s
21
20
  operation_wrapper.validate_response_params(status, headers, response_data, strict, check_header)
22
21
  end
23
22
 
@@ -15,7 +15,8 @@ module Committee
15
15
  :coerce_query_params,
16
16
  :coerce_recursive,
17
17
  :optimistic_json,
18
- :validate_success_only
18
+ :validate_success_only,
19
+ :parse_response_by_content_type
19
20
 
20
21
  # Non-boolean options:
21
22
  attr_reader :headers_key,
@@ -35,6 +36,12 @@ module Committee
35
36
  @check_header = options.fetch(:check_header, true)
36
37
  @coerce_recursive = options.fetch(:coerce_recursive, true)
37
38
  @optimistic_json = options.fetch(:optimistic_json, false)
39
+ @parse_response_by_content_type = if options[:parse_response_by_content_type].nil?
40
+ Committee.warn_deprecated('Committee: please set parse_response_by_content_type = false because we\'ll change default value in next major version.')
41
+ false
42
+ else
43
+ options.fetch(:parse_response_by_content_type)
44
+ end
38
45
 
39
46
  # Boolean options and have a different value by default
40
47
  @allow_get_body = options.fetch(:allow_get_body, schema.driver.default_allow_get_body)
@@ -43,7 +43,11 @@ describe Committee::Bin::CommitteeStub, "app" do
43
43
  end
44
44
 
45
45
  def app
46
- @bin.get_app(hyper_schema, {})
46
+ options = {}
47
+ # TODO: delete when 5.0.0 released because default value changed
48
+ options[:parse_response_by_content_type] = false
49
+
50
+ @bin.get_app(hyper_schema, options)
47
51
  end
48
52
 
49
53
  it "defaults to a 404" do
@@ -103,17 +103,20 @@ describe Committee::Middleware::Base do
103
103
 
104
104
  describe 'initialize option' do
105
105
  it "schema_path option with hyper-schema" do
106
- b = Committee::Middleware::Base.new(nil, schema_path: hyper_schema_schema_path)
106
+ # TODO: delete when 5.0.0 released because default value changed
107
+ b = Committee::Middleware::Base.new(nil, schema_path: hyper_schema_schema_path, parse_response_by_content_type: false)
107
108
  assert_kind_of Committee::Drivers::HyperSchema::Schema, b.instance_variable_get(:@schema)
108
109
  end
109
110
 
110
111
  it "schema_path option with OpenAPI2" do
111
- b = Committee::Middleware::Base.new(nil, schema_path: open_api_2_schema_path)
112
+ # TODO: delete when 5.0.0 released because default value changed
113
+ b = Committee::Middleware::Base.new(nil, schema_path: open_api_2_schema_path, parse_response_by_content_type: false)
112
114
  assert_kind_of Committee::Drivers::OpenAPI2::Schema, b.instance_variable_get(:@schema)
113
115
  end
114
116
 
115
117
  it "schema_path option with OpenAPI3" do
116
- b = Committee::Middleware::Base.new(nil, schema_path: open_api_3_schema_path)
118
+ # TODO: delete when 5.0.0 released because default value changed
119
+ b = Committee::Middleware::Base.new(nil, schema_path: open_api_3_schema_path, parse_response_by_content_type: false)
117
120
  assert_kind_of Committee::Drivers::OpenAPI3::Schema, b.instance_variable_get(:@schema)
118
121
  end
119
122
  end
@@ -121,6 +124,9 @@ describe Committee::Middleware::Base do
121
124
  private
122
125
 
123
126
  def new_rack_app(options = {})
127
+ # TODO: delete when 5.0.0 released because default value changed
128
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
129
+
124
130
  Rack::Builder.new {
125
131
  use Committee::Middleware::RequestValidation, options
126
132
  run lambda { |_|
@@ -249,8 +249,7 @@ describe Committee::Middleware::RequestValidation do
249
249
  }
250
250
  post "/characters", JSON.generate(params)
251
251
  assert_equal 400, last_response.status
252
- # FIXME: when ruby 2.3 dropped, fix because ruby 2.3 return Fixnum, ruby 2.4 or later return Integer
253
- assert_match(/1 class is #{1.class}/i, last_response.body)
252
+ assert_match(/expected string, but received Integer:/i, last_response.body)
254
253
  end
255
254
 
256
255
  it "rescues JSON errors" do
@@ -279,7 +278,7 @@ describe Committee::Middleware::RequestValidation do
279
278
  header "Content-Type", "application/json"
280
279
  post "/v1/characters", JSON.generate(params)
281
280
  assert_equal 400, last_response.status
282
- assert_match(/1 class is/i, last_response.body)
281
+ assert_match(/expected string, but received Integer: /i, last_response.body)
283
282
  end
284
283
 
285
284
  it "ignores paths outside the prefix" do
@@ -321,7 +320,7 @@ describe Committee::Middleware::RequestValidation do
321
320
  get "/validate", nil
322
321
  end
323
322
 
324
- assert_match(/required parameters query_string not exist in/i, e.message)
323
+ assert_match(/missing required parameters: query_string/i, e.message)
325
324
  end
326
325
 
327
326
  it "raises error when required path parameter is invalid" do
@@ -332,7 +331,7 @@ describe Committee::Middleware::RequestValidation do
332
331
  get "/coerce_path_params/#{not_an_integer}", nil
333
332
  end
334
333
 
335
- assert_match(/is String but it's not valid integer in/i, e.message)
334
+ assert_match(/expected integer, but received String: abc/i, e.message)
336
335
  end
337
336
 
338
337
  it "optionally raises an error" do
@@ -356,7 +355,7 @@ describe Committee::Middleware::RequestValidation do
356
355
  get "/string_params_coercer", {"integer_1" => "1"}
357
356
 
358
357
  assert_equal 400, last_response.status
359
- assert_match(/1 class/i, last_response.body)
358
+ assert_match(/expected integer, but received String:/i, last_response.body)
360
359
  end
361
360
 
362
361
  it "passes through a valid request for OpenAPI3" do
@@ -375,7 +374,14 @@ describe Committee::Middleware::RequestValidation do
375
374
  get "/characters?limit=foo"
376
375
 
377
376
  assert_equal 400, last_response.status
378
- assert_match(/foo class/i, last_response.body)
377
+ assert_match(/expected integer, but received String: foo/i, last_response.body)
378
+ end
379
+
380
+ it "ignores errors when ignore_error: true" do
381
+ @app = new_rack_app(schema: open_api_3_schema, ignore_error: true)
382
+ get "/characters?limit=foo"
383
+
384
+ assert_equal 200, last_response.status
379
385
  end
380
386
 
381
387
  it "coerce string to integer" do
@@ -399,21 +405,55 @@ describe Committee::Middleware::RequestValidation do
399
405
  end
400
406
 
401
407
  describe 'check header' do
402
- it 'no required header' do
403
- @app = new_rack_app(schema: open_api_3_schema, check_header: true)
404
-
405
- get "/header"
406
-
407
- assert_equal 400, last_response.status
408
- assert_match(/required parameters integer not exist/i, last_response.body)
408
+ [
409
+ { check_header: true, description: 'valid value', value: 1, expected: { status: 200 } },
410
+ { check_header: true, description: 'missing value', value: nil, expected: { status: 400, error: 'missing required parameters: integer' } },
411
+ { check_header: true, description: 'invalid value', value: 'x', expected: { status: 400, error: 'expected integer, but received String: x' } },
412
+
413
+ { check_header: false, description: 'valid value', value: 1, expected: { status: 200 } },
414
+ { check_header: false, description: 'missing value', value: nil, expected: { status: 200 } },
415
+ { check_header: false, description: 'invalid value', value: 'x', expected: { status: 200 } },
416
+ ].each do |h|
417
+ check_header = h[:check_header]
418
+ description = h[:description]
419
+ value = h[:value]
420
+ expected = h[:expected]
421
+ describe "when #{check_header}" do
422
+ %w(get post put patch delete).each do |method|
423
+ describe method do
424
+ describe description do
425
+ it (expected[:error].nil? ? 'should pass' : 'should fail') do
426
+ @app = new_rack_app(schema: open_api_3_schema, check_header: check_header)
427
+
428
+ header 'integer', value
429
+ send(method, "/header")
430
+
431
+ assert_equal expected[:status], last_response.status
432
+ assert_match(expected[:error], last_response.body) if expected[:error]
433
+ end
434
+ end
435
+ end
436
+ end
437
+ end
409
438
  end
439
+ end
410
440
 
411
- it 'no required header but not check' do
412
- @app = new_rack_app(schema: open_api_3_schema, check_header: false)
413
-
414
- get "/header"
415
-
416
- assert_equal 200, last_response.status
441
+ describe ':accept_request_filter' do
442
+ [
443
+ { description: 'when not specified, includes everything', accept_request_filter: nil, expected: { status: 400 } },
444
+ { description: 'when predicate matches, performs validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/c') }, expected: { status: 400 } },
445
+ { description: 'when predicate does not match, skips validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/x') }, expected: { status: 200 } },
446
+ ].each do |h|
447
+ description = h[:description]
448
+ accept_request_filter = h[:accept_request_filter]
449
+ expected = h[:expected]
450
+ it description do
451
+ @app = new_rack_app(prefix: '/v1', schema: open_api_3_schema, accept_request_filter: accept_request_filter)
452
+
453
+ post 'v1/characters', JSON.generate(string_post_1: 1)
454
+
455
+ assert_equal expected[:status], last_response.status
456
+ end
417
457
  end
418
458
  end
419
459
 
@@ -426,6 +466,9 @@ describe Committee::Middleware::RequestValidation do
426
466
  end
427
467
 
428
468
  def new_rack_app_with_lambda(check_lambda, options = {})
469
+ # TODO: delete when 5.0.0 released because default value changed
470
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
471
+
429
472
  Rack::Builder.new {
430
473
  use Committee::Middleware::RequestValidation, options
431
474
  run check_lambda
@@ -296,6 +296,16 @@ describe Committee::Middleware::RequestValidation do
296
296
  assert_match(/invalid request/i, last_response.body)
297
297
  end
298
298
 
299
+ it "ignores errors when ignore_error: true" do
300
+ @app = new_rack_app(schema: hyper_schema, ignore_error: true)
301
+ header "Content-Type", "application/json"
302
+ params = {
303
+ "name" => 1
304
+ }
305
+ post "/apps", JSON.generate(params)
306
+ assert_equal 200, last_response.status
307
+ end
308
+
299
309
  it "calls error_handler (has a arg) when request is invalid" do
300
310
  called_err = nil
301
311
  pr = ->(e) { called_err = e }
@@ -480,6 +490,25 @@ describe Committee::Middleware::RequestValidation do
480
490
  assert_equal 200, last_response.status
481
491
  end
482
492
 
493
+ describe ':accept_request_filter' do
494
+ [
495
+ { description: 'when not specified, includes everything', accept_request_filter: nil, expected: { status: 400 } },
496
+ { description: 'when predicate matches, performs validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/a') }, expected: { status: 400 } },
497
+ { description: 'when predicate does not match, skips validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/x') }, expected: { status: 200 } },
498
+ ].each do |h|
499
+ description = h[:description]
500
+ accept_request_filter = h[:accept_request_filter]
501
+ expected = h[:expected]
502
+ it description do
503
+ @app = new_rack_app(prefix: '/v1', schema: hyper_schema, accept_request_filter: accept_request_filter)
504
+
505
+ post '/v1/apps', '{x:y}'
506
+
507
+ assert_equal expected[:status], last_response.status
508
+ end
509
+ end
510
+ end
511
+
483
512
  private
484
513
 
485
514
  def new_rack_app(options = {})
@@ -490,6 +519,9 @@ describe Committee::Middleware::RequestValidation do
490
519
 
491
520
 
492
521
  def new_rack_app_with_lambda(check_lambda, options = {})
522
+ # TODO: delete when 5.0.0 released because default value changed
523
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
524
+
493
525
  Rack::Builder.new {
494
526
  use Committee::Middleware::RequestValidation, options
495
527
  run check_lambda
@@ -26,6 +26,22 @@ describe Committee::Middleware::ResponseValidation do
26
26
  assert_equal "{\"id\":\"invalid_response\",\"message\":\"Response wasn't valid JSON.\"}", last_response.body
27
27
  end
28
28
 
29
+ it "passes through a invalid json with ignore_error option" do
30
+ @app = new_response_rack("not_json", {}, schema: open_api_3_schema, ignore_error: true)
31
+
32
+ get "/characters"
33
+
34
+ assert_equal 200, last_response.status
35
+ end
36
+
37
+ it "passes through a invalid json with parse_response_by_content_type option" do
38
+ @app = new_response_rack("csv response", { "Content-Type" => "test/csv"}, schema: open_api_3_schema, parse_response_by_content_type: true)
39
+
40
+ get "/csv"
41
+
42
+ assert_equal 200, last_response.status
43
+ end
44
+
29
45
  it "passes through not definition" do
30
46
  @app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), {}, schema: open_api_3_schema)
31
47
  get "/no_data"
@@ -39,7 +55,7 @@ describe Committee::Middleware::ResponseValidation do
39
55
  get "/characters"
40
56
  }
41
57
 
42
- assert_match(/class is Array but it's not valid object/i, e.message)
58
+ assert_match(/expected object, but received Array: /i, e.message)
43
59
  end
44
60
 
45
61
  it "passes through a 204 (no content) response" do
@@ -91,29 +107,59 @@ describe Committee::Middleware::ResponseValidation do
91
107
  assert_match(/valid JSON/i, last_response.body)
92
108
  end
93
109
 
94
- describe 'check header' do
95
- it 'valid type header' do
96
- @app = new_response_rack({}.to_json, {'x-limit' => 1}, schema: open_api_3_schema, raise: true)
97
-
98
- get "/header"
99
-
110
+ describe "remote schema $ref" do
111
+ it "passes through a valid response" do
112
+ @app = new_response_rack(JSON.generate({ "sample" => "value" }), {}, schema: open_api_3_schema)
113
+ get "/ref-sample"
100
114
  assert_equal 200, last_response.status
101
115
  end
102
116
 
103
- it 'invalid type header' do
104
- @app = new_response_rack({}.to_json, {'x-limit' => '1'}, schema: open_api_3_schema, raise: true)
105
-
106
- assert_raises(Committee::InvalidResponse) do
107
- get "/header"
108
- end
117
+ it "detects a invalid response" do
118
+ @app = new_response_rack("{}", {}, schema: open_api_3_schema)
119
+ get "/ref-sample"
120
+ assert_equal 500, last_response.status
109
121
  end
122
+ end
110
123
 
111
- it 'invalid type but not check' do
112
- @app = new_response_rack({}.to_json, {'x-limit' => '1'}, schema: open_api_3_schema, raise: true, check_header: false)
113
-
114
- get "/header"
115
-
116
- assert_equal 200, last_response.status
124
+ describe 'check header' do
125
+ [
126
+ { check_header: true, description: 'valid value', header: { 'integer' => 1 }, expected: { status: 200 } },
127
+ { check_header: true, description: 'missing value', header: { 'integer' => nil }, expected: { error: 'headers/integer/schema does not allow null values' } },
128
+ { check_header: true, description: 'invalid value', header: { 'integer' => 'x' }, expected: { error: 'headers/integer/schema expected integer, but received String: x' } },
129
+
130
+ { check_header: false, description: 'valid value', header: { 'integer' => 1 }, expected: { status: 200 } },
131
+ { check_header: false, description: 'missing value', header: { 'integer' => nil }, expected: { status: 200 } },
132
+ { check_header: false, description: 'invalid value', header: { 'integer' => 'x' }, expected: { status: 200 } },
133
+ ].each do |h|
134
+ check_header = h[:check_header]
135
+ description = h[:description]
136
+ header = h[:header]
137
+ expected = h[:expected]
138
+ describe "when #{check_header}" do
139
+ %w(get post put patch delete).each do |method|
140
+ describe method do
141
+ describe description do
142
+ if expected[:error].nil?
143
+ it 'should pass' do
144
+ @app = new_response_rack({}.to_json, header, schema: open_api_3_schema, raise: true, check_header: check_header)
145
+
146
+ send(method, "/header")
147
+ assert_equal expected[:status], last_response.status
148
+ end
149
+ else
150
+ it 'should fail' do
151
+ @app = new_response_rack({}.to_json, header, schema: open_api_3_schema, raise: true, check_header: check_header)
152
+
153
+ error = assert_raises(Committee::InvalidResponse) do
154
+ get "/header"
155
+ end
156
+ assert_match(expected[:error], error.message)
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
117
163
  end
118
164
  end
119
165
 
@@ -130,7 +176,8 @@ describe Committee::Middleware::ResponseValidation do
130
176
  e = assert_raises(Committee::InvalidResponse) do
131
177
  get "/characters"
132
178
  end
133
- assert_match(/1 class is String/i, e.message)
179
+
180
+ assert_match(/but received String: 1/i, e.message)
134
181
  end
135
182
 
136
183
  it "detects an invalid response status code with validate_success_only=true" do
@@ -148,9 +195,32 @@ describe Committee::Middleware::ResponseValidation do
148
195
  end
149
196
  end
150
197
 
198
+ describe ':accept_request_filter' do
199
+ [
200
+ { description: 'when not specified, includes everything', accept_request_filter: nil, expected: { status: 500 } },
201
+ { description: 'when predicate matches, performs validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/c') }, expected: { status: 500 } },
202
+ { description: 'when predicate does not match, skips validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/x') }, expected: { status: 200 } },
203
+ ].each do |h|
204
+ description = h[:description]
205
+ accept_request_filter = h[:accept_request_filter]
206
+ expected = h[:expected]
207
+
208
+ it description do
209
+ @app = new_response_rack('not_json', {}, schema: open_api_3_schema, prefix: '/v1', accept_request_filter: accept_request_filter)
210
+
211
+ get 'v1/characters'
212
+
213
+ assert_equal expected[:status], last_response.status
214
+ end
215
+ end
216
+ end
217
+
151
218
  private
152
219
 
153
220
  def new_response_rack(response, headers = {}, options = {}, rack_options = {})
221
+ # TODO: delete when 5.0.0 released because default value changed
222
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
223
+
154
224
  status = rack_options[:status] || 200
155
225
  headers = {
156
226
  "Content-Type" => "application/json"
@@ -38,6 +38,12 @@ describe Committee::Middleware::ResponseValidation do
38
38
  assert_match(/{} is not an array/i, last_response.body)
39
39
  end
40
40
 
41
+ it "detects a response invalid due to schema with ignore_error option" do
42
+ @app = new_rack_app("{}", {}, schema: hyper_schema, ignore_error: true)
43
+ get "/apps"
44
+ assert_equal 200, last_response.status
45
+ end
46
+
41
47
  it "detects a response invalid due to not being JSON" do
42
48
  @app = new_rack_app("{_}", {}, schema: hyper_schema)
43
49
  get "/apps"
@@ -156,9 +162,31 @@ describe Committee::Middleware::ResponseValidation do
156
162
  assert_match(/valid JSON/i, last_response.body)
157
163
  end
158
164
 
165
+ describe ':accept_request_filter' do
166
+ [
167
+ { description: 'when not specified, includes everything', accept_request_filter: nil, expected: { status: 500 } },
168
+ { description: 'when predicate matches, performs validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/a') }, expected: { status: 500 } },
169
+ { description: 'when predicate does not match, skips validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/x') }, expected: { status: 200 } },
170
+ ].each do |h|
171
+ description = h[:description]
172
+ accept_request_filter = h[:accept_request_filter]
173
+ expected = h[:expected]
174
+ it description do
175
+ @app = new_rack_app('not_json', {}, schema: hyper_schema, prefix: '/v1', accept_request_filter: accept_request_filter)
176
+
177
+ get '/v1/apps'
178
+
179
+ assert_equal expected[:status], last_response.status
180
+ end
181
+ end
182
+ end
183
+
159
184
  private
160
185
 
161
186
  def new_rack_app(response, headers = {}, options = {})
187
+ # TODO: delete when 5.0.0 released because default value changed
188
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
189
+
162
190
  headers = {
163
191
  "Content-Type" => "application/json"
164
192
  }.merge(headers)
@@ -112,6 +112,10 @@ describe Committee::Middleware::Stub do
112
112
  def new_rack_app(options = {})
113
113
  response = options.delete(:response)
114
114
  suppress = options.delete(:suppress)
115
+
116
+ # TODO: delete when 5.0.0 released because default value changed
117
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
118
+
115
119
  Rack::Builder.new {
116
120
  use Committee::Middleware::Stub, options
117
121
  run lambda { |env|
@@ -100,7 +100,10 @@ describe Committee::RequestUnpacker do
100
100
  }
101
101
  request = Rack::Request.new(env)
102
102
 
103
- router = hyper_schema.build_router({})
103
+ options = {}
104
+ # TODO: delete when 5.0.0 released because default value changed
105
+ options[:parse_response_by_content_type] = false
106
+ router = hyper_schema.build_router(options)
104
107
  validator = router.build_schema_validator(request)
105
108
 
106
109
  schema = JsonSchema::Schema.new
@@ -133,7 +136,10 @@ describe Committee::RequestUnpacker do
133
136
  }
134
137
  request = Rack::Request.new(env)
135
138
 
136
- router = open_api_3_schema.build_router({})
139
+ options = {}
140
+ # TODO: delete when 5.0.0 released because default value changed
141
+ options[:parse_response_by_content_type] = false
142
+ router = open_api_3_schema.build_router(options)
137
143
  validator = router.build_schema_validator(request)
138
144
 
139
145
  params, _ = Committee::RequestUnpacker.new(
@@ -158,7 +164,10 @@ describe Committee::RequestUnpacker do
158
164
  }
159
165
  request = Rack::Request.new(env)
160
166
 
161
- router = open_api_3_schema.build_router({})
167
+ options = {}
168
+ # TODO: delete when 5.0.0 released because default value changed
169
+ options[:parse_response_by_content_type] = false
170
+ router = open_api_3_schema.build_router(options)
162
171
  validator = router.build_schema_validator(request)
163
172
 
164
173
  params, _ = Committee::RequestUnpacker.new(
@@ -69,6 +69,8 @@ describe Committee::SchemaValidator::HyperSchema::Router do
69
69
  end
70
70
 
71
71
  def hyper_schema_router(options = {})
72
+ # TODO: delete when 5.0.0 released because default value changed
73
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
72
74
  schema = Committee::Drivers::HyperSchema::Driver.new.parse(hyper_schema_data)
73
75
  validator_option = Committee::SchemaValidator::Option.new(options, schema, :hyper_schema)
74
76
 
@@ -76,6 +78,8 @@ describe Committee::SchemaValidator::HyperSchema::Router do
76
78
  end
77
79
 
78
80
  def open_api_2_router(options = {})
81
+ # TODO: delete when 5.0.0 released because default value changed
82
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
79
83
  schema = Committee::Drivers::OpenAPI2::Driver.new.parse(open_api_2_data)
80
84
  validator_option = Committee::SchemaValidator::Option.new(options, schema, :hyper_schema)
81
85
 
@@ -9,7 +9,12 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
9
9
  before do
10
10
  @path = '/validate'
11
11
  @method = 'post'
12
- @validator_option = Committee::SchemaValidator::Option.new({}, open_api_3_schema, :open_api_3)
12
+
13
+ # TODO: delete when 5.0.0 released because default value changed
14
+ options = {}
15
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
16
+
17
+ @validator_option = Committee::SchemaValidator::Option.new(options, open_api_3_schema, :open_api_3)
13
18
  end
14
19
 
15
20
  def operation_object
@@ -52,8 +57,7 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
52
57
  operation_object.validate_request_params({"string" => 1}, HEADER, @validator_option)
53
58
  }
54
59
 
55
- # FIXME: when ruby 2.3 dropped, fix because ruby 2.3 return Fixnum, ruby 2.4 or later return Integer
56
- assert e.message.start_with?("1 class is #{1.class} but it's not valid")
60
+ assert_match(/expected string, but received Integer: 1/i, e.message)
57
61
  end
58
62
 
59
63
  it 'support put method' do
@@ -64,8 +68,7 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
64
68
  operation_object.validate_request_params({"string" => 1}, HEADER, @validator_option)
65
69
  }
66
70
 
67
- # FIXME: when ruby 2.3 dropped, fix because ruby 2.3 return Fixnum, ruby 2.4 or later return Integer
68
- assert e.message.start_with?("1 class is #{1.class} but it's not valid")
71
+ assert_match(/expected string, but received Integer: 1/i, e.message)
69
72
  end
70
73
 
71
74
  it 'support patch method' do
@@ -76,7 +79,7 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
76
79
  operation_object.validate_request_params({"integer" => "str"}, HEADER, @validator_option)
77
80
  }
78
81
 
79
- assert e.message.start_with?("str class is String but it's not valid")
82
+ assert_match(/expected integer, but received String: str/i, e.message)
80
83
  end
81
84
 
82
85
  it 'unknown param' do
@@ -109,7 +112,7 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
109
112
  operation_object.validate_request_params({"query_integer_list" => [1, 2]}, HEADER, @validator_option)
110
113
  }
111
114
 
112
- assert e.message.start_with?("required parameters query_string not exist in")
115
+ assert_match(/missing required parameters: query_string/i, e.message)
113
116
  end
114
117
 
115
118
  it 'invalid type' do
@@ -121,8 +124,7 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
121
124
  )
122
125
  }
123
126
 
124
- # FIXME: when ruby 2.3 dropped, fix because ruby 2.3 return Fixnum, ruby 2.4 or later return Integer
125
- assert e.message.start_with?("1 class is #{1.class} but")
127
+ assert_match(/expected string, but received Integer: 1/i, e.message)
126
128
  end
127
129
  end
128
130
 
@@ -143,7 +145,23 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
143
145
  operation_object.validate_request_params({"limit" => "a"}, HEADER, @validator_option)
144
146
  }
145
147
 
146
- assert e.message.start_with?("a class is String but")
148
+ assert_match(/expected integer, but received String: a/i, e.message)
149
+ end
150
+ end
151
+
152
+ describe '#content_types' do
153
+ it 'returns supported content types' do
154
+ @path = '/validate_content_types'
155
+ @method = 'post'
156
+
157
+ assert_equal ["application/json", "application/binary"], operation_object.request_content_types
158
+ end
159
+
160
+ it 'returns an empty array when the content of requestBody does not exist' do
161
+ @path = '/characters'
162
+ @method = 'get'
163
+
164
+ assert_equal [], operation_object.request_content_types
147
165
  end
148
166
  end
149
167
  end
@@ -10,7 +10,7 @@ describe Committee::SchemaValidator::OpenAPI3::RequestValidator do
10
10
  @app
11
11
  end
12
12
 
13
- it "skip validaiton when link does not exist" do
13
+ it "skip validation when link does not exist" do
14
14
  @app = new_rack_app(schema: open_api_3_schema)
15
15
  params = {}
16
16
  header "Content-Type", "application/json"
@@ -21,11 +21,34 @@ describe Committee::SchemaValidator::OpenAPI3::RequestValidator do
21
21
  it "optionally content_type check" do
22
22
  @app = new_rack_app(check_content_type: true, schema: open_api_3_schema)
23
23
  params = {
24
- "string_post_1" => "cloudnasium"
24
+ "string_post_1" => "cloudnasium"
25
25
  }
26
26
  header "Content-Type", "text/html"
27
27
  post "/characters", JSON.generate(params)
28
28
  assert_equal 400, last_response.status
29
+
30
+ body = JSON.parse(last_response.body)
31
+ message =
32
+ %{"Content-Type" request header must be set to "application/json".}
33
+
34
+ assert_equal "bad_request", body['id']
35
+ assert_equal message, body['message']
36
+ end
37
+
38
+ it "validates content_type" do
39
+ @app = new_rack_app(check_content_type: true, schema: open_api_3_schema)
40
+ params = {
41
+ "string_post_1" => "cloudnasium"
42
+ }
43
+ header "Content-Type", "text/html"
44
+ post "/validate_content_types", JSON.generate(params)
45
+ assert_equal 400, last_response.status
46
+
47
+ body = JSON.parse(last_response.body)
48
+ message =
49
+ %{"Content-Type" request header must be set to any of the following: ["application/json", "application/binary"].}
50
+
51
+ assert_equal message, body['message']
29
52
  end
30
53
 
31
54
  it "optionally skip content_type check" do
@@ -38,7 +61,7 @@ describe Committee::SchemaValidator::OpenAPI3::RequestValidator do
38
61
  assert_equal 200, last_response.status
39
62
  end
40
63
 
41
- it "if not exist requsetBody definition, skip content_type check" do
64
+ it "if not exist requestBody definition, skip content_type check" do
42
65
  @app = new_rack_app(check_content_type: true, schema: open_api_3_schema)
43
66
  params = {
44
67
  "string_post_1" => "cloudnasium"
@@ -49,6 +72,9 @@ describe Committee::SchemaValidator::OpenAPI3::RequestValidator do
49
72
  end
50
73
 
51
74
  def new_rack_app(options = {})
75
+ # TODO: delete when 5.0.0 released because default value changed
76
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
77
+
52
78
  Rack::Builder.new {
53
79
  use Committee::Middleware::RequestValidation, options
54
80
  run lambda { |_|
@@ -12,7 +12,12 @@ describe Committee::SchemaValidator::OpenAPI3::ResponseValidator do
12
12
 
13
13
  @path = '/validate'
14
14
  @method = 'post'
15
- @validator_option = Committee::SchemaValidator::Option.new({}, open_api_3_schema, :open_api_3)
15
+
16
+ # TODO: delete when 5.0.0 released because default value changed
17
+ options = {}
18
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
19
+
20
+ @validator_option = Committee::SchemaValidator::Option.new(options, open_api_3_schema, :open_api_3)
16
21
  end
17
22
 
18
23
  it "passes through a valid response" do
@@ -29,7 +34,7 @@ describe Committee::SchemaValidator::OpenAPI3::ResponseValidator do
29
34
  call_response_validator
30
35
  end
31
36
 
32
- it "passes through a valid response with no registered Content-Type with strict = true" do
37
+ it "raises InvalidResponse when a valid response with no registered body with strict option" do
33
38
  @headers = { "Content-Type" => "application/xml" }
34
39
  assert_raises(Committee::InvalidResponse) {
35
40
  call_response_validator(true)
@@ -41,7 +46,7 @@ describe Committee::SchemaValidator::OpenAPI3::ResponseValidator do
41
46
  call_response_validator
42
47
  end
43
48
 
44
- it "passes through a valid response with no Content-Type with strict option" do
49
+ it "raises InvalidResponse when a valid response with no Content-Type headers with strict option" do
45
50
  @headers = {}
46
51
  assert_raises(Committee::InvalidResponse) {
47
52
  call_response_validator(true)
@@ -30,6 +30,9 @@ describe Committee::Test::Methods do
30
30
  @committee_schema = nil
31
31
 
32
32
  @committee_options = {schema: hyper_schema}
33
+
34
+ # TODO: delete when 5.0.0 released because default value changed
35
+ @committee_options[:parse_response_by_content_type] = false
33
36
  end
34
37
 
35
38
  describe "#assert_schema_conform" do
@@ -28,7 +28,10 @@ describe Committee::Test::Methods do
28
28
  # our purposes here in testing the module.
29
29
  @committee_router = nil
30
30
  @committee_schema = nil
31
- @committee_options = nil
31
+ @committee_options = {}
32
+
33
+ # TODO: delete when 5.0.0 released because default value changed
34
+ @committee_options[:parse_response_by_content_type] = true
32
35
  end
33
36
 
34
37
  describe "Hyper-Schema" do
@@ -36,7 +39,7 @@ describe Committee::Test::Methods do
36
39
  sc = JsonSchema.parse!(hyper_schema_data)
37
40
  sc.expand_references!
38
41
  s = Committee::Drivers::HyperSchema::Driver.new.parse(sc)
39
- @committee_options = {schema: s}
42
+ @committee_options.merge!({schema: s})
40
43
  end
41
44
 
42
45
  describe "#assert_schema_conform" do
@@ -120,7 +123,7 @@ describe Committee::Test::Methods do
120
123
 
121
124
  describe "OpenAPI3" do
122
125
  before do
123
- @committee_options = {schema: open_api_3_schema}
126
+ @committee_options.merge!({schema: open_api_3_schema})
124
127
 
125
128
  @correct_response = { string_1: :honoka }
126
129
  end
@@ -138,7 +141,7 @@ describe Committee::Test::Methods do
138
141
  e = assert_raises(Committee::InvalidResponse) do
139
142
  assert_schema_conform
140
143
  end
141
- assert_match(/don't exist response definition/i, e.message)
144
+ assert_match(/response definition does not exist/i, e.message)
142
145
  end
143
146
 
144
147
  it "detects an invalid response status code" do
@@ -149,7 +152,7 @@ describe Committee::Test::Methods do
149
152
  e = assert_raises(Committee::InvalidResponse) do
150
153
  assert_schema_conform
151
154
  end
152
- assert_match(/don't exist status code definition/i, e.message)
155
+ assert_match(/status code definition does not exist/i, e.message)
153
156
  end
154
157
 
155
158
  it "outputs deprecation warning" do
@@ -175,7 +178,8 @@ describe Committee::Test::Methods do
175
178
  e = assert_raises(Committee::InvalidRequest) do
176
179
  assert_request_schema_confirm
177
180
  end
178
- assert_match(/required parameters query_string not exist in #\/paths/i, e.message)
181
+
182
+ assert_match(/missing required parameters: query_string/i, e.message)
179
183
  end
180
184
 
181
185
  it "path undefined in schema" do
@@ -201,7 +205,7 @@ describe Committee::Test::Methods do
201
205
  e = assert_raises(Committee::InvalidResponse) do
202
206
  assert_response_schema_confirm
203
207
  end
204
- assert_match(/don't exist response definition/i, e.message)
208
+ assert_match(/response definition does not exist/i, e.message)
205
209
  end
206
210
 
207
211
  it "detects an invalid response status code" do
@@ -212,7 +216,7 @@ describe Committee::Test::Methods do
212
216
  e = assert_raises(Committee::InvalidResponse) do
213
217
  assert_response_schema_confirm
214
218
  end
215
- assert_match(/don't exist status code definition/i, e.message)
219
+ assert_match(/status code definition does not exist/i, e.message)
216
220
  end
217
221
 
218
222
  it "path undefined in schema" do
@@ -56,7 +56,7 @@ def open_api_2_schema
56
56
  end
57
57
 
58
58
  def open_api_3_schema
59
- @open_api_3_schema ||= Committee::Drivers.load_from_data(open_api_3_data)
59
+ @open_api_3_schema ||= Committee::Drivers.load_from_file(open_api_3_schema_path)
60
60
  end
61
61
 
62
62
  # Don't cache this because we'll often manipulate the created hash in tests.
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: 3.1.1
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandur
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2019-09-04 00:00:00.000000000 Z
13
+ date: 2020-06-27 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: json_schema
@@ -52,14 +52,14 @@ dependencies:
52
52
  requirements:
53
53
  - - ">="
54
54
  - !ruby/object:Gem::Version
55
- version: 0.2.2
55
+ version: 0.11.1
56
56
  type: :runtime
57
57
  prerelease: false
58
58
  version_requirements: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - ">="
61
61
  - !ruby/object:Gem::Version
62
- version: 0.2.2
62
+ version: 0.11.1
63
63
  - !ruby/object:Gem::Dependency
64
64
  name: minitest
65
65
  requirement: !ruby/object:Gem::Requirement
@@ -282,14 +282,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
282
282
  requirements:
283
283
  - - ">="
284
284
  - !ruby/object:Gem::Version
285
- version: 2.3.0
285
+ version: 2.4.0
286
286
  required_rubygems_version: !ruby/object:Gem::Requirement
287
287
  requirements:
288
288
  - - ">="
289
289
  - !ruby/object:Gem::Version
290
290
  version: '0'
291
291
  requirements: []
292
- rubygems_version: 3.0.3
292
+ rubygems_version: 3.1.2
293
293
  signing_key:
294
294
  specification_version: 4
295
295
  summary: A collection of Rack middleware to support JSON Schema.