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.
- checksums.yaml +7 -0
- data/bin/committee-stub +23 -0
- data/lib/committee/bin/committee_stub.rb +67 -0
- data/lib/committee/drivers/driver.rb +47 -0
- data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
- data/lib/committee/drivers/hyper_schema/link.rb +68 -0
- data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
- data/lib/committee/drivers/hyper_schema.rb +12 -0
- data/lib/committee/drivers/open_api_2/driver.rb +252 -0
- data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
- data/lib/committee/drivers/open_api_2/link.rb +36 -0
- data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
- data/lib/committee/drivers/open_api_2/schema.rb +26 -0
- data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
- data/lib/committee/drivers/open_api_2.rb +13 -0
- data/lib/committee/drivers/open_api_3/driver.rb +51 -0
- data/lib/committee/drivers/open_api_3/schema.rb +41 -0
- data/lib/committee/drivers/open_api_3.rb +11 -0
- data/lib/committee/drivers/schema.rb +23 -0
- data/lib/committee/drivers.rb +84 -0
- data/lib/committee/errors.rb +36 -0
- data/lib/committee/middleware/base.rb +57 -0
- data/lib/committee/middleware/request_validation.rb +41 -0
- data/lib/committee/middleware/response_validation.rb +58 -0
- data/lib/committee/middleware/stub.rb +75 -0
- data/lib/committee/middleware.rb +11 -0
- data/lib/committee/request_unpacker.rb +91 -0
- data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +79 -0
- data/lib/committee/schema_validator/hyper_schema/request_validator.rb +55 -0
- data/lib/committee/schema_validator/hyper_schema/response_generator.rb +102 -0
- data/lib/committee/schema_validator/hyper_schema/response_validator.rb +89 -0
- data/lib/committee/schema_validator/hyper_schema/router.rb +46 -0
- data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +105 -0
- data/lib/committee/schema_validator/hyper_schema.rb +119 -0
- data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +139 -0
- data/lib/committee/schema_validator/open_api_3/request_validator.rb +52 -0
- data/lib/committee/schema_validator/open_api_3/response_validator.rb +29 -0
- data/lib/committee/schema_validator/open_api_3/router.rb +45 -0
- data/lib/committee/schema_validator/open_api_3.rb +120 -0
- data/lib/committee/schema_validator/option.rb +60 -0
- data/lib/committee/schema_validator.rb +23 -0
- data/lib/committee/test/methods.rb +84 -0
- data/lib/committee/test/schema_coverage.rb +101 -0
- data/lib/committee/utils.rb +28 -0
- data/lib/committee/validation_error.rb +26 -0
- data/lib/committee/version.rb +5 -0
- data/lib/committee.rb +40 -0
- data/test/bin/committee_stub_test.rb +57 -0
- data/test/bin_test.rb +25 -0
- data/test/committee_test.rb +77 -0
- data/test/drivers/hyper_schema/driver_test.rb +49 -0
- data/test/drivers/hyper_schema/link_test.rb +56 -0
- data/test/drivers/open_api_2/driver_test.rb +156 -0
- data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
- data/test/drivers/open_api_2/link_test.rb +52 -0
- data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
- data/test/drivers/open_api_3/driver_test.rb +84 -0
- data/test/drivers_test.rb +154 -0
- data/test/middleware/base_test.rb +130 -0
- data/test/middleware/request_validation_open_api_3_test.rb +626 -0
- data/test/middleware/request_validation_test.rb +516 -0
- data/test/middleware/response_validation_open_api_3_test.rb +291 -0
- data/test/middleware/response_validation_test.rb +189 -0
- data/test/middleware/stub_test.rb +145 -0
- data/test/request_unpacker_test.rb +200 -0
- data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +111 -0
- data/test/schema_validator/hyper_schema/request_validator_test.rb +151 -0
- data/test/schema_validator/hyper_schema/response_generator_test.rb +142 -0
- data/test/schema_validator/hyper_schema/response_validator_test.rb +118 -0
- data/test/schema_validator/hyper_schema/router_test.rb +88 -0
- data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +137 -0
- data/test/schema_validator/open_api_3/operation_wrapper_test.rb +218 -0
- data/test/schema_validator/open_api_3/request_validator_test.rb +110 -0
- data/test/schema_validator/open_api_3/response_validator_test.rb +92 -0
- data/test/test/methods_new_version_test.rb +97 -0
- data/test/test/methods_test.rb +363 -0
- data/test/test/schema_coverage_test.rb +216 -0
- data/test/test_helper.rb +120 -0
- data/test/validation_error_test.rb +25 -0
- 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
|