committee 4.0.0 → 4.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/committee.rb +3 -3
  3. data/lib/committee/drivers/open_api_2/driver.rb +0 -1
  4. data/lib/committee/errors.rb +12 -0
  5. data/lib/committee/middleware/request_validation.rb +4 -11
  6. data/lib/committee/middleware/response_validation.rb +7 -7
  7. data/lib/committee/request_unpacker.rb +46 -60
  8. data/lib/committee/schema_validator/hyper_schema.rb +41 -27
  9. data/lib/committee/schema_validator/open_api_3.rb +34 -21
  10. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +8 -4
  11. data/lib/committee/schema_validator/open_api_3/router.rb +3 -1
  12. data/lib/committee/schema_validator/option.rb +22 -3
  13. data/lib/committee/test/methods.rb +27 -11
  14. data/lib/committee/test/schema_coverage.rb +101 -0
  15. data/lib/committee/utils.rb +28 -0
  16. data/lib/committee/validation_error.rb +3 -2
  17. data/test/bin/committee_stub_test.rb +5 -1
  18. data/test/committee_test.rb +1 -1
  19. data/test/middleware/base_test.rb +9 -3
  20. data/test/middleware/request_validation_open_api_3_test.rb +82 -6
  21. data/test/middleware/request_validation_test.rb +16 -0
  22. data/test/middleware/response_validation_open_api_3_test.rb +26 -2
  23. data/test/middleware/response_validation_test.rb +11 -0
  24. data/test/middleware/stub_test.rb +4 -0
  25. data/test/request_unpacker_test.rb +51 -110
  26. data/test/schema_validator/hyper_schema/router_test.rb +4 -0
  27. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +14 -3
  28. data/test/schema_validator/open_api_3/request_validator_test.rb +3 -0
  29. data/test/schema_validator/open_api_3/response_validator_test.rb +12 -5
  30. data/test/test/methods_new_version_test.rb +16 -4
  31. data/test/test/methods_test.rb +151 -7
  32. data/test/test/schema_coverage_test.rb +216 -0
  33. data/test/test_helper.rb +20 -0
  34. metadata +41 -10
@@ -72,6 +72,9 @@ describe Committee::SchemaValidator::OpenAPI3::RequestValidator do
72
72
  end
73
73
 
74
74
  def new_rack_app(options = {})
75
+ # TODO: delete when 5.0.0 released because default value changed
76
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
77
+
75
78
  Rack::Builder.new {
76
79
  use Committee::Middleware::RequestValidation, options
77
80
  run lambda { |_|
@@ -12,7 +12,12 @@ describe Committee::SchemaValidator::OpenAPI3::ResponseValidator do
12
12
 
13
13
  @path = '/validate'
14
14
  @method = 'post'
15
- @validator_option = Committee::SchemaValidator::Option.new({}, open_api_3_schema, :open_api_3)
15
+
16
+ # TODO: delete when 5.0.0 released because default value changed
17
+ options = {}
18
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
19
+
20
+ @validator_option = Committee::SchemaValidator::Option.new(options, open_api_3_schema, :open_api_3)
16
21
  end
17
22
 
18
23
  it "passes through a valid response" do
@@ -29,11 +34,12 @@ describe Committee::SchemaValidator::OpenAPI3::ResponseValidator do
29
34
  call_response_validator
30
35
  end
31
36
 
32
- it "passes through a valid response with no registered Content-Type with strict = true" do
37
+ it "raises InvalidResponse when a valid response with no registered body with strict option" do
33
38
  @headers = { "Content-Type" => "application/xml" }
34
- assert_raises(Committee::InvalidResponse) {
39
+ e = assert_raises(Committee::InvalidResponse) {
35
40
  call_response_validator(true)
36
41
  }
42
+ assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
37
43
  end
38
44
 
39
45
  it "passes through a valid response with no Content-Type" do
@@ -41,11 +47,12 @@ describe Committee::SchemaValidator::OpenAPI3::ResponseValidator do
41
47
  call_response_validator
42
48
  end
43
49
 
44
- it "passes through a valid response with no Content-Type with strict option" do
50
+ it "raises InvalidResponse when a valid response with no Content-Type headers with strict option" do
45
51
  @headers = {}
46
- assert_raises(Committee::InvalidResponse) {
52
+ e = assert_raises(Committee::InvalidResponse) {
47
53
  call_response_validator(true)
48
54
  }
55
+ assert_kind_of(OpenAPIParser::OpenAPIError, e.original_error)
49
56
  end
50
57
 
51
58
  it "passes through a valid list response" do
@@ -30,13 +30,16 @@ describe Committee::Test::Methods do
30
30
  @committee_schema = nil
31
31
 
32
32
  @committee_options = {schema: hyper_schema}
33
+
34
+ # TODO: delete when 5.0.0 released because default value changed
35
+ @committee_options[:parse_response_by_content_type] = false
33
36
  end
34
37
 
35
38
  describe "#assert_schema_conform" do
36
39
  it "passes through a valid response" do
37
40
  @app = new_rack_app(JSON.generate([ValidApp]))
38
41
  get "/apps"
39
- assert_schema_conform
42
+ assert_schema_conform(200)
40
43
  end
41
44
 
42
45
  it "passes with prefix" do
@@ -44,18 +47,27 @@ describe Committee::Test::Methods do
44
47
 
45
48
  @app = new_rack_app(JSON.generate([ValidApp]))
46
49
  get "/v1/apps"
47
- assert_schema_conform
50
+ assert_schema_conform(200)
48
51
  end
49
52
 
50
53
  it "detects an invalid response Content-Type" do
51
54
  @app = new_rack_app(JSON.generate([ValidApp]), 200, {})
52
55
  get "/apps"
53
56
  e = assert_raises(Committee::InvalidResponse) do
54
- assert_schema_conform
57
+ assert_schema_conform(200)
55
58
  end
56
59
  assert_match(/response header must be set to/i, e.message)
57
60
  end
58
61
 
62
+ it "it detects unexpected response code" do
63
+ @app = new_rack_app(JSON.generate([ValidApp]), 400)
64
+ get "/apps"
65
+ e = assert_raises(Committee::InvalidResponse) do
66
+ assert_schema_conform(200)
67
+ end
68
+ assert_match(/Expected `200` status code, but it was `400`/i, e.message)
69
+ end
70
+
59
71
  it "detects an invalid response Content-Type but ignore because it's not success status code" do
60
72
  @committee_options.merge!(validate_success_only: true)
61
73
  @app = new_rack_app(JSON.generate([ValidApp]), 400, {})
@@ -67,7 +79,7 @@ describe Committee::Test::Methods do
67
79
  @app = new_rack_app(JSON.generate([ValidApp]), 400, {})
68
80
  get "/apps"
69
81
  e = assert_raises(Committee::InvalidResponse) do
70
- assert_schema_conform
82
+ assert_schema_conform(400)
71
83
  end
72
84
  assert_match(/response header must be set to/i, e.message)
73
85
  end
@@ -28,7 +28,10 @@ describe Committee::Test::Methods do
28
28
  # our purposes here in testing the module.
29
29
  @committee_router = nil
30
30
  @committee_schema = nil
31
- @committee_options = nil
31
+ @committee_options = {}
32
+
33
+ # TODO: delete when 5.0.0 released because default value changed
34
+ @committee_options[:parse_response_by_content_type] = true
32
35
  end
33
36
 
34
37
  describe "Hyper-Schema" do
@@ -36,21 +39,21 @@ describe Committee::Test::Methods do
36
39
  sc = JsonSchema.parse!(hyper_schema_data)
37
40
  sc.expand_references!
38
41
  s = Committee::Drivers::HyperSchema::Driver.new.parse(sc)
39
- @committee_options = {schema: s}
42
+ @committee_options.merge!({schema: s})
40
43
  end
41
44
 
42
45
  describe "#assert_schema_conform" do
43
46
  it "passes through a valid response" do
44
47
  @app = new_rack_app(JSON.generate([ValidApp]))
45
48
  get "/apps"
46
- assert_schema_conform
49
+ assert_schema_conform(200)
47
50
  end
48
51
 
49
52
  it "detects an invalid response Content-Type" do
50
53
  @app = new_rack_app(JSON.generate([ValidApp]), {})
51
54
  get "/apps"
52
55
  e = assert_raises(Committee::InvalidResponse) do
53
- assert_schema_conform
56
+ assert_schema_conform(200)
54
57
  end
55
58
  assert_match(/response header must be set to/i, e.message)
56
59
  end
@@ -61,7 +64,8 @@ describe Committee::Test::Methods do
61
64
  _, err = capture_io do
62
65
  assert_schema_conform
63
66
  end
64
- assert_match(/\[DEPRECATION\]/i, err)
67
+ assert_match(/\[DEPRECATION\] Now assert_schema_conform check response schema only/i, err)
68
+ assert_match(/\[DEPRECATION\] Pass expected response status code/i, err)
65
69
  end
66
70
  end
67
71
 
@@ -120,7 +124,7 @@ describe Committee::Test::Methods do
120
124
 
121
125
  describe "OpenAPI3" do
122
126
  before do
123
- @committee_options = {schema: open_api_3_schema}
127
+ @committee_options.merge!({schema: open_api_3_schema})
124
128
 
125
129
  @correct_response = { string_1: :honoka }
126
130
  end
@@ -158,7 +162,8 @@ describe Committee::Test::Methods do
158
162
  _, err = capture_io do
159
163
  assert_schema_conform
160
164
  end
161
- assert_match(/\[DEPRECATION\]/i, err)
165
+ assert_match(/\[DEPRECATION\] Now assert_schema_conform check response schema only/i, err)
166
+ assert_match(/\[DEPRECATION\] Pass expected response status code/i, err)
162
167
  end
163
168
  end
164
169
 
@@ -224,6 +229,145 @@ describe Committee::Test::Methods do
224
229
  end
225
230
  assert_match(/`GET \/undefined` undefined in schema/i, e.message)
226
231
  end
232
+
233
+ it "raises error when path does not match prefix" do
234
+ @committee_options.merge!({prefix: '/api'})
235
+ @app = new_rack_app(JSON.generate(@correct_response))
236
+ get "/characters"
237
+ e = assert_raises(Committee::InvalidResponse) do
238
+ assert_response_schema_confirm
239
+ end
240
+ assert_match(/`GET \/characters` undefined in schema \(prefix: "\/api"\)/i, e.message)
241
+ end
242
+
243
+ describe 'coverage' do
244
+ before do
245
+ @schema_coverage = Committee::Test::SchemaCoverage.new(open_api_3_coverage_schema)
246
+ @committee_options.merge!(schema: open_api_3_coverage_schema, schema_coverage: @schema_coverage)
247
+
248
+ @app = new_rack_app(JSON.generate({ success: true }))
249
+ end
250
+ it 'records openapi coverage' do
251
+ get "/posts"
252
+ assert_response_schema_confirm
253
+ assert_equal({
254
+ '/threads/{id}' => {
255
+ 'get' => {
256
+ 'responses' => {
257
+ '200' => false,
258
+ },
259
+ },
260
+ },
261
+ '/posts' => {
262
+ 'get' => {
263
+ 'responses' => {
264
+ '200' => true,
265
+ '404' => false,
266
+ 'default' => false,
267
+ },
268
+ },
269
+ 'post' => {
270
+ 'responses' => {
271
+ '200' => false,
272
+ },
273
+ },
274
+ },
275
+ '/likes' => {
276
+ 'post' => {
277
+ 'responses' => {
278
+ '200' => false,
279
+ },
280
+ },
281
+ 'delete' => {
282
+ 'responses' => {
283
+ '200' => false,
284
+ },
285
+ },
286
+ },
287
+ }, @schema_coverage.report)
288
+ end
289
+
290
+ it 'can record openapi coverage correctly when prefix is set' do
291
+ @committee_options.merge!(prefix: '/api')
292
+ post "/api/likes"
293
+ assert_response_schema_confirm
294
+ assert_equal({
295
+ '/threads/{id}' => {
296
+ 'get' => {
297
+ 'responses' => {
298
+ '200' => false,
299
+ },
300
+ },
301
+ },
302
+ '/posts' => {
303
+ 'get' => {
304
+ 'responses' => {
305
+ '200' => false,
306
+ '404' => false,
307
+ 'default' => false,
308
+ },
309
+ },
310
+ 'post' => {
311
+ 'responses' => {
312
+ '200' => false,
313
+ },
314
+ },
315
+ },
316
+ '/likes' => {
317
+ 'post' => {
318
+ 'responses' => {
319
+ '200' => true,
320
+ },
321
+ },
322
+ 'delete' => {
323
+ 'responses' => {
324
+ '200' => false,
325
+ },
326
+ },
327
+ },
328
+ }, @schema_coverage.report)
329
+ end
330
+
331
+ it 'records openapi coverage correctly with path param' do
332
+ get "/threads/asd"
333
+ assert_response_schema_confirm
334
+ assert_equal({
335
+ '/threads/{id}' => {
336
+ 'get' => {
337
+ 'responses' => {
338
+ '200' => true,
339
+ },
340
+ },
341
+ },
342
+ '/posts' => {
343
+ 'get' => {
344
+ 'responses' => {
345
+ '200' => false,
346
+ '404' => false,
347
+ 'default' => false,
348
+ },
349
+ },
350
+ 'post' => {
351
+ 'responses' => {
352
+ '200' => false,
353
+ },
354
+ },
355
+ },
356
+ '/likes' => {
357
+ 'post' => {
358
+ 'responses' => {
359
+ '200' => false,
360
+ },
361
+ },
362
+ 'delete' => {
363
+ 'responses' => {
364
+ '200' => false,
365
+ },
366
+ },
367
+ },
368
+ }, @schema_coverage.report)
369
+ end
370
+ end
227
371
  end
228
372
  end
229
373
 
@@ -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