committee 3.3.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/committee/drivers/open_api_2/driver.rb +1 -2
  3. data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +1 -1
  4. data/lib/committee/drivers.rb +22 -10
  5. data/lib/committee/errors.rb +12 -0
  6. data/lib/committee/middleware/base.rb +5 -4
  7. data/lib/committee/middleware/request_validation.rb +4 -18
  8. data/lib/committee/middleware/response_validation.rb +15 -16
  9. data/lib/committee/request_unpacker.rb +46 -60
  10. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +8 -2
  11. data/lib/committee/schema_validator/hyper_schema.rb +41 -27
  12. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +44 -37
  13. data/lib/committee/schema_validator/open_api_3/request_validator.rb +11 -2
  14. data/lib/committee/schema_validator/open_api_3/router.rb +3 -1
  15. data/lib/committee/schema_validator/open_api_3.rb +52 -26
  16. data/lib/committee/schema_validator/option.rb +14 -3
  17. data/lib/committee/schema_validator.rb +1 -1
  18. data/lib/committee/test/methods.rb +27 -16
  19. data/lib/committee/test/schema_coverage.rb +101 -0
  20. data/lib/committee/utils.rb +28 -0
  21. data/lib/committee/validation_error.rb +3 -2
  22. data/lib/committee/version.rb +5 -0
  23. data/lib/committee.rb +11 -4
  24. data/test/bin/committee_stub_test.rb +5 -1
  25. data/test/committee_test.rb +29 -3
  26. data/test/drivers/open_api_3/driver_test.rb +1 -1
  27. data/test/drivers_test.rb +20 -7
  28. data/test/middleware/base_test.rb +9 -10
  29. data/test/middleware/request_validation_open_api_3_test.rb +175 -18
  30. data/test/middleware/request_validation_test.rb +20 -28
  31. data/test/middleware/response_validation_open_api_3_test.rb +96 -7
  32. data/test/middleware/response_validation_test.rb +21 -26
  33. data/test/middleware/stub_test.rb +4 -0
  34. data/test/request_unpacker_test.rb +51 -110
  35. data/test/schema_validator/hyper_schema/response_validator_test.rb +10 -0
  36. data/test/schema_validator/hyper_schema/router_test.rb +4 -0
  37. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +1 -1
  38. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +72 -20
  39. data/test/schema_validator/open_api_3/request_validator_test.rb +27 -0
  40. data/test/schema_validator/open_api_3/response_validator_test.rb +26 -5
  41. data/test/test/methods_new_version_test.rb +17 -5
  42. data/test/test/methods_test.rb +155 -31
  43. data/test/test/schema_coverage_test.rb +216 -0
  44. data/test/test_helper.rb +34 -4
  45. metadata +47 -15
@@ -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,33 +39,24 @@ 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
57
-
58
- it "outputs deprecation warning" do
59
- @app = new_rack_app(JSON.generate([ValidApp]))
60
- get "/apps"
61
- _, err = capture_io do
62
- assert_schema_conform
63
- end
64
- assert_match(/\[DEPRECATION\]/i, err)
65
- end
66
60
  end
67
61
 
68
62
  describe "assert_request_schema_confirm" do
@@ -95,14 +89,14 @@ describe Committee::Test::Methods do
95
89
  it "passes through a valid response" do
96
90
  @app = new_rack_app(JSON.generate([ValidApp]))
97
91
  get "/apps"
98
- assert_response_schema_confirm
92
+ assert_response_schema_confirm(200)
99
93
  end
100
94
 
101
95
  it "detects an invalid response Content-Type" do
102
96
  @app = new_rack_app(JSON.generate([ValidApp]), {})
103
97
  get "/apps"
104
98
  e = assert_raises(Committee::InvalidResponse) do
105
- assert_response_schema_confirm
99
+ assert_response_schema_confirm(200)
106
100
  end
107
101
  assert_match(/response header must be set to/i, e.message)
108
102
  end
@@ -120,7 +114,7 @@ describe Committee::Test::Methods do
120
114
 
121
115
  describe "OpenAPI3" do
122
116
  before do
123
- @committee_options = {schema: open_api_3_schema}
117
+ @committee_options.merge!({schema: open_api_3_schema})
124
118
 
125
119
  @correct_response = { string_1: :honoka }
126
120
  end
@@ -129,14 +123,14 @@ describe Committee::Test::Methods do
129
123
  it "passes through a valid response" do
130
124
  @app = new_rack_app(JSON.generate(@correct_response))
131
125
  get "/characters"
132
- assert_schema_conform
126
+ assert_schema_conform(200)
133
127
  end
134
128
 
135
129
  it "detects an invalid response Content-Type" do
136
130
  @app = new_rack_app(JSON.generate([@correct_response]), {})
137
131
  get "/characters"
138
132
  e = assert_raises(Committee::InvalidResponse) do
139
- assert_schema_conform
133
+ assert_schema_conform(200)
140
134
  end
141
135
  assert_match(/response definition does not exist/i, e.message)
142
136
  end
@@ -147,19 +141,10 @@ describe Committee::Test::Methods do
147
141
  get "/characters"
148
142
 
149
143
  e = assert_raises(Committee::InvalidResponse) do
150
- assert_schema_conform
144
+ assert_schema_conform(419)
151
145
  end
152
146
  assert_match(/status code definition does not exist/i, e.message)
153
147
  end
154
-
155
- it "outputs deprecation warning" do
156
- @app = new_rack_app(JSON.generate(@correct_response))
157
- get "/characters"
158
- _, err = capture_io do
159
- assert_schema_conform
160
- end
161
- assert_match(/\[DEPRECATION\]/i, err)
162
- end
163
148
  end
164
149
 
165
150
  describe "assert_request_schema_confirm" do
@@ -193,14 +178,14 @@ describe Committee::Test::Methods do
193
178
  it "passes through a valid response" do
194
179
  @app = new_rack_app(JSON.generate(@correct_response))
195
180
  get "/characters"
196
- assert_response_schema_confirm
181
+ assert_response_schema_confirm(200)
197
182
  end
198
183
 
199
184
  it "detects an invalid response Content-Type" do
200
185
  @app = new_rack_app(JSON.generate([@correct_response]), {})
201
186
  get "/characters"
202
187
  e = assert_raises(Committee::InvalidResponse) do
203
- assert_response_schema_confirm
188
+ assert_response_schema_confirm(200)
204
189
  end
205
190
  assert_match(/response definition does not exist/i, e.message)
206
191
  end
@@ -211,7 +196,7 @@ describe Committee::Test::Methods do
211
196
  get "/characters"
212
197
 
213
198
  e = assert_raises(Committee::InvalidResponse) do
214
- assert_response_schema_confirm
199
+ assert_response_schema_confirm(419)
215
200
  end
216
201
  assert_match(/status code definition does not exist/i, e.message)
217
202
  end
@@ -224,6 +209,145 @@ describe Committee::Test::Methods do
224
209
  end
225
210
  assert_match(/`GET \/undefined` undefined in schema/i, e.message)
226
211
  end
212
+
213
+ it "raises error when path does not match prefix" do
214
+ @committee_options.merge!({prefix: '/api'})
215
+ @app = new_rack_app(JSON.generate(@correct_response))
216
+ get "/characters"
217
+ e = assert_raises(Committee::InvalidResponse) do
218
+ assert_response_schema_confirm
219
+ end
220
+ assert_match(/`GET \/characters` undefined in schema \(prefix: "\/api"\)/i, e.message)
221
+ end
222
+
223
+ describe 'coverage' do
224
+ before do
225
+ @schema_coverage = Committee::Test::SchemaCoverage.new(open_api_3_coverage_schema)
226
+ @committee_options.merge!(schema: open_api_3_coverage_schema, schema_coverage: @schema_coverage)
227
+
228
+ @app = new_rack_app(JSON.generate({ success: true }))
229
+ end
230
+ it 'records openapi coverage' do
231
+ get "/posts"
232
+ assert_response_schema_confirm(200)
233
+ assert_equal({
234
+ '/threads/{id}' => {
235
+ 'get' => {
236
+ 'responses' => {
237
+ '200' => false,
238
+ },
239
+ },
240
+ },
241
+ '/posts' => {
242
+ 'get' => {
243
+ 'responses' => {
244
+ '200' => true,
245
+ '404' => false,
246
+ 'default' => false,
247
+ },
248
+ },
249
+ 'post' => {
250
+ 'responses' => {
251
+ '200' => false,
252
+ },
253
+ },
254
+ },
255
+ '/likes' => {
256
+ 'post' => {
257
+ 'responses' => {
258
+ '200' => false,
259
+ },
260
+ },
261
+ 'delete' => {
262
+ 'responses' => {
263
+ '200' => false,
264
+ },
265
+ },
266
+ },
267
+ }, @schema_coverage.report)
268
+ end
269
+
270
+ it 'can record openapi coverage correctly when prefix is set' do
271
+ @committee_options.merge!(prefix: '/api')
272
+ post "/api/likes"
273
+ assert_response_schema_confirm(200)
274
+ assert_equal({
275
+ '/threads/{id}' => {
276
+ 'get' => {
277
+ 'responses' => {
278
+ '200' => false,
279
+ },
280
+ },
281
+ },
282
+ '/posts' => {
283
+ 'get' => {
284
+ 'responses' => {
285
+ '200' => false,
286
+ '404' => false,
287
+ 'default' => false,
288
+ },
289
+ },
290
+ 'post' => {
291
+ 'responses' => {
292
+ '200' => false,
293
+ },
294
+ },
295
+ },
296
+ '/likes' => {
297
+ 'post' => {
298
+ 'responses' => {
299
+ '200' => true,
300
+ },
301
+ },
302
+ 'delete' => {
303
+ 'responses' => {
304
+ '200' => false,
305
+ },
306
+ },
307
+ },
308
+ }, @schema_coverage.report)
309
+ end
310
+
311
+ it 'records openapi coverage correctly with path param' do
312
+ get "/threads/asd"
313
+ assert_response_schema_confirm(200)
314
+ assert_equal({
315
+ '/threads/{id}' => {
316
+ 'get' => {
317
+ 'responses' => {
318
+ '200' => true,
319
+ },
320
+ },
321
+ },
322
+ '/posts' => {
323
+ 'get' => {
324
+ 'responses' => {
325
+ '200' => false,
326
+ '404' => false,
327
+ 'default' => false,
328
+ },
329
+ },
330
+ 'post' => {
331
+ 'responses' => {
332
+ '200' => false,
333
+ },
334
+ },
335
+ },
336
+ '/likes' => {
337
+ 'post' => {
338
+ 'responses' => {
339
+ '200' => false,
340
+ },
341
+ },
342
+ 'delete' => {
343
+ 'responses' => {
344
+ '200' => false,
345
+ },
346
+ },
347
+ },
348
+ }, @schema_coverage.report)
349
+ end
350
+ end
227
351
  end
228
352
  end
229
353
 
@@ -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
data/test/test_helper.rb CHANGED
@@ -11,8 +11,10 @@ SimpleCov.start do
11
11
  add_filter "/test/"
12
12
 
13
13
  # This library has a pretty modest number of lines, so let's try to stick
14
- # to a 100% coverage target for a while and see what happens.
15
- minimum_coverage 100
14
+ # to a 99% coverage target for a while and see what happens.
15
+ # We can't use 100% because old rack version doesn't support media_type and it's not testable :(
16
+ # https://github.com/interagent/committee/pull/360/files#diff-ce1125b6594690a88a70dbe2869f7fcfa2962c2bca80751f3720888920e2dfabR54
17
+ minimum_coverage 99
16
18
  end
17
19
 
18
20
  require "minitest"
@@ -55,8 +57,16 @@ def open_api_2_schema
55
57
  @open_api_2_schema ||= Committee::Drivers.load_from_file(open_api_2_schema_path)
56
58
  end
57
59
 
60
+ def open_api_2_form_schema
61
+ @open_api_2_form_schema ||= Committee::Drivers.load_from_file(open_api_2_form_schema_path)
62
+ end
63
+
58
64
  def open_api_3_schema
59
- @open_api_3_schema ||= Committee::Drivers.load_from_data(open_api_3_data)
65
+ @open_api_3_schema ||= Committee::Drivers.load_from_file(open_api_3_schema_path, parser_options:{strict_reference_validation: true})
66
+ end
67
+
68
+ def open_api_3_coverage_schema
69
+ @open_api_3_coverage_schema ||= Committee::Drivers.load_from_file(open_api_3_coverage_schema_path, parser_options:{strict_reference_validation: true})
60
70
  end
61
71
 
62
72
  # Don't cache this because we'll often manipulate the created hash in tests.
@@ -69,8 +79,16 @@ def open_api_2_data
69
79
  JSON.parse(File.read(open_api_2_schema_path))
70
80
  end
71
81
 
82
+ def open_api_2_form_data
83
+ JSON.parse(File.read(open_api_2_form_schema_path))
84
+ end
85
+
72
86
  def open_api_3_data
73
- YAML.load_file(open_api_3_schema_path)
87
+ if YAML.respond_to?(:unsafe_load_file)
88
+ YAML.unsafe_load_file(open_api_3_schema_path)
89
+ else
90
+ YAML.load_file(open_api_3_schema_path)
91
+ end
74
92
  end
75
93
 
76
94
  def hyper_schema_schema_path
@@ -81,10 +99,22 @@ def open_api_2_schema_path
81
99
  "./test/data/openapi2/petstore-expanded.json"
82
100
  end
83
101
 
102
+ def open_api_2_form_schema_path
103
+ "./test/data/openapi2/petstore-expanded-form.json"
104
+ end
105
+
84
106
  def open_api_3_schema_path
85
107
  "./test/data/openapi3/normal.yaml"
86
108
  end
87
109
 
110
+ def open_api_3_coverage_schema_path
111
+ "./test/data/openapi3/coverage.yaml"
112
+ end
113
+
88
114
  def open_api_3_0_1_schema_path
89
115
  "./test/data/openapi3/3_0_1.yaml"
90
116
  end
117
+
118
+ def open_api_3_invalid_reference_path
119
+ "./test/data/openapi3/invalid_reference.yaml"
120
+ end