committee 4.2.1 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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