committee 4.2.1 → 4.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 238def2dc052574b0bb8022a3765df9a6f5c3b359ee34d9bcdbdccef7240748a
4
- data.tar.gz: 615f73429b3c9e4336cdfef38db5d4f9ec778298fdc96139f10d83f8d12cf296
3
+ metadata.gz: 19c70ee2e436755ddf7b0b59b213ab7c00b60e67ce52520b956b889121b24e4c
4
+ data.tar.gz: 06066adc6056231938d36d93a98c5d7318d9c1ff367784265d66c0eb271fb7b6
5
5
  SHA512:
6
- metadata.gz: 113e66a74a6e2214f53aaf572c8563f1d99b273e70ac0f96e1f77c3aaf44c0af4964468bd11bfaf906449db8fb9a7f32b7e6943aa11e5895b11c92e09481c62f
7
- data.tar.gz: 8c7fd8cddfea01a569a85ee1f49ff41e1854474b922c0cdeec0b96cfff57349fa9905d84cf8727b48fdfa3fd026d69b4f4493964e6d7b7917086777c1cdf89ad
6
+ metadata.gz: bfbcc7aa85c676291125ed16b20badadda3eb8d895b6834cdc884602345c6ed3a052ecbdd381615b8bbca49815d884407cf8cc2f8cbbe05702593b5cc171f568
7
+ data.tar.gz: cc454350c3d04969ad3f24c1922b887a6e319cea4f0a869b647ef439dbc9f5f83f3922bde82abdeb0994a540e53d5e68345e76d7291b408e42cd2f5a4df9eec1
@@ -16,9 +16,7 @@ module Committee
16
16
  end
17
17
 
18
18
  def self.warn_deprecated(message)
19
- if !$VERBOSE.nil?
20
- $stderr.puts(message)
21
- end
19
+ warn("[DEPRECATION] #{message}")
22
20
  end
23
21
  end
24
22
 
@@ -31,3 +29,4 @@ require_relative "committee/validation_error"
31
29
 
32
30
  require_relative "committee/bin/committee_stub"
33
31
  require_relative "committee/test/methods"
32
+ require_relative "committee/test/schema_coverage"
@@ -21,14 +21,14 @@ module Committee
21
21
  rescue Committee::BadRequest, Committee::InvalidRequest
22
22
  handle_exception($!, request.env)
23
23
  raise if @raise
24
- return @error_class.new(400, :bad_request, $!.message).render unless @ignore_error
24
+ return @error_class.new(400, :bad_request, $!.message, request).render unless @ignore_error
25
25
  rescue Committee::NotFound => e
26
26
  raise if @raise
27
- return @error_class.new(404, :not_found, e.message).render unless @ignore_error
27
+ return @error_class.new(404, :not_found, e.message, request).render unless @ignore_error
28
28
  rescue JSON::ParserError
29
29
  handle_exception($!, request.env)
30
30
  raise Committee::InvalidRequest if @raise
31
- return @error_class.new(400, :bad_request, "Request body wasn't valid JSON.").render unless @ignore_error
31
+ return @error_class.new(400, :bad_request, "Request body wasn't valid JSON.", request).render unless @ignore_error
32
32
  end
33
33
 
34
34
  @app.call(request.env)
@@ -42,7 +42,7 @@ module Committee
42
42
  if @error_handler.arity > 1
43
43
  @error_handler.call(e, env)
44
44
  else
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.'
45
+ Committee.warn_deprecated('Using `error_handler.call(exception)` is deprecated and will be change to `error_handler.call(exception, request.env)` in next major version.')
46
46
  @error_handler.call(e)
47
47
  end
48
48
  end
@@ -46,7 +46,7 @@ module Committee
46
46
  if @error_handler.arity > 1
47
47
  @error_handler.call(e, env)
48
48
  else
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.'
49
+ Committee.warn_deprecated('Using `error_handler.call(exception)` is deprecated and will be change to `error_handler.call(exception, request.env)` in next major version.')
50
50
  @error_handler.call(e)
51
51
  end
52
52
  end
@@ -17,6 +17,10 @@ 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
@@ -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
 
@@ -10,7 +10,7 @@ module Committee
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
 
@@ -19,11 +19,17 @@ module Committee
19
19
 
20
20
  def assert_response_schema_confirm
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 schema_coverage
29
+ operation_object = router.operation_object(request_object)
30
+ schema_coverage&.update_response_coverage!(operation_object.original_path, operation_object.http_method, status)
31
+ end
32
+
27
33
  schema_validator.response_validate(status, headers, [body], true) if validate_response?(status)
28
34
  end
29
35
 
@@ -55,10 +61,18 @@ module Committee
55
61
  @schema_validator ||= router.build_schema_validator(request_object)
56
62
  end
57
63
 
64
+ def schema_coverage
65
+ return nil unless schema.is_a?(Committee::Drivers::OpenAPI3::Schema)
66
+
67
+ coverage = committee_options.fetch(:schema_coverage, nil)
68
+
69
+ coverage.is_a?(SchemaCoverage) ? coverage : nil
70
+ end
71
+
58
72
  def old_behavior
59
73
  old_assert_behavior = committee_options.fetch(:old_assert_behavior, nil)
60
74
  if old_assert_behavior.nil?
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.'
75
+ 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.')
62
76
  old_assert_behavior = true
63
77
  end
64
78
  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
+
@@ -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
@@ -227,6 +227,145 @@ describe Committee::Test::Methods do
227
227
  end
228
228
  assert_match(/`GET \/undefined` undefined in schema/i, e.message)
229
229
  end
230
+
231
+ it "raises error when path does not match prefix" do
232
+ @committee_options.merge!({prefix: '/api'})
233
+ @app = new_rack_app(JSON.generate(@correct_response))
234
+ get "/characters"
235
+ e = assert_raises(Committee::InvalidResponse) do
236
+ assert_response_schema_confirm
237
+ end
238
+ assert_match(/`GET \/characters` undefined in schema \(prefix: "\/api"\)/i, e.message)
239
+ end
240
+
241
+ describe 'coverage' do
242
+ before do
243
+ @schema_coverage = Committee::Test::SchemaCoverage.new(open_api_3_coverage_schema)
244
+ @committee_options.merge!(schema: open_api_3_coverage_schema, schema_coverage: @schema_coverage)
245
+
246
+ @app = new_rack_app(JSON.generate({ success: true }))
247
+ end
248
+ it 'records openapi coverage' do
249
+ get "/posts"
250
+ assert_response_schema_confirm
251
+ assert_equal({
252
+ '/threads/{id}' => {
253
+ 'get' => {
254
+ 'responses' => {
255
+ '200' => false,
256
+ },
257
+ },
258
+ },
259
+ '/posts' => {
260
+ 'get' => {
261
+ 'responses' => {
262
+ '200' => true,
263
+ '404' => false,
264
+ 'default' => false,
265
+ },
266
+ },
267
+ 'post' => {
268
+ 'responses' => {
269
+ '200' => false,
270
+ },
271
+ },
272
+ },
273
+ '/likes' => {
274
+ 'post' => {
275
+ 'responses' => {
276
+ '200' => false,
277
+ },
278
+ },
279
+ 'delete' => {
280
+ 'responses' => {
281
+ '200' => false,
282
+ },
283
+ },
284
+ },
285
+ }, @schema_coverage.report)
286
+ end
287
+
288
+ it 'can record openapi coverage correctly when prefix is set' do
289
+ @committee_options.merge!(prefix: '/api')
290
+ post "/api/likes"
291
+ assert_response_schema_confirm
292
+ assert_equal({
293
+ '/threads/{id}' => {
294
+ 'get' => {
295
+ 'responses' => {
296
+ '200' => false,
297
+ },
298
+ },
299
+ },
300
+ '/posts' => {
301
+ 'get' => {
302
+ 'responses' => {
303
+ '200' => false,
304
+ '404' => false,
305
+ 'default' => false,
306
+ },
307
+ },
308
+ 'post' => {
309
+ 'responses' => {
310
+ '200' => false,
311
+ },
312
+ },
313
+ },
314
+ '/likes' => {
315
+ 'post' => {
316
+ 'responses' => {
317
+ '200' => true,
318
+ },
319
+ },
320
+ 'delete' => {
321
+ 'responses' => {
322
+ '200' => false,
323
+ },
324
+ },
325
+ },
326
+ }, @schema_coverage.report)
327
+ end
328
+
329
+ it 'records openapi coverage correctly with path param' do
330
+ get "/threads/asd"
331
+ assert_response_schema_confirm
332
+ assert_equal({
333
+ '/threads/{id}' => {
334
+ 'get' => {
335
+ 'responses' => {
336
+ '200' => true,
337
+ },
338
+ },
339
+ },
340
+ '/posts' => {
341
+ 'get' => {
342
+ 'responses' => {
343
+ '200' => false,
344
+ '404' => false,
345
+ 'default' => false,
346
+ },
347
+ },
348
+ 'post' => {
349
+ 'responses' => {
350
+ '200' => false,
351
+ },
352
+ },
353
+ },
354
+ '/likes' => {
355
+ 'post' => {
356
+ 'responses' => {
357
+ '200' => false,
358
+ },
359
+ },
360
+ 'delete' => {
361
+ 'responses' => {
362
+ '200' => false,
363
+ },
364
+ },
365
+ },
366
+ }, @schema_coverage.report)
367
+ end
368
+ end
230
369
  end
231
370
  end
232
371
 
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ describe Committee::Test::SchemaCoverage do
6
+ before do
7
+ @schema_coverage = Committee::Test::SchemaCoverage.new(open_api_3_coverage_schema)
8
+ end
9
+
10
+ describe 'recording coverage' do
11
+ def response_as_str(response)
12
+ [:path, :method, :status].map { |key| response[key] }.join(' ')
13
+ end
14
+
15
+ def uncovered_responses
16
+ @schema_coverage.report_flatten[:responses].select { |r| !r[:is_covered] }.map { |r| response_as_str(r) }
17
+ end
18
+
19
+ def covered_responses
20
+ @schema_coverage.report_flatten[:responses].select { |r| r[:is_covered] }.map { |r| response_as_str(r) }
21
+ end
22
+ it 'can record and report coverage properly' do
23
+ @schema_coverage.update_response_coverage!('/posts', 'get', '200')
24
+ assert_equal([
25
+ '/posts get 200',
26
+ ], covered_responses)
27
+ assert_equal([
28
+ '/threads/{id} get 200',
29
+ '/posts get 404',
30
+ '/posts get default',
31
+ '/posts post 200',
32
+ '/likes post 200',
33
+ '/likes delete 200',
34
+ ], uncovered_responses)
35
+
36
+ @schema_coverage.update_response_coverage!('/likes', 'post', '200')
37
+ assert_equal([
38
+ '/posts get 200',
39
+ '/likes post 200',
40
+ ], covered_responses)
41
+ assert_equal([
42
+ '/threads/{id} get 200',
43
+ '/posts get 404',
44
+ '/posts get default',
45
+ '/posts post 200',
46
+ '/likes delete 200',
47
+ ], uncovered_responses)
48
+
49
+ @schema_coverage.update_response_coverage!('/likes', 'delete', '200')
50
+ assert_equal([
51
+ '/posts get 200',
52
+ '/likes post 200',
53
+ '/likes delete 200',
54
+ ], covered_responses)
55
+ assert_equal([
56
+ '/threads/{id} get 200',
57
+ '/posts get 404',
58
+ '/posts get default',
59
+ '/posts post 200',
60
+ ], uncovered_responses)
61
+
62
+ @schema_coverage.update_response_coverage!('/posts', 'get', '422')
63
+ assert_equal([
64
+ '/posts get 200',
65
+ '/posts get default',
66
+ '/likes post 200',
67
+ '/likes delete 200',
68
+ ], covered_responses)
69
+ assert_equal([
70
+ '/threads/{id} get 200',
71
+ '/posts get 404',
72
+ '/posts post 200',
73
+ ], uncovered_responses)
74
+
75
+ assert_equal({
76
+ '/threads/{id}' => {
77
+ 'get' => {
78
+ 'responses' => {
79
+ '200' => false,
80
+ },
81
+ },
82
+ },
83
+ '/posts' => {
84
+ 'get' => {
85
+ 'responses' => {
86
+ '200' => true,
87
+ '404' => false,
88
+ 'default' => true,
89
+ },
90
+ },
91
+ 'post' => {
92
+ 'responses' => {
93
+ '200' => false,
94
+ },
95
+ },
96
+ },
97
+ '/likes' => {
98
+ 'post' => {
99
+ 'responses' => {
100
+ '200' => true,
101
+ },
102
+ },
103
+ 'delete' => {
104
+ 'responses' => {
105
+ '200' => true,
106
+ },
107
+ },
108
+ },
109
+ }, @schema_coverage.report)
110
+
111
+ @schema_coverage.update_response_coverage!('/posts', 'post', '200')
112
+ @schema_coverage.update_response_coverage!('/posts', 'get', '404')
113
+ @schema_coverage.update_response_coverage!('/threads/{id}', 'get', '200')
114
+ assert_equal([
115
+ '/threads/{id} get 200',
116
+ '/posts get 200',
117
+ '/posts get 404',
118
+ '/posts get default',
119
+ '/posts post 200',
120
+ '/likes post 200',
121
+ '/likes delete 200',
122
+ ], covered_responses)
123
+ assert_equal([], uncovered_responses)
124
+ end
125
+ end
126
+
127
+ describe '.merge_report' do
128
+ it 'can merge 2 coverage reports together' do
129
+ report = Committee::Test::SchemaCoverage.merge_report(
130
+ {
131
+ '/posts' => {
132
+ 'get' => {
133
+ 'responses' => {
134
+ '200' => true,
135
+ '404' => false,
136
+ },
137
+ },
138
+ 'post' => {
139
+ 'responses' => {
140
+ '200' => false,
141
+ },
142
+ },
143
+ },
144
+ '/likes' => {
145
+ 'post' => {
146
+ 'responses' => {
147
+ '200' => true,
148
+ },
149
+ },
150
+ },
151
+ },
152
+ {
153
+ '/posts' => {
154
+ 'get' => {
155
+ 'responses' => {
156
+ '200' => true,
157
+ '404' => true,
158
+ },
159
+ },
160
+ 'post' => {
161
+ 'responses' => {
162
+ '200' => false,
163
+ },
164
+ },
165
+ },
166
+ '/likes' => {
167
+ 'post' => {
168
+ 'responses' => {
169
+ '200' => false,
170
+ '400' => false,
171
+ },
172
+ },
173
+ },
174
+ '/users' => {
175
+ 'get' => {
176
+ 'responses' => {
177
+ '200' => true,
178
+ },
179
+ },
180
+ },
181
+ },
182
+ )
183
+
184
+ assert_equal({
185
+ '/posts' => {
186
+ 'get' => {
187
+ 'responses' => {
188
+ '200' => true,
189
+ '404' => true,
190
+ },
191
+ },
192
+ 'post' => {
193
+ 'responses' => {
194
+ '200' => false,
195
+ },
196
+ },
197
+ },
198
+ '/likes' => {
199
+ 'post' => {
200
+ 'responses' => {
201
+ '200' => true,
202
+ '400' => false,
203
+ },
204
+ },
205
+ },
206
+ '/users' => {
207
+ 'get' => {
208
+ 'responses' => {
209
+ '200' => true,
210
+ },
211
+ },
212
+ },
213
+ }, report)
214
+ end
215
+ end
216
+ end
@@ -59,6 +59,10 @@ def open_api_3_schema
59
59
  @open_api_3_schema ||= Committee::Drivers.load_from_file(open_api_3_schema_path)
60
60
  end
61
61
 
62
+ def open_api_3_coverage_schema
63
+ @open_api_3_coverage_schema ||= Committee::Drivers.load_from_file(open_api_3_coverage_schema_path)
64
+ end
65
+
62
66
  # Don't cache this because we'll often manipulate the created hash in tests.
63
67
  def hyper_schema_data
64
68
  JSON.parse(File.read(hyper_schema_schema_path))
@@ -85,6 +89,10 @@ def open_api_3_schema_path
85
89
  "./test/data/openapi3/normal.yaml"
86
90
  end
87
91
 
92
+ def open_api_3_coverage_schema_path
93
+ "./test/data/openapi3/coverage.yaml"
94
+ end
95
+
88
96
  def open_api_3_0_1_schema_path
89
97
  "./test/data/openapi3/3_0_1.yaml"
90
98
  end
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: 4.2.1
4
+ version: 4.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandur
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-11-07 00:00:00.000000000 Z
13
+ date: 2020-12-23 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: json_schema
@@ -238,6 +238,7 @@ files:
238
238
  - lib/committee/schema_validator/open_api_3/router.rb
239
239
  - lib/committee/schema_validator/option.rb
240
240
  - lib/committee/test/methods.rb
241
+ - lib/committee/test/schema_coverage.rb
241
242
  - lib/committee/validation_error.rb
242
243
  - test/bin/committee_stub_test.rb
243
244
  - test/bin_test.rb
@@ -268,6 +269,7 @@ files:
268
269
  - test/schema_validator/open_api_3/response_validator_test.rb
269
270
  - test/test/methods_new_version_test.rb
270
271
  - test/test/methods_test.rb
272
+ - test/test/schema_coverage_test.rb
271
273
  - test/test_helper.rb
272
274
  - test/validation_error_test.rb
273
275
  homepage: https://github.com/interagent/committee