committee_firetail 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/bin/committee-stub +23 -0
  3. data/lib/committee/bin/committee_stub.rb +67 -0
  4. data/lib/committee/drivers/driver.rb +47 -0
  5. data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
  6. data/lib/committee/drivers/hyper_schema/link.rb +68 -0
  7. data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
  8. data/lib/committee/drivers/hyper_schema.rb +12 -0
  9. data/lib/committee/drivers/open_api_2/driver.rb +252 -0
  10. data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
  11. data/lib/committee/drivers/open_api_2/link.rb +36 -0
  12. data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
  13. data/lib/committee/drivers/open_api_2/schema.rb +26 -0
  14. data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
  15. data/lib/committee/drivers/open_api_2.rb +13 -0
  16. data/lib/committee/drivers/open_api_3/driver.rb +51 -0
  17. data/lib/committee/drivers/open_api_3/schema.rb +41 -0
  18. data/lib/committee/drivers/open_api_3.rb +11 -0
  19. data/lib/committee/drivers/schema.rb +23 -0
  20. data/lib/committee/drivers.rb +84 -0
  21. data/lib/committee/errors.rb +36 -0
  22. data/lib/committee/middleware/base.rb +57 -0
  23. data/lib/committee/middleware/request_validation.rb +41 -0
  24. data/lib/committee/middleware/response_validation.rb +58 -0
  25. data/lib/committee/middleware/stub.rb +75 -0
  26. data/lib/committee/middleware.rb +11 -0
  27. data/lib/committee/request_unpacker.rb +91 -0
  28. data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +79 -0
  29. data/lib/committee/schema_validator/hyper_schema/request_validator.rb +55 -0
  30. data/lib/committee/schema_validator/hyper_schema/response_generator.rb +102 -0
  31. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +89 -0
  32. data/lib/committee/schema_validator/hyper_schema/router.rb +46 -0
  33. data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +105 -0
  34. data/lib/committee/schema_validator/hyper_schema.rb +119 -0
  35. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +139 -0
  36. data/lib/committee/schema_validator/open_api_3/request_validator.rb +52 -0
  37. data/lib/committee/schema_validator/open_api_3/response_validator.rb +29 -0
  38. data/lib/committee/schema_validator/open_api_3/router.rb +45 -0
  39. data/lib/committee/schema_validator/open_api_3.rb +120 -0
  40. data/lib/committee/schema_validator/option.rb +60 -0
  41. data/lib/committee/schema_validator.rb +23 -0
  42. data/lib/committee/test/methods.rb +84 -0
  43. data/lib/committee/test/schema_coverage.rb +101 -0
  44. data/lib/committee/utils.rb +28 -0
  45. data/lib/committee/validation_error.rb +26 -0
  46. data/lib/committee/version.rb +5 -0
  47. data/lib/committee.rb +40 -0
  48. data/test/bin/committee_stub_test.rb +57 -0
  49. data/test/bin_test.rb +25 -0
  50. data/test/committee_test.rb +77 -0
  51. data/test/drivers/hyper_schema/driver_test.rb +49 -0
  52. data/test/drivers/hyper_schema/link_test.rb +56 -0
  53. data/test/drivers/open_api_2/driver_test.rb +156 -0
  54. data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
  55. data/test/drivers/open_api_2/link_test.rb +52 -0
  56. data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
  57. data/test/drivers/open_api_3/driver_test.rb +84 -0
  58. data/test/drivers_test.rb +154 -0
  59. data/test/middleware/base_test.rb +130 -0
  60. data/test/middleware/request_validation_open_api_3_test.rb +626 -0
  61. data/test/middleware/request_validation_test.rb +516 -0
  62. data/test/middleware/response_validation_open_api_3_test.rb +291 -0
  63. data/test/middleware/response_validation_test.rb +189 -0
  64. data/test/middleware/stub_test.rb +145 -0
  65. data/test/request_unpacker_test.rb +200 -0
  66. data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +111 -0
  67. data/test/schema_validator/hyper_schema/request_validator_test.rb +151 -0
  68. data/test/schema_validator/hyper_schema/response_generator_test.rb +142 -0
  69. data/test/schema_validator/hyper_schema/response_validator_test.rb +118 -0
  70. data/test/schema_validator/hyper_schema/router_test.rb +88 -0
  71. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +137 -0
  72. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +218 -0
  73. data/test/schema_validator/open_api_3/request_validator_test.rb +110 -0
  74. data/test/schema_validator/open_api_3/response_validator_test.rb +92 -0
  75. data/test/test/methods_new_version_test.rb +97 -0
  76. data/test/test/methods_test.rb +363 -0
  77. data/test/test/schema_coverage_test.rb +216 -0
  78. data/test/test_helper.rb +120 -0
  79. data/test/validation_error_test.rb +25 -0
  80. metadata +328 -0
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class OpenAPI3
6
+ class OperationWrapper
7
+ # # @param request_operation [OpenAPIParser::RequestOperation]
8
+ def initialize(request_operation)
9
+ @request_operation = request_operation
10
+ end
11
+
12
+ def path_params
13
+ request_operation.path_params
14
+ end
15
+
16
+ def original_path
17
+ request_operation.original_path
18
+ end
19
+
20
+ def http_method
21
+ request_operation.http_method
22
+ end
23
+
24
+ def coerce_path_parameter(validator_option)
25
+ options = build_openapi_parser_path_option(validator_option)
26
+ return {} unless options.coerce_value
27
+
28
+ request_operation.validate_path_params(options)
29
+ rescue OpenAPIParser::OpenAPIError => e
30
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
31
+ end
32
+
33
+ # @param [Boolean] strict when not content_type or status code definition, raise error
34
+ def validate_response_params(status_code, headers, response_data, strict, check_header)
35
+ response_body = OpenAPIParser::RequestOperation::ValidatableResponseBody.new(status_code, response_data, headers)
36
+
37
+ return request_operation.validate_response_body(response_body, response_validate_options(strict, check_header))
38
+ rescue OpenAPIParser::OpenAPIError => e
39
+ raise Committee::InvalidResponse.new(e.message, original_error: e)
40
+ end
41
+
42
+ def validate_request_params(path_params, query_params, body_params, headers, validator_option)
43
+ ret, err = case request_operation.http_method
44
+ when 'get', 'delete', 'head'
45
+ validate_get_request_params(path_params, query_params, headers, validator_option)
46
+ when 'post', 'put', 'patch', 'options'
47
+ validate_post_request_params(path_params, query_params, body_params, headers, validator_option)
48
+ else
49
+ raise "Committee OpenAPI3 not support #{request_operation.http_method} method"
50
+ end
51
+ raise err if err
52
+ ret
53
+ end
54
+
55
+ def optional_body?
56
+ !request_operation.operation_object&.request_body&.required
57
+ end
58
+
59
+ def valid_request_content_type?(content_type)
60
+ if (request_body = request_operation.operation_object&.request_body)
61
+ !request_body.select_media_type(content_type).nil?
62
+ else
63
+ # if not exist request body object, all content_type allow.
64
+ # because request body object required content field so when it exists there're content type definition.
65
+ true
66
+ end
67
+ end
68
+
69
+ def request_content_types
70
+ request_operation.operation_object&.request_body&.content&.keys || []
71
+ end
72
+
73
+ private
74
+
75
+ attr_reader :request_operation
76
+
77
+ # @!attribute [r] request_operation
78
+ # @return [OpenAPIParser::RequestOperation]
79
+
80
+ # @return [OpenAPIParser::SchemaValidator::Options]
81
+ def build_openapi_parser_body_option(validator_option)
82
+ build_openapi_parser_option(validator_option, validator_option.coerce_form_params)
83
+ end
84
+
85
+ # @return [OpenAPIParser::SchemaValidator::Options]
86
+ def build_openapi_parser_path_option(validator_option)
87
+ build_openapi_parser_option(validator_option, validator_option.coerce_query_params)
88
+ end
89
+
90
+ # @return [OpenAPIParser::SchemaValidator::Options]
91
+ def build_openapi_parser_option(validator_option, coerce_value)
92
+ datetime_coerce_class = validator_option.coerce_date_times ? DateTime : nil
93
+ validate_header = validator_option.check_header
94
+ OpenAPIParser::SchemaValidator::Options.new(coerce_value: coerce_value,
95
+ datetime_coerce_class: datetime_coerce_class,
96
+ validate_header: validate_header)
97
+ end
98
+
99
+ def validate_get_request_params(path_params, query_params, headers, validator_option)
100
+ # bad performance because when we coerce value, same check
101
+ validate_path_and_query_params(path_params, query_params, headers, validator_option)
102
+ rescue OpenAPIParser::OpenAPIError => e
103
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
104
+ end
105
+
106
+ def validate_post_request_params(path_params, query_params, body_params, headers, validator_option)
107
+ content_type = headers['Content-Type'].to_s.split(";").first.to_s
108
+
109
+ # bad performance because when we coerce value, same check
110
+ validate_path_and_query_params(path_params, query_params, headers, validator_option)
111
+ request_operation.validate_request_body(content_type, body_params, build_openapi_parser_body_option(validator_option))
112
+ rescue => e
113
+ raise Committee::InvalidRequest.new(e.message, original_error: e)
114
+ end
115
+
116
+ def validate_path_and_query_params(path_params, query_params, headers, validator_option)
117
+ # it's currently impossible to validate path params and query params separately
118
+ # so we have to resort to this workaround
119
+
120
+ path_keys = path_params.keys.to_set
121
+ query_keys = query_params.keys.to_set
122
+
123
+ merged_params = query_params.merge(path_params)
124
+
125
+ request_operation.validate_request_parameter(merged_params, headers, build_openapi_parser_path_option(validator_option))
126
+
127
+ merged_params.each do |k, v|
128
+ path_params[k] = v if path_keys.include?(k)
129
+ query_params[k] = v if query_keys.include?(k)
130
+ end
131
+ end
132
+
133
+ def response_validate_options(strict, check_header)
134
+ ::OpenAPIParser::SchemaValidator::ResponseValidateOptions.new(strict: strict, validate_header: check_header)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class OpenAPI3
6
+ class RequestValidator
7
+ # @param [SchemaValidator::OpenAPI3::OperationWrapper] operation_object
8
+ # @param [Committee::SchemaValidator::Option] validator_option
9
+ def initialize(operation_object, validator_option:)
10
+ @operation_object = operation_object
11
+ @validator_option = validator_option
12
+ end
13
+
14
+ def call(request, path_params, query_params, body_params, headers)
15
+ content_type = ::Committee::SchemaValidator.request_media_type(request)
16
+ check_content_type(request, content_type) if @validator_option.check_content_type
17
+
18
+ @operation_object.validate_request_params(path_params, query_params, body_params, headers, @validator_option)
19
+ end
20
+
21
+ private
22
+
23
+ def check_content_type(request, content_type)
24
+ # support post, put, patch only
25
+ return true unless request.post? || request.put? || request.patch?
26
+ return true if @operation_object.valid_request_content_type?(content_type)
27
+ return true if @operation_object.optional_body? && empty_request?(request)
28
+
29
+ message = if valid_content_types.size > 1
30
+ types = valid_content_types.map {|x| %{"#{x}"} }.join(', ')
31
+ %{"Content-Type" request header must be set to any of the following: [#{types}].}
32
+ else
33
+ %{"Content-Type" request header must be set to "#{valid_content_types.first}".}
34
+ end
35
+ raise Committee::InvalidRequest, message
36
+ end
37
+
38
+ def valid_content_types
39
+ @operation_object&.request_content_types
40
+ end
41
+
42
+ def empty_request?(request)
43
+ return true if !request.body
44
+
45
+ data = request.body.read
46
+ request.body.rewind
47
+ data.empty?
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class OpenAPI3
6
+ class ResponseValidator
7
+ attr_reader :validate_success_only
8
+
9
+ # @param [Committee::SchemaValidator::Options] validator_option
10
+ # @param [Committee::SchemaValidator::OpenAPI3::OperationWrapper] operation_wrapper
11
+ def initialize(operation_wrapper, validator_option)
12
+ @operation_wrapper = operation_wrapper
13
+ @validate_success_only = validator_option.validate_success_only
14
+ @check_header = validator_option.check_header
15
+ end
16
+
17
+ def call(status, headers, response_data, strict)
18
+ return unless Committee::Middleware::ResponseValidation.validate?(status, validate_success_only)
19
+
20
+ operation_wrapper.validate_response_params(status, headers, response_data, strict, check_header)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :operation_wrapper, :check_header
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class OpenAPI3
6
+ class Router
7
+ # @param [Committee::SchemaValidator::Option] validator_option
8
+ def initialize(schema, validator_option)
9
+ @schema = schema
10
+ @prefix_regexp = ::Committee::SchemaValidator.build_prefix_regexp(validator_option.prefix)
11
+ @validator_option = validator_option
12
+ end
13
+
14
+ def includes_request?(request)
15
+ return true unless @prefix_regexp
16
+
17
+ prefix_request?(request)
18
+ end
19
+
20
+ def build_schema_validator(request)
21
+ Committee::SchemaValidator::OpenAPI3.new(self, request, @validator_option)
22
+ end
23
+
24
+ def operation_object(request)
25
+ return nil unless includes_request?(request)
26
+
27
+ path = request.path
28
+ path = path.gsub(@prefix_regexp, '') if @prefix_regexp
29
+
30
+ request_method = request.request_method.downcase
31
+
32
+ @schema.operation_object(path, request_method)
33
+ end
34
+
35
+ private
36
+
37
+ def prefix_request?(request)
38
+ return false unless @prefix_regexp
39
+
40
+ request.path =~ @prefix_regexp
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class OpenAPI3
6
+ # @param [Committee::SchemaValidator::Option] validator_option
7
+ def initialize(router, request, validator_option)
8
+ @router = router
9
+ @request = request
10
+ @operation_object = router.operation_object(request)
11
+ @validator_option = validator_option
12
+ end
13
+
14
+ def request_validate(request)
15
+ return unless link_exist?
16
+
17
+ request_unpack(request)
18
+ request_schema_validation(request)
19
+
20
+ copy_coerced_data_to_params(request)
21
+ end
22
+
23
+ def response_validate(status, headers, response, test_method = false)
24
+ full_body = +""
25
+ response.each do |chunk|
26
+ full_body << chunk
27
+ end
28
+
29
+ parse_to_json = !validator_option.parse_response_by_content_type ||
30
+ headers.fetch('Content-Type', nil)&.start_with?('application/json')
31
+ data = if parse_to_json
32
+ full_body.empty? ? {} : JSON.parse(full_body)
33
+ else
34
+ full_body
35
+ end
36
+
37
+ # TODO: refactoring name
38
+ strict = test_method
39
+ Committee::SchemaValidator::OpenAPI3::ResponseValidator.
40
+ new(@operation_object, validator_option).
41
+ call(status, headers, data, strict)
42
+ end
43
+
44
+ def link_exist?
45
+ !@operation_object.nil?
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :validator_option
51
+
52
+ def coerce_path_params
53
+ return Committee::Utils.indifferent_hash unless validator_option.coerce_path_params
54
+ Committee::RequestUnpacker.indifferent_params(@operation_object.coerce_path_parameter(@validator_option))
55
+ end
56
+
57
+ def request_schema_validation(request)
58
+ return unless @operation_object
59
+
60
+ validator = Committee::SchemaValidator::OpenAPI3::RequestValidator.new(@operation_object, validator_option: validator_option)
61
+ validator.call(request, path_params(request), query_params(request), body_params(request), header(request))
62
+ end
63
+
64
+ def path_params(request)
65
+ request.env[validator_option.path_hash_key]
66
+ end
67
+
68
+ def query_params(request)
69
+ request.env[validator_option.query_hash_key]
70
+ end
71
+
72
+ def body_params(request)
73
+ request.env[validator_option.request_body_hash_key]
74
+ end
75
+
76
+ def header(request)
77
+ request.env[validator_option.headers_key]
78
+ end
79
+
80
+ def request_unpack(request)
81
+ unpacker = Committee::RequestUnpacker.new(
82
+ allow_form_params: validator_option.allow_form_params,
83
+ allow_get_body: validator_option.allow_get_body,
84
+ allow_query_params: validator_option.allow_query_params,
85
+ optimistic_json: validator_option.optimistic_json,
86
+ )
87
+
88
+ request.env[validator_option.headers_key] = unpacker.unpack_headers(request)
89
+
90
+ request_param, is_form_params = unpacker.unpack_request_params(request)
91
+ request.env[validator_option.request_body_hash_key] = request_param
92
+ request.env[validator_option.path_hash_key] = coerce_path_params
93
+
94
+ query_param = unpacker.unpack_query_params(request)
95
+ query_param.merge!(request_param) if request.get? && validator_option.allow_get_body
96
+ request.env[validator_option.query_hash_key] = query_param
97
+ end
98
+
99
+ def copy_coerced_data_to_params(request)
100
+ order = if validator_option.parameter_overwite_by_rails_rule
101
+ # (high priority) path_hash_key -> query_param -> request_body_hash
102
+ [validator_option.request_body_hash_key, validator_option.query_hash_key, validator_option.path_hash_key]
103
+ else
104
+ # (high priority) path_hash_key -> request_body_hash -> query_param
105
+ [validator_option.query_hash_key, validator_option.request_body_hash_key, validator_option.path_hash_key]
106
+ end
107
+
108
+ request.env[validator_option.params_key] = Committee::Utils.indifferent_hash
109
+ order.each do |key|
110
+ request.env[validator_option.params_key].merge!(Committee::Utils.deep_copy(request.env[key]))
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ require_relative "open_api_3/router"
118
+ require_relative "open_api_3/operation_wrapper"
119
+ require_relative "open_api_3/request_validator"
120
+ require_relative "open_api_3/response_validator"
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class Option
6
+ # Boolean Options
7
+ attr_reader :allow_form_params,
8
+ :allow_get_body,
9
+ :allow_query_params,
10
+ :check_content_type,
11
+ :check_header,
12
+ :coerce_date_times,
13
+ :coerce_form_params,
14
+ :coerce_path_params,
15
+ :coerce_query_params,
16
+ :coerce_recursive,
17
+ :optimistic_json,
18
+ :validate_success_only,
19
+ :parse_response_by_content_type,
20
+ :parameter_overwite_by_rails_rule
21
+
22
+ # Non-boolean options:
23
+ attr_reader :headers_key,
24
+ :params_key,
25
+ :query_hash_key,
26
+ :request_body_hash_key,
27
+ :path_hash_key,
28
+ :prefix
29
+
30
+ def initialize(options, schema, schema_type)
31
+ # Non-boolean options
32
+ @headers_key = options[:headers_key] || "committee.headers"
33
+ @params_key = options[:params_key] || "committee.params"
34
+ @query_hash_key = options[:query_hash_key] || "committee.query_hash"
35
+ @path_hash_key = options[:path_hash_key] || "committee.path_hash"
36
+ @request_body_hash_key = options[:request_body_hash_key] || "committee.request_body_hash"
37
+
38
+ @prefix = options[:prefix]
39
+
40
+ # Boolean options and have a common value by default
41
+ @allow_form_params = options.fetch(:allow_form_params, true)
42
+ @allow_query_params = options.fetch(:allow_query_params, true)
43
+ @check_content_type = options.fetch(:check_content_type, true)
44
+ @check_header = options.fetch(:check_header, true)
45
+ @coerce_recursive = options.fetch(:coerce_recursive, true)
46
+ @optimistic_json = options.fetch(:optimistic_json, false)
47
+ @parse_response_by_content_type = options.fetch(:parse_response_by_content_type, true)
48
+ @parameter_overwite_by_rails_rule = options.fetch(:parameter_overwite_by_rails_rule, true)
49
+
50
+ # Boolean options and have a different value by default
51
+ @allow_get_body = options.fetch(:allow_get_body, schema.driver.default_allow_get_body)
52
+ @coerce_date_times = options.fetch(:coerce_date_times, schema.driver.default_coerce_date_times)
53
+ @coerce_form_params = options.fetch(:coerce_form_params, schema.driver.default_coerce_form_params)
54
+ @coerce_path_params = options.fetch(:coerce_path_params, schema.driver.default_path_params)
55
+ @coerce_query_params = options.fetch(:coerce_query_params, schema.driver.default_query_params)
56
+ @validate_success_only = options.fetch(:validate_success_only, schema.driver.default_validate_success_only)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module SchemaValidator
5
+ class << self
6
+ def request_media_type(request)
7
+ request.media_type.to_s
8
+ end
9
+
10
+ # @param [String] prefix
11
+ # @return [Regexp]
12
+ def build_prefix_regexp(prefix)
13
+ return nil unless prefix
14
+
15
+ /\A#{Regexp.escape(prefix)}/.freeze
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ require_relative "schema_validator/hyper_schema"
22
+ require_relative "schema_validator/open_api_3"
23
+ require_relative "schema_validator/option"
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committee
4
+ module Test
5
+ module Methods
6
+ def assert_schema_conform(expected_status = nil)
7
+ assert_request_schema_confirm unless old_behavior
8
+ assert_response_schema_confirm(expected_status)
9
+ end
10
+
11
+ def assert_request_schema_confirm
12
+ unless schema_validator.link_exist?
13
+ request = "`#{request_object.request_method} #{request_object.path_info}` undefined in schema (prefix: #{committee_options[:prefix].inspect})."
14
+ raise Committee::InvalidRequest.new(request)
15
+ end
16
+
17
+ schema_validator.request_validate(request_object)
18
+ end
19
+
20
+ def assert_response_schema_confirm(expected_status = nil)
21
+ unless schema_validator.link_exist?
22
+ response = "`#{request_object.request_method} #{request_object.path_info}` undefined in schema (prefix: #{committee_options[:prefix].inspect})."
23
+ raise Committee::InvalidResponse.new(response)
24
+ end
25
+
26
+ status, headers, body = response_data
27
+
28
+ if expected_status.nil?
29
+ Committee.need_good_option('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
+
40
+ schema_validator.response_validate(status, headers, [body], true) if validate_response?(status)
41
+ end
42
+
43
+ def committee_options
44
+ raise "please set options"
45
+ end
46
+
47
+ def request_object
48
+ raise "please set object like 'last_request'"
49
+ end
50
+
51
+ def response_data
52
+ raise "please set response data like 'last_response.status, last_response.headers, last_response.body'"
53
+ end
54
+
55
+ def validate_response?(status)
56
+ Committee::Middleware::ResponseValidation.validate?(status, committee_options.fetch(:validate_success_only, false))
57
+ end
58
+
59
+ def schema
60
+ @schema ||= Committee::Middleware::Base.get_schema(committee_options)
61
+ end
62
+
63
+ def router
64
+ @router ||= schema.build_router(committee_options)
65
+ end
66
+
67
+ def schema_validator
68
+ @schema_validator ||= router.build_schema_validator(request_object)
69
+ end
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
+
79
+ def old_behavior
80
+ committee_options.fetch(:old_assert_behavior, false)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -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