committee 4.1.0 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,13 +17,17 @@ module Committee
17
17
  request_operation.original_path
18
18
  end
19
19
 
20
+ def http_method
21
+ request_operation.http_method
22
+ end
23
+
20
24
  def coerce_path_parameter(validator_option)
21
25
  options = build_openapi_parser_path_option(validator_option)
22
26
  return {} unless options.coerce_value
23
27
 
24
28
  request_operation.validate_path_params(options)
25
29
  rescue OpenAPIParser::OpenAPIError => e
26
- raise Committee::InvalidRequest.new(e.message)
30
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
27
31
  end
28
32
 
29
33
  # @param [Boolean] strict when not content_type or status code definition, raise error
@@ -32,7 +36,7 @@ module Committee
32
36
 
33
37
  return request_operation.validate_response_body(response_body, response_validate_options(strict, check_header))
34
38
  rescue OpenAPIParser::OpenAPIError => e
35
- raise Committee::InvalidResponse.new(e.message)
39
+ raise Committee::InvalidResponse.new(e.message, original_error: e)
36
40
  end
37
41
 
38
42
  def validate_request_params(params, headers, validator_option)
@@ -109,7 +113,7 @@ module Committee
109
113
  # bad performance because when we coerce value, same check
110
114
  request_operation.validate_request_parameter(params, headers, build_openapi_parser_get_option(validator_option))
111
115
  rescue OpenAPIParser::OpenAPIError => e
112
- raise Committee::InvalidRequest.new(e.message)
116
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
113
117
  end
114
118
 
115
119
  def validate_post_request_params(params, headers, validator_option)
@@ -120,7 +124,7 @@ module Committee
120
124
  request_operation.validate_request_parameter(params, headers, schema_validator_options)
121
125
  request_operation.validate_request_body(content_type, params, schema_validator_options)
122
126
  rescue => e
123
- raise Committee::InvalidRequest.new(e.message)
127
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
124
128
  end
125
129
 
126
130
  def response_validate_options(strict, check_header)
@@ -22,8 +22,10 @@ module Committee
22
22
  end
23
23
 
24
24
  def operation_object(request)
25
+ return nil unless includes_request?(request)
26
+
25
27
  path = request.path
26
- path = path.gsub(@prefix_regexp, '') if prefix_request?(request)
28
+ path = path.gsub(@prefix_regexp, '') if @prefix_regexp
27
29
 
28
30
  request_method = request.request_method.downcase
29
31
 
@@ -21,12 +21,24 @@ module Committee
21
21
  # Non-boolean options:
22
22
  attr_reader :headers_key,
23
23
  :params_key,
24
+ :query_hash_key,
25
+ :request_body_hash_key,
26
+ :path_hash_key,
24
27
  :prefix
25
28
 
26
29
  def initialize(options, schema, schema_type)
27
30
  # Non-boolean options
28
- @headers_key = options[:headers_key] || "committee.headers"
29
- @params_key = options[:params_key] || "committee.params"
31
+ @headers_key = options[:headers_key] || "committee.headers"
32
+ @params_key = options[:params_key] || "committee.params"
33
+ @query_hash_key = if options[:query_hash_key].nil?
34
+ Committee.warn_deprecated('Committee: please set query_hash_key = rack.request.query_hash because we\'ll change default value in next major version.')
35
+ 'rack.request.query_hash'
36
+ else
37
+ options.fetch(:query_hash_key)
38
+ end
39
+ @path_hash_key = options[:path_hash_key] || "committee.path_hash"
40
+ @request_body_hash_key = options[:request_body_hash_key] || "committee.request_body_hash"
41
+
30
42
  @prefix = options[:prefix]
31
43
 
32
44
  # Boolean options and have a common value by default
@@ -3,27 +3,40 @@
3
3
  module Committee
4
4
  module Test
5
5
  module Methods
6
- def assert_schema_conform
6
+ def assert_schema_conform(expected_status = nil)
7
7
  assert_request_schema_confirm unless old_behavior
8
- assert_response_schema_confirm
8
+ assert_response_schema_confirm(expected_status)
9
9
  end
10
10
 
11
11
  def assert_request_schema_confirm
12
12
  unless schema_validator.link_exist?
13
- request = "`#{request_object.request_method} #{request_object.path_info}` undefined in schema."
13
+ request = "`#{request_object.request_method} #{request_object.path_info}` undefined in schema (prefix: #{committee_options[:prefix].inspect})."
14
14
  raise Committee::InvalidRequest.new(request)
15
15
  end
16
16
 
17
17
  schema_validator.request_validate(request_object)
18
18
  end
19
19
 
20
- def assert_response_schema_confirm
20
+ def assert_response_schema_confirm(expected_status = nil)
21
21
  unless schema_validator.link_exist?
22
- response = "`#{request_object.request_method} #{request_object.path_info}` undefined in schema."
22
+ response = "`#{request_object.request_method} #{request_object.path_info}` undefined in schema (prefix: #{committee_options[:prefix].inspect})."
23
23
  raise Committee::InvalidResponse.new(response)
24
24
  end
25
25
 
26
26
  status, headers, body = response_data
27
+
28
+ if expected_status.nil?
29
+ Committee.warn_deprecated('Pass expected response status code to check it against the corresponding schema explicitly.')
30
+ elsif expected_status != status
31
+ response = "Expected `#{expected_status}` status code, but it was `#{status}`."
32
+ raise Committee::InvalidResponse.new(response)
33
+ end
34
+
35
+ if schema_coverage
36
+ operation_object = router.operation_object(request_object)
37
+ schema_coverage&.update_response_coverage!(operation_object.original_path, operation_object.http_method, status)
38
+ end
39
+
27
40
  schema_validator.response_validate(status, headers, [body], true) if validate_response?(status)
28
41
  end
29
42
 
@@ -55,15 +68,18 @@ module Committee
55
68
  @schema_validator ||= router.build_schema_validator(request_object)
56
69
  end
57
70
 
71
+ def schema_coverage
72
+ return nil unless schema.is_a?(Committee::Drivers::OpenAPI3::Schema)
73
+
74
+ coverage = committee_options.fetch(:schema_coverage, nil)
75
+
76
+ coverage.is_a?(SchemaCoverage) ? coverage : nil
77
+ end
78
+
58
79
  def old_behavior
59
80
  old_assert_behavior = committee_options.fetch(:old_assert_behavior, nil)
60
81
  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
82
+ Committee.warn_deprecated('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
83
  old_assert_behavior = true
68
84
  end
69
85
  old_assert_behavior
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module Test
5
+ class SchemaCoverage
6
+ attr_reader :schema
7
+
8
+ class << self
9
+ def merge_report(first, second)
10
+ report = first.dup
11
+ second.each do |k, v|
12
+ if v.is_a?(Hash)
13
+ if report[k].nil?
14
+ report[k] = v
15
+ else
16
+ report[k] = merge_report(report[k], v)
17
+ end
18
+ else
19
+ report[k] ||= v
20
+ end
21
+ end
22
+ report
23
+ end
24
+
25
+ def flatten_report(report)
26
+ responses = []
27
+ report.each do |path_name, path_coverage|
28
+ path_coverage.each do |method, method_coverage|
29
+ responses_coverage = method_coverage['responses']
30
+ responses_coverage.each do |response_status, is_covered|
31
+ responses << {
32
+ path: path_name,
33
+ method: method,
34
+ status: response_status,
35
+ is_covered: is_covered,
36
+ }
37
+ end
38
+ end
39
+ end
40
+ {
41
+ responses: responses,
42
+ }
43
+ end
44
+ end
45
+
46
+ def initialize(schema)
47
+ raise 'Unsupported schema' unless schema.is_a?(Committee::Drivers::OpenAPI3::Schema)
48
+
49
+ @schema = schema
50
+ @covered = {}
51
+ end
52
+
53
+ def update_response_coverage!(path, method, response_status)
54
+ method = method.to_s.downcase
55
+ response_status = response_status.to_s
56
+
57
+ @covered[path] ||= {}
58
+ @covered[path][method] ||= {}
59
+ @covered[path][method]['responses'] ||= {}
60
+ @covered[path][method]['responses'][response_status] = true
61
+ end
62
+
63
+ def report
64
+ report = {}
65
+
66
+ schema.open_api.paths.path.each do |path_name, path_item|
67
+ report[path_name] = {}
68
+ path_item._openapi_all_child_objects.each do |object_name, object|
69
+ next unless object.is_a?(OpenAPIParser::Schemas::Operation)
70
+
71
+ method = object_name.split('/').last&.downcase
72
+ next unless method
73
+
74
+ report[path_name][method] ||= {}
75
+
76
+ # TODO: check coverage on request params/body as well?
77
+
78
+ report[path_name][method]['responses'] ||= {}
79
+ object.responses.response.each do |response_status, _|
80
+ is_covered = @covered.dig(path_name, method, 'responses', response_status) || false
81
+ report[path_name][method]['responses'][response_status] = is_covered
82
+ end
83
+ if object.responses.default
84
+ is_default_covered = (@covered.dig(path_name, method, 'responses') || {}).any? do |status, is_covered|
85
+ is_covered && !object.responses.response.key?(status)
86
+ end
87
+ report[path_name][method]['responses']['default'] = is_default_covered
88
+ end
89
+ end
90
+ end
91
+
92
+ report
93
+ end
94
+
95
+ def report_flatten
96
+ self.class.flatten_report(report)
97
+ end
98
+ end
99
+ end
100
+ end
101
+
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module Utils
5
+ # Creates a Hash with indifferent access.
6
+ #
7
+ # (Copied from Sinatra)
8
+ def self.indifferent_hash
9
+ Hash.new { |hash,key| hash[key.to_s] if Symbol === key }
10
+ end
11
+
12
+ def self.deep_copy(from)
13
+ if from.is_a?(Hash)
14
+ h = Committee::Utils.indifferent_hash
15
+ from.each_pair do |k, v|
16
+ h[k] = deep_copy(v)
17
+ end
18
+ return h
19
+ end
20
+
21
+ if from.is_a?(Array)
22
+ return from.map{ |v| deep_copy(v) }
23
+ end
24
+
25
+ return from
26
+ end
27
+ end
28
+ end
@@ -2,12 +2,13 @@
2
2
 
3
3
  module Committee
4
4
  class ValidationError
5
- attr_reader :id, :message, :status
5
+ attr_reader :id, :message, :status, :request
6
6
 
7
- def initialize(status, id, message)
7
+ def initialize(status, id, message, request = nil)
8
8
  @status = status
9
9
  @id = id
10
10
  @message = message
11
+ @request = request
11
12
  end
12
13
 
13
14
  def error_body
@@ -42,7 +42,7 @@ describe Committee do
42
42
 
43
43
  $VERBOSE = true
44
44
  Committee.warn_deprecated "blah"
45
- assert_equal "blah\n", $stderr.string
45
+ assert_equal "[DEPRECATION] blah\n", $stderr.string
46
46
  ensure
47
47
  $stderr = old_stderr
48
48
  $VERBOSE = old_verbose
@@ -34,11 +34,27 @@ describe Committee::Middleware::RequestValidation do
34
34
  params = { "datetime_string" => "2016-04-01T16:00:00.000+09:00" }
35
35
 
36
36
  check_parameter = lambda { |env|
37
+ assert_equal DateTime, env['committee.query_hash']["datetime_string"].class
38
+ assert_equal String, env['rack.request.query_hash']["datetime_string"].class
39
+ [200, {}, []]
40
+ }
41
+
42
+ @app = new_rack_app_with_lambda(check_parameter, schema: open_api_3_schema, coerce_date_times: true, query_hash_key: "committee.query_hash")
43
+
44
+ get "/string_params_coercer", params
45
+ assert_equal 200, last_response.status
46
+ end
47
+
48
+ it "passes given a datetime and with coerce_date_times enabled on GET endpoint overwrite query_hash" do
49
+ params = { "datetime_string" => "2016-04-01T16:00:00.000+09:00" }
50
+
51
+ check_parameter = lambda { |env|
52
+ assert_equal nil, env['committee.query_hash']
37
53
  assert_equal DateTime, env['rack.request.query_hash']["datetime_string"].class
38
54
  [200, {}, []]
39
55
  }
40
56
 
41
- @app = new_rack_app_with_lambda(check_parameter, schema: open_api_3_schema, coerce_date_times: true)
57
+ @app = new_rack_app_with_lambda(check_parameter, schema: open_api_3_schema, coerce_date_times: true, query_hash_key: "rack.request.query_hash")
42
58
 
43
59
  get "/string_params_coercer", params
44
60
  assert_equal 200, last_response.status
@@ -154,7 +170,8 @@ describe Committee::Middleware::RequestValidation do
154
170
  }
155
171
 
156
172
  check_parameter = lambda { |env|
157
- hash = env['rack.request.query_hash']
173
+ # hash = env["committee.query_hash"] # 5.0.x-
174
+ hash = env["rack.request.query_hash"]
158
175
  assert_equal DateTime, hash['nested_array'].first['update_time'].class
159
176
  assert_equal 1, hash['nested_array'].first['per_page']
160
177
 
@@ -331,7 +348,7 @@ describe Committee::Middleware::RequestValidation do
331
348
  get "/coerce_path_params/#{not_an_integer}", nil
332
349
  end
333
350
 
334
- assert_match(/expected integer, but received String: abc/i, e.message)
351
+ assert_match(/expected integer, but received String: \"abc\"/i, e.message)
335
352
  end
336
353
 
337
354
  it "optionally raises an error" do
@@ -360,7 +377,8 @@ describe Committee::Middleware::RequestValidation do
360
377
 
361
378
  it "passes through a valid request for OpenAPI3" do
362
379
  check_parameter = lambda { |env|
363
- assert_equal 3, env['rack.request.query_hash']['limit']
380
+ # assert_equal 3, env['committee.query_hash']['limit'] #5.0.x-
381
+ assert_equal 3, env['rack.request.query_hash']['limit'] #5.0.x-
364
382
  [200, {}, []]
365
383
  }
366
384
 
@@ -374,7 +392,7 @@ describe Committee::Middleware::RequestValidation do
374
392
  get "/characters?limit=foo"
375
393
 
376
394
  assert_equal 400, last_response.status
377
- assert_match(/expected integer, but received String: foo/i, last_response.body)
395
+ assert_match(/expected integer, but received String: \\"foo\\"/i, last_response.body)
378
396
  end
379
397
 
380
398
  it "ignores errors when ignore_error: true" do
@@ -394,6 +412,51 @@ describe Committee::Middleware::RequestValidation do
394
412
  get "/coerce_path_params/1"
395
413
  end
396
414
 
415
+ it "corce string and save path hash" do
416
+ @app = new_rack_app_with_lambda(lambda do |env|
417
+ assert_equal env['committee.params']['integer'], 21
418
+ assert_equal env['committee.params'][:integer], 21
419
+ assert_equal env['committee.path_hash']['integer'], 21
420
+ assert_equal env['committee.path_hash'][:integer], 21
421
+ [204, {}, []]
422
+ end, schema: open_api_3_schema)
423
+
424
+ header "Content-Type", "application/json"
425
+ post '/parameter_option_test/21'
426
+ assert_equal 204, last_response.status
427
+ end
428
+
429
+ it "corce string and save request body hash" do
430
+ @app = new_rack_app_with_lambda(lambda do |env|
431
+ assert_equal env['committee.params']['integer'], 21 # use path parameter
432
+ assert_equal env['committee.params'][:integer], 21
433
+ assert_equal env['committee.request_body_hash']['integer'], 42
434
+ assert_equal env['committee.request_body_hash'][:integer], 42
435
+ [204, {}, []]
436
+ end, schema: open_api_3_schema)
437
+
438
+ params = {integer: 42}
439
+
440
+ header "Content-Type", "application/json"
441
+ post '/parameter_option_test/21', JSON.generate(params)
442
+ assert_equal 204, last_response.status
443
+ end
444
+
445
+ it "unpacker test" do
446
+ @app = new_rack_app_with_lambda(lambda do |env|
447
+ assert_equal env['committee.params']['integer'], 42
448
+ assert_equal env['committee.params'][:integer], 42
449
+ # overwrite by request body...
450
+ assert_equal env['rack.request.query_hash']['integer'], 42
451
+ # assert_equal env['rack.request.query_hash'][:integer], 42
452
+ [204, {}, []]
453
+ end, schema: open_api_3_schema, raise: true)
454
+
455
+ header "Content-Type", "application/x-www-form-urlencoded"
456
+ post '/validate?integer=21', "integer=42"
457
+ assert_equal 204, last_response.status
458
+ end
459
+
397
460
  it "OpenAPI3 raise not support method" do
398
461
  @app = new_rack_app(schema: open_api_3_schema)
399
462
 
@@ -408,7 +471,7 @@ describe Committee::Middleware::RequestValidation do
408
471
  [
409
472
  { check_header: true, description: 'valid value', value: 1, expected: { status: 200 } },
410
473
  { 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' } },
474
+ { check_header: true, description: 'invalid value', value: 'x', expected: { status: 400, error: 'expected integer, but received String: \\"x\\"' } },
412
475
 
413
476
  { check_header: false, description: 'valid value', value: 1, expected: { status: 200 } },
414
477
  { check_header: false, description: 'missing value', value: nil, expected: { status: 200 } },
@@ -457,6 +520,16 @@ describe Committee::Middleware::RequestValidation do
457
520
  end
458
521
  end
459
522
 
523
+ it 'does not suppress application error' do
524
+ @app = new_rack_app_with_lambda(lambda { |_|
525
+ JSON.load('-') # invalid json
526
+ }, schema: open_api_3_schema, raise: true)
527
+
528
+ assert_raises(JSON::ParserError) do
529
+ get "/error", nil
530
+ end
531
+ end
532
+
460
533
  private
461
534
 
462
535
  def new_rack_app(options = {})