committee 3.2.1 → 4.2.1

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: 2af076998ef23743c239df64c5517e3e334422a3523f09e6f8baa4f1a9f1ef77
4
- data.tar.gz: a5ae6eca5d34310a69e1c33ca6f11a807e19316c8fad9123521ecf835cfe091c
3
+ metadata.gz: 238def2dc052574b0bb8022a3765df9a6f5c3b359ee34d9bcdbdccef7240748a
4
+ data.tar.gz: 615f73429b3c9e4336cdfef38db5d4f9ec778298fdc96139f10d83f8d12cf296
5
5
  SHA512:
6
- metadata.gz: 65e65734251a25cb3a3735081c8a3e2e6973b93d1e14b66261d3978e7dac3ea5a9081fe7a530625cf9407d3ac25bc8941d6bea064d07f546186b6a2ebd043fa6
7
- data.tar.gz: 7dee370a2748aa7b67d1e360e86aacb204d16a5a68d18ac879ececc3633d6d92f5ad9b49ba36ac8851e1a04f2322fec2269b8926f30f061d1524bf284af90848
6
+ metadata.gz: 113e66a74a6e2214f53aaf572c8563f1d99b273e70ac0f96e1f77c3aaf44c0af4964468bd11bfaf906449db8fb9a7f32b7e6943aa11e5895b11c92e09481c62f
7
+ data.tar.gz: 8c7fd8cddfea01a569a85ee1f49ff41e1854474b922c0cdeec0b96cfff57349fa9905d84cf8727b48fdfa3fd026d69b4f4493964e6d7b7917086777c1cdf89ad
@@ -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,9 +8,21 @@ module Committee
8
8
  end
9
9
 
10
10
  class InvalidRequest < Error
11
+ attr_reader :original_error
12
+
13
+ def initialize(error_message=nil, original_error: nil)
14
+ @original_error = original_error
15
+ super(error_message)
16
+ end
11
17
  end
12
18
 
13
19
  class InvalidResponse < Error
20
+ attr_reader :original_error
21
+
22
+ def initialize(error_message=nil, original_error: nil)
23
+ @original_error = original_error
24
+ super(error_message)
25
+ end
14
26
  end
15
27
 
16
28
  class NotFound < Error
@@ -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
@@ -44,11 +42,7 @@ module Committee
44
42
  if @error_handler.arity > 1
45
43
  @error_handler.call(e, env)
46
44
  else
47
- warn <<-MESSAGE
48
- [DEPRECATION] Using `error_handler.call(exception)` is deprecated and will be change to
49
- `error_handler.call(exception, request.env)` in next major version.
50
- MESSAGE
51
-
45
+ warn '[DEPRECATION] Using `error_handler.call(exception)` is deprecated and will be change to `error_handler.call(exception, request.env)` in next major version.'
52
46
  @error_handler.call(e)
53
47
  end
54
48
  end
@@ -8,13 +8,12 @@ module Committee
8
8
  def initialize(app, options = {})
9
9
  super
10
10
  @validate_success_only = @schema.validator_option.validate_success_only
11
- @ignore_error = options.fetch(:ignore_error, false)
12
11
  end
13
12
 
14
13
  def handle(request)
15
- begin
16
- status, headers, response = @app.call(request.env)
14
+ status, headers, response = @app.call(request.env)
17
15
 
16
+ begin
18
17
  v = build_schema_validator(request)
19
18
  v.response_validate(status, headers, response) if v.link_exist? && self.class.validate?(status, validate_success_only)
20
19
 
@@ -47,11 +46,7 @@ module Committee
47
46
  if @error_handler.arity > 1
48
47
  @error_handler.call(e, env)
49
48
  else
50
- warn <<-MESSAGE
51
- [DEPRECATION] Using `error_handler.call(exception)` is deprecated and will be change to
52
- `error_handler.call(exception, request.env)` in next major version.
53
- MESSAGE
54
-
49
+ warn '[DEPRECATION] Using `error_handler.call(exception)` is deprecated and will be change to `error_handler.call(exception, request.env)` in next major version.'
55
50
  @error_handler.call(e)
56
51
  end
57
52
  end
@@ -16,7 +16,7 @@ module Committee
16
16
  def call
17
17
  # if Content-Type is empty or JSON, and there was a request body, try to
18
18
  # interpret it as JSON
19
- params = if !@request.media_type || @request.media_type =~ %r{application/.*json}
19
+ params = if !@request.media_type || @request.media_type =~ %r{application/(?:.*\+)?json}
20
20
  parse_json
21
21
  elsif @optimistic_json
22
22
  begin
@@ -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.
@@ -23,7 +23,7 @@ module Committee
23
23
 
24
24
  request_operation.validate_path_params(options)
25
25
  rescue OpenAPIParser::OpenAPIError => e
26
- raise Committee::InvalidRequest.new(e.message)
26
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
27
27
  end
28
28
 
29
29
  # @param [Boolean] strict when not content_type or status code definition, raise error
@@ -32,7 +32,7 @@ module Committee
32
32
 
33
33
  return request_operation.validate_response_body(response_body, response_validate_options(strict, check_header))
34
34
  rescue OpenAPIParser::OpenAPIError => e
35
- raise Committee::InvalidResponse.new(e.message)
35
+ raise Committee::InvalidResponse.new(e.message, original_error: e)
36
36
  end
37
37
 
38
38
  def validate_request_params(params, headers, validator_option)
@@ -109,7 +109,7 @@ module Committee
109
109
  # bad performance because when we coerce value, same check
110
110
  request_operation.validate_request_parameter(params, headers, build_openapi_parser_get_option(validator_option))
111
111
  rescue OpenAPIParser::OpenAPIError => e
112
- raise Committee::InvalidRequest.new(e.message)
112
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
113
113
  end
114
114
 
115
115
  def validate_post_request_params(params, headers, validator_option)
@@ -120,7 +120,7 @@ module Committee
120
120
  request_operation.validate_request_parameter(params, headers, schema_validator_options)
121
121
  request_operation.validate_request_body(content_type, params, schema_validator_options)
122
122
  rescue => e
123
- raise Committee::InvalidRequest.new(e.message)
123
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
124
124
  end
125
125
 
126
126
  def response_validate_options(strict, check_header)
@@ -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)
@@ -58,12 +58,7 @@ module Committee
58
58
  def old_behavior
59
59
  old_assert_behavior = committee_options.fetch(:old_assert_behavior, nil)
60
60
  if old_assert_behavior.nil?
61
- warn <<-MSG
62
- [DEPRECATION] now assert_schema_conform check response schema only.
63
- but we will change check request and response in future major version.
64
- so if you want to conform response only, please use assert_response_schema_confirm,
65
- or you can suppress this message and keep old behavior by setting old_assert_behavior=true.
66
- MSG
61
+ warn '[DEPRECATION] now assert_schema_conform check response schema only. but we will change check request and response in future major version. so if you want to conform response only, please use assert_response_schema_confirm, or you can suppress this message and keep old behavior by setting old_assert_behavior=true.'
67
62
  old_assert_behavior = true
68
63
  end
69
64
  old_assert_behavior
@@ -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(/expected string, but received #{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,8 +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
- # FIXME: when ruby 2.3 dropped, fix because ruby 2.3 return Fixnum, ruby 2.4 or later return Integer
283
- assert_match(/expected string, but received #{1.class}: /i, last_response.body)
281
+ assert_match(/expected string, but received Integer: /i, last_response.body)
284
282
  end
285
283
 
286
284
  it "ignores paths outside the prefix" do
@@ -379,6 +377,13 @@ describe Committee::Middleware::RequestValidation do
379
377
  assert_match(/expected integer, but received String: foo/i, last_response.body)
380
378
  end
381
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
385
+ end
386
+
382
387
  it "coerce string to integer" do
383
388
  check_parameter_string = lambda { |env|
384
389
  assert env['committee.params']['integer'].is_a?(Integer)
@@ -408,7 +413,11 @@ describe Committee::Middleware::RequestValidation do
408
413
  { check_header: false, description: 'valid value', value: 1, expected: { status: 200 } },
409
414
  { check_header: false, description: 'missing value', value: nil, expected: { status: 200 } },
410
415
  { check_header: false, description: 'invalid value', value: 'x', expected: { status: 200 } },
411
- ].each do |check_header:, description:, value:, expected:|
416
+ ].each do |h|
417
+ check_header = h[:check_header]
418
+ description = h[:description]
419
+ value = h[:value]
420
+ expected = h[:expected]
412
421
  describe "when #{check_header}" do
413
422
  %w(get post put patch delete).each do |method|
414
423
  describe method do
@@ -429,6 +438,35 @@ describe Committee::Middleware::RequestValidation do
429
438
  end
430
439
  end
431
440
 
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
457
+ end
458
+ end
459
+
460
+ it 'does not suppress application error' do
461
+ @app = new_rack_app_with_lambda(lambda { |_|
462
+ JSON.load('-') # invalid json
463
+ }, schema: open_api_3_schema, raise: true)
464
+
465
+ assert_raises(JSON::ParserError) do
466
+ get "/error", nil
467
+ end
468
+ end
469
+
432
470
  private
433
471
 
434
472
  def new_rack_app(options = {})
@@ -438,6 +476,9 @@ describe Committee::Middleware::RequestValidation do
438
476
  end
439
477
 
440
478
  def new_rack_app_with_lambda(check_lambda, options = {})
479
+ # TODO: delete when 5.0.0 released because default value changed
480
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
481
+
441
482
  Rack::Builder.new {
442
483
  use Committee::Middleware::RequestValidation, options
443
484
  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
@@ -34,6 +34,14 @@ describe Committee::Middleware::ResponseValidation do
34
34
  assert_equal 200, last_response.status
35
35
  end
36
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
+
37
45
  it "passes through not definition" do
38
46
  @app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), {}, schema: open_api_3_schema)
39
47
  get "/no_data"
@@ -99,29 +107,59 @@ describe Committee::Middleware::ResponseValidation do
99
107
  assert_match(/valid JSON/i, last_response.body)
100
108
  end
101
109
 
102
- describe 'check header' do
103
- it 'valid type header' do
104
- @app = new_response_rack({}.to_json, {'x-limit' => 1}, schema: open_api_3_schema, raise: true)
105
-
106
- get "/header"
107
-
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"
108
114
  assert_equal 200, last_response.status
109
115
  end
110
116
 
111
- it 'invalid type header' do
112
- @app = new_response_rack({}.to_json, {'x-limit' => '1'}, schema: open_api_3_schema, raise: true)
113
-
114
- assert_raises(Committee::InvalidResponse) do
115
- get "/header"
116
- 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
117
121
  end
122
+ end
118
123
 
119
- it 'invalid type but not check' do
120
- @app = new_response_rack({}.to_json, {'x-limit' => '1'}, schema: open_api_3_schema, raise: true, check_header: false)
121
-
122
- get "/header"
123
-
124
- 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
125
163
  end
126
164
  end
127
165
 
@@ -157,9 +195,45 @@ describe Committee::Middleware::ResponseValidation do
157
195
  end
158
196
  end
159
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
+
218
+ it 'does not suppress application error' do
219
+ @app = Rack::Builder.new {
220
+ use Committee::Middleware::ResponseValidation, {schema: open_api_3_schema, raise: true}
221
+ run lambda { |_|
222
+ JSON.load('-') # invalid json
223
+ }
224
+ }
225
+
226
+ assert_raises(JSON::ParserError) do
227
+ get "/error", nil
228
+ end
229
+ end
230
+
160
231
  private
161
232
 
162
233
  def new_response_rack(response, headers = {}, options = {}, rack_options = {})
234
+ # TODO: delete when 5.0.0 released because default value changed
235
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
236
+
163
237
  status = rack_options[:status] || 200
164
238
  headers = {
165
239
  "Content-Type" => "application/json"
@@ -162,9 +162,31 @@ describe Committee::Middleware::ResponseValidation do
162
162
  assert_match(/valid JSON/i, last_response.body)
163
163
  end
164
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
+
165
184
  private
166
185
 
167
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
+
168
190
  headers = {
169
191
  "Content-Type" => "application/json"
170
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|
@@ -13,6 +13,16 @@ describe Committee::RequestUnpacker do
13
13
  assert_equal({ "x" => "y" }, params)
14
14
  end
15
15
 
16
+ it "unpacks JSON on Content-Type: application/vnd.api+json" do
17
+ env = {
18
+ "CONTENT_TYPE" => "application/vnd.api+json",
19
+ "rack.input" => StringIO.new('{"x":"y"}'),
20
+ }
21
+ request = Rack::Request.new(env)
22
+ params, _ = Committee::RequestUnpacker.new(request).call
23
+ assert_equal({ "x" => "y" }, params)
24
+ end
25
+
16
26
  it "unpacks JSON on no Content-Type" do
17
27
  env = {
18
28
  "rack.input" => StringIO.new('{"x":"y"}'),
@@ -22,6 +32,16 @@ describe Committee::RequestUnpacker do
22
32
  assert_equal({ "x" => "y" }, params)
23
33
  end
24
34
 
35
+ it "doesn't unpack JSON on application/x-ndjson" do
36
+ env = {
37
+ "CONTENT_TYPE" => "application/x-ndjson",
38
+ "rack.input" => StringIO.new('{"x":"y"}\n{"a":"b"}'),
39
+ }
40
+ request = Rack::Request.new(env)
41
+ params, _ = Committee::RequestUnpacker.new(request).call
42
+ assert_equal({}, params)
43
+ end
44
+
25
45
  it "doesn't unpack JSON under other Content-Types" do
26
46
  %w[application/x-www-form-urlencoded multipart/form-data].each do |content_type|
27
47
  env = {
@@ -100,7 +120,10 @@ describe Committee::RequestUnpacker do
100
120
  }
101
121
  request = Rack::Request.new(env)
102
122
 
103
- router = hyper_schema.build_router({})
123
+ options = {}
124
+ # TODO: delete when 5.0.0 released because default value changed
125
+ options[:parse_response_by_content_type] = false
126
+ router = hyper_schema.build_router(options)
104
127
  validator = router.build_schema_validator(request)
105
128
 
106
129
  schema = JsonSchema::Schema.new
@@ -133,7 +156,10 @@ describe Committee::RequestUnpacker do
133
156
  }
134
157
  request = Rack::Request.new(env)
135
158
 
136
- router = open_api_3_schema.build_router({})
159
+ options = {}
160
+ # TODO: delete when 5.0.0 released because default value changed
161
+ options[:parse_response_by_content_type] = false
162
+ router = open_api_3_schema.build_router(options)
137
163
  validator = router.build_schema_validator(request)
138
164
 
139
165
  params, _ = Committee::RequestUnpacker.new(
@@ -158,7 +184,10 @@ describe Committee::RequestUnpacker do
158
184
  }
159
185
  request = Rack::Request.new(env)
160
186
 
161
- router = open_api_3_schema.build_router({})
187
+ options = {}
188
+ # TODO: delete when 5.0.0 released because default value changed
189
+ options[:parse_response_by_content_type] = false
190
+ router = open_api_3_schema.build_router(options)
162
191
  validator = router.build_schema_validator(request)
163
192
 
164
193
  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,8 @@ 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_match(/expected string, but received #{1.class}: 1/i, e.message)
60
+ assert_match(/expected string, but received Integer: 1/i, e.message)
61
+ assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
57
62
  end
58
63
 
59
64
  it 'support put method' do
@@ -64,8 +69,8 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
64
69
  operation_object.validate_request_params({"string" => 1}, HEADER, @validator_option)
65
70
  }
66
71
 
67
- # FIXME: when ruby 2.3 dropped, fix because ruby 2.3 return Fixnum, ruby 2.4 or later return Integer
68
- assert_match(/expected string, but received #{1.class}: 1/i, e.message)
72
+ assert_match(/expected string, but received Integer: 1/i, e.message)
73
+ assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
69
74
  end
70
75
 
71
76
  it 'support patch method' do
@@ -77,6 +82,7 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
77
82
  }
78
83
 
79
84
  assert_match(/expected integer, but received String: str/i, e.message)
85
+ assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
80
86
  end
81
87
 
82
88
  it 'unknown param' do
@@ -110,6 +116,7 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
110
116
  }
111
117
 
112
118
  assert_match(/missing required parameters: query_string/i, e.message)
119
+ assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
113
120
  end
114
121
 
115
122
  it 'invalid type' do
@@ -121,8 +128,8 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
121
128
  )
122
129
  }
123
130
 
124
- # FIXME: when ruby 2.3 dropped, fix because ruby 2.3 return Fixnum, ruby 2.4 or later return Integer
125
- assert_match(/expected string, but received #{1.class}: 1/i, e.message)
131
+ assert_match(/expected string, but received Integer: 1/i, e.message)
132
+ assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
126
133
  end
127
134
  end
128
135
 
@@ -144,6 +151,7 @@ describe Committee::SchemaValidator::OpenAPI3::OperationWrapper do
144
151
  }
145
152
 
146
153
  assert_match(/expected integer, but received String: a/i, e.message)
154
+ assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
147
155
  end
148
156
  end
149
157
 
@@ -72,6 +72,9 @@ describe Committee::SchemaValidator::OpenAPI3::RequestValidator do
72
72
  end
73
73
 
74
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
+
75
78
  Rack::Builder.new {
76
79
  use Committee::Middleware::RequestValidation, options
77
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,11 +34,12 @@ 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
- assert_raises(Committee::InvalidResponse) {
39
+ e = assert_raises(Committee::InvalidResponse) {
35
40
  call_response_validator(true)
36
41
  }
42
+ assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
37
43
  end
38
44
 
39
45
  it "passes through a valid response with no Content-Type" do
@@ -41,11 +47,12 @@ describe Committee::SchemaValidator::OpenAPI3::ResponseValidator do
41
47
  call_response_validator
42
48
  end
43
49
 
44
- it "passes through a valid response with no Content-Type with strict option" do
50
+ it "raises InvalidResponse when a valid response with no Content-Type headers with strict option" do
45
51
  @headers = {}
46
- assert_raises(Committee::InvalidResponse) {
52
+ e = assert_raises(Committee::InvalidResponse) {
47
53
  call_response_validator(true)
48
54
  }
55
+ assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
49
56
  end
50
57
 
51
58
  it "passes through a valid list response" do
@@ -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
@@ -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.2.1
4
+ version: 4.2.1
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-10-13 00:00:00.000000000 Z
13
+ date: 2020-11-07 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.6.1
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.6.1
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.