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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/bin/committee-stub +23 -0
  3. data/lib/committee/bin/committee_stub.rb +67 -0
  4. data/lib/committee/drivers/driver.rb +47 -0
  5. data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
  6. data/lib/committee/drivers/hyper_schema/link.rb +68 -0
  7. data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
  8. data/lib/committee/drivers/hyper_schema.rb +12 -0
  9. data/lib/committee/drivers/open_api_2/driver.rb +252 -0
  10. data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
  11. data/lib/committee/drivers/open_api_2/link.rb +36 -0
  12. data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
  13. data/lib/committee/drivers/open_api_2/schema.rb +26 -0
  14. data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
  15. data/lib/committee/drivers/open_api_2.rb +13 -0
  16. data/lib/committee/drivers/open_api_3/driver.rb +51 -0
  17. data/lib/committee/drivers/open_api_3/schema.rb +41 -0
  18. data/lib/committee/drivers/open_api_3.rb +11 -0
  19. data/lib/committee/drivers/schema.rb +23 -0
  20. data/lib/committee/drivers.rb +84 -0
  21. data/lib/committee/errors.rb +36 -0
  22. data/lib/committee/middleware/base.rb +57 -0
  23. data/lib/committee/middleware/request_validation.rb +41 -0
  24. data/lib/committee/middleware/response_validation.rb +58 -0
  25. data/lib/committee/middleware/stub.rb +75 -0
  26. data/lib/committee/middleware.rb +11 -0
  27. data/lib/committee/request_unpacker.rb +91 -0
  28. data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +79 -0
  29. data/lib/committee/schema_validator/hyper_schema/request_validator.rb +55 -0
  30. data/lib/committee/schema_validator/hyper_schema/response_generator.rb +102 -0
  31. data/lib/committee/schema_validator/hyper_schema/response_validator.rb +89 -0
  32. data/lib/committee/schema_validator/hyper_schema/router.rb +46 -0
  33. data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +105 -0
  34. data/lib/committee/schema_validator/hyper_schema.rb +119 -0
  35. data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +139 -0
  36. data/lib/committee/schema_validator/open_api_3/request_validator.rb +52 -0
  37. data/lib/committee/schema_validator/open_api_3/response_validator.rb +29 -0
  38. data/lib/committee/schema_validator/open_api_3/router.rb +45 -0
  39. data/lib/committee/schema_validator/open_api_3.rb +120 -0
  40. data/lib/committee/schema_validator/option.rb +60 -0
  41. data/lib/committee/schema_validator.rb +23 -0
  42. data/lib/committee/test/methods.rb +84 -0
  43. data/lib/committee/test/schema_coverage.rb +101 -0
  44. data/lib/committee/utils.rb +28 -0
  45. data/lib/committee/validation_error.rb +26 -0
  46. data/lib/committee/version.rb +5 -0
  47. data/lib/committee.rb +40 -0
  48. data/test/bin/committee_stub_test.rb +57 -0
  49. data/test/bin_test.rb +25 -0
  50. data/test/committee_test.rb +77 -0
  51. data/test/drivers/hyper_schema/driver_test.rb +49 -0
  52. data/test/drivers/hyper_schema/link_test.rb +56 -0
  53. data/test/drivers/open_api_2/driver_test.rb +156 -0
  54. data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
  55. data/test/drivers/open_api_2/link_test.rb +52 -0
  56. data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
  57. data/test/drivers/open_api_3/driver_test.rb +84 -0
  58. data/test/drivers_test.rb +154 -0
  59. data/test/middleware/base_test.rb +130 -0
  60. data/test/middleware/request_validation_open_api_3_test.rb +626 -0
  61. data/test/middleware/request_validation_test.rb +516 -0
  62. data/test/middleware/response_validation_open_api_3_test.rb +291 -0
  63. data/test/middleware/response_validation_test.rb +189 -0
  64. data/test/middleware/stub_test.rb +145 -0
  65. data/test/request_unpacker_test.rb +200 -0
  66. data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +111 -0
  67. data/test/schema_validator/hyper_schema/request_validator_test.rb +151 -0
  68. data/test/schema_validator/hyper_schema/response_generator_test.rb +142 -0
  69. data/test/schema_validator/hyper_schema/response_validator_test.rb +118 -0
  70. data/test/schema_validator/hyper_schema/router_test.rb +88 -0
  71. data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +137 -0
  72. data/test/schema_validator/open_api_3/operation_wrapper_test.rb +218 -0
  73. data/test/schema_validator/open_api_3/request_validator_test.rb +110 -0
  74. data/test/schema_validator/open_api_3/response_validator_test.rb +92 -0
  75. data/test/test/methods_new_version_test.rb +97 -0
  76. data/test/test/methods_test.rb +363 -0
  77. data/test/test/schema_coverage_test.rb +216 -0
  78. data/test/test_helper.rb +120 -0
  79. data/test/validation_error_test.rb +25 -0
  80. metadata +328 -0
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ describe Committee::Middleware::ResponseValidation do
6
+ include Rack::Test::Methods
7
+
8
+ CHARACTERS_RESPONSE = {"Otonokizaka" => ["Honoka.Kousaka"]}
9
+
10
+ def app
11
+ @app
12
+ end
13
+
14
+ it "passes through a valid response" do
15
+ @app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), {}, schema: open_api_3_schema)
16
+ get "/characters"
17
+ assert_equal 200, last_response.status
18
+ end
19
+
20
+ it "passes through a invalid json" do
21
+ @app = new_response_rack("not_json", {}, schema: open_api_3_schema)
22
+
23
+ get "/characters"
24
+
25
+ assert_equal 500, last_response.status
26
+ assert_equal "{\"id\":\"invalid_response\",\"message\":\"Response wasn't valid JSON.\"}", last_response.body
27
+ end
28
+
29
+ it "passes through a invalid json with ignore_error option" do
30
+ @app = new_response_rack("not_json", {}, schema: open_api_3_schema, ignore_error: true)
31
+
32
+ get "/characters"
33
+
34
+ assert_equal 200, last_response.status
35
+ end
36
+
37
+ it "passes through a invalid json with parse_response_by_content_type option" do
38
+ @app = new_response_rack("csv response", { "Content-Type" => "test/csv"}, schema: open_api_3_schema, parse_response_by_content_type: true)
39
+
40
+ get "/csv"
41
+
42
+ assert_equal 200, last_response.status
43
+ end
44
+
45
+ it "passes through not definition" do
46
+ @app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), {}, schema: open_api_3_schema)
47
+ get "/no_data"
48
+ assert_equal 200, last_response.status
49
+ end
50
+
51
+ it "detects a response invalid due to schema" do
52
+ @app = new_response_rack("[]", {}, schema: open_api_3_schema, raise: true)
53
+
54
+ e = assert_raises(Committee::InvalidResponse) {
55
+ get "/characters"
56
+ }
57
+
58
+ assert_match(/expected object, but received Array: /i, e.message)
59
+ end
60
+
61
+ it "passes through a 204 (no content) response" do
62
+ @app = new_response_rack("", {}, {schema: open_api_3_schema}, {status: 204})
63
+ post "/validate"
64
+ assert_equal 204, last_response.status
65
+ end
66
+
67
+ it "passes through a 304 (not modified) response" do
68
+ @app = new_response_rack("", {}, {schema: open_api_3_schema}, {status: 304})
69
+ post "/validate"
70
+ assert_equal 304, last_response.status
71
+ end
72
+
73
+ it "passes through a valid response with prefix" do
74
+ @app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), {}, schema: open_api_3_schema, prefix: "/v1")
75
+ get "/v1/characters"
76
+ assert_equal 200, last_response.status
77
+ end
78
+
79
+ it "passes through a invalid json with prefix" do
80
+ @app = new_response_rack("not_json", {}, schema: open_api_3_schema, prefix: "/v1")
81
+
82
+ get "/v1/characters"
83
+
84
+ assert_equal 500, last_response.status
85
+ assert_equal "{\"id\":\"invalid_response\",\"message\":\"Response wasn't valid JSON.\"}", last_response.body
86
+ end
87
+
88
+ it "rescues JSON errors" do
89
+ @app = new_response_rack("_42", {}, schema: open_api_3_schema, raise: true)
90
+ assert_raises(Committee::InvalidResponse) do
91
+ get "/characters"
92
+ end
93
+ end
94
+
95
+ it "not parameter request" do
96
+ @app = new_response_rack({integer: '1'}.to_json, {}, schema: open_api_3_schema, raise: true)
97
+
98
+ assert_raises(Committee::InvalidResponse) do
99
+ patch "/validate_no_parameter", {no_schema: 'no'}
100
+ end
101
+ end
102
+
103
+ it "optionally validates non-2xx blank responses" do
104
+ @app = new_response_rack("", {}, schema: open_api_3_schema, validate_success_only: false)
105
+ get "/characters"
106
+ assert_equal 200, last_response.status
107
+ end
108
+
109
+ it "optionally validates non-2xx invalid responses with invalid json" do
110
+ @app = new_response_rack("{_}", {}, schema: open_api_3_schema, validate_success_only: false)
111
+ get "/characters"
112
+ assert_equal 500, last_response.status
113
+ assert_match(/valid JSON/i, last_response.body)
114
+ end
115
+
116
+ describe "remote schema $ref" do
117
+ it "passes through a valid response" do
118
+ @app = new_response_rack(JSON.generate({ "sample" => "value" }), {}, schema: open_api_3_schema)
119
+ get "/ref-sample"
120
+ assert_equal 200, last_response.status
121
+ end
122
+
123
+ it "detects a invalid response" do
124
+ @app = new_response_rack("{}", {}, schema: open_api_3_schema)
125
+ get "/ref-sample"
126
+ assert_equal 500, last_response.status
127
+ end
128
+ end
129
+
130
+ describe 'check header' do
131
+ [
132
+ { check_header: true, description: 'valid value', header: { 'integer' => 1 }, expected: { status: 200 } },
133
+ { check_header: true, description: 'missing value', header: { 'integer' => nil }, expected: { error: 'headers/integer/schema does not allow null values' } },
134
+ { check_header: true, description: 'invalid value', header: { 'integer' => 'x' }, expected: { error: 'headers/integer/schema expected integer, but received String: "x"' } },
135
+
136
+ { check_header: false, description: 'valid value', header: { 'integer' => 1 }, expected: { status: 200 } },
137
+ { check_header: false, description: 'missing value', header: { 'integer' => nil }, expected: { status: 200 } },
138
+ { check_header: false, description: 'invalid value', header: { 'integer' => 'x' }, expected: { status: 200 } },
139
+ ].each do |h|
140
+ check_header = h[:check_header]
141
+ description = h[:description]
142
+ header = h[:header]
143
+ expected = h[:expected]
144
+ describe "when #{check_header}" do
145
+ %w(get post put patch delete options).each do |method|
146
+ describe method do
147
+ describe description do
148
+ if expected[:error].nil?
149
+ it 'should pass' do
150
+ @app = new_response_rack({}.to_json, header, schema: open_api_3_schema, raise: true, check_header: check_header)
151
+
152
+ send(method, "/header")
153
+ assert_equal expected[:status], last_response.status
154
+ end
155
+ else
156
+ it 'should fail' do
157
+ @app = new_response_rack({}.to_json, header, schema: open_api_3_schema, raise: true, check_header: check_header)
158
+
159
+ error = assert_raises(Committee::InvalidResponse) do
160
+ get "/header"
161
+ end
162
+ assert_match(expected[:error], error.message)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ describe 'validate error option' do
173
+ it "detects an invalid response status code" do
174
+ @app = new_response_rack({ integer: '1' }.to_json,
175
+ {},
176
+ app_status: 400,
177
+ schema: open_api_3_schema,
178
+ raise: true,
179
+ validate_success_only: false)
180
+
181
+
182
+ e = assert_raises(Committee::InvalidResponse) do
183
+ get "/characters"
184
+ end
185
+
186
+ assert_match(/but received String: \"1\"/i, e.message)
187
+ end
188
+
189
+ it "detects an invalid response status code with validate_success_only=true" do
190
+ @app = new_response_rack({ string_1: :honoka }.to_json,
191
+ {},
192
+ app_status: 400,
193
+ schema: open_api_3_schema,
194
+ raise: true,
195
+ validate_success_only: true)
196
+
197
+
198
+ get "/characters"
199
+
200
+ assert_equal 400, last_response.status
201
+ end
202
+ end
203
+
204
+ describe ':accept_request_filter' do
205
+ [
206
+ { description: 'when not specified, includes everything', accept_request_filter: nil, expected: { status: 500 } },
207
+ { description: 'when predicate matches, performs validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/c') }, expected: { status: 500 } },
208
+ { description: 'when predicate does not match, skips validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/x') }, expected: { status: 200 } },
209
+ ].each do |h|
210
+ description = h[:description]
211
+ accept_request_filter = h[:accept_request_filter]
212
+ expected = h[:expected]
213
+
214
+ it description do
215
+ @app = new_response_rack('not_json', {}, schema: open_api_3_schema, prefix: '/v1', accept_request_filter: accept_request_filter)
216
+
217
+ get 'v1/characters'
218
+
219
+ assert_equal expected[:status], last_response.status
220
+ end
221
+ end
222
+ end
223
+
224
+ it 'does not suppress application error' do
225
+ @app = Rack::Builder.new {
226
+ use Committee::Middleware::ResponseValidation, {schema: open_api_3_schema, raise: true}
227
+ run lambda { |_|
228
+ JSON.load('-') # invalid json
229
+ }
230
+ }
231
+
232
+ assert_raises(JSON::ParserError) do
233
+ get "/error", nil
234
+ end
235
+ end
236
+
237
+ it "strict and invalid status" do
238
+ @app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), {}, {schema: open_api_3_schema, strict: true}, {status: 201})
239
+ get "/characters"
240
+ assert_equal 500, last_response.status
241
+ end
242
+
243
+ it "strict and invalid status with raise" do
244
+ @app = new_response_rack(JSON.generate(CHARACTERS_RESPONSE), {}, {schema: open_api_3_schema, strict: true, raise: true}, {status: 201})
245
+
246
+ assert_raises(Committee::InvalidResponse) do
247
+ get "/characters"
248
+ end
249
+ end
250
+
251
+ it "strict and invalid content type" do
252
+ @app = new_response_rack("abc",
253
+ {},
254
+ {schema: open_api_3_schema, strict: true},
255
+ {content_type: 'application/text'}
256
+ )
257
+ get "/characters"
258
+ assert_equal 500, last_response.status
259
+ end
260
+
261
+ it "strict and invalid content type with raise" do
262
+ @app = new_response_rack("abc",
263
+ {},
264
+ {schema: open_api_3_schema, strict: true, raise: true},
265
+ {content_type: 'application/text'}
266
+ )
267
+
268
+ assert_raises(Committee::InvalidResponse) do
269
+ get "/characters"
270
+ end
271
+ end
272
+
273
+ private
274
+
275
+ def new_response_rack(response, headers = {}, options = {}, rack_options = {})
276
+ # TODO: delete when 5.0.0 released because default value changed
277
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
278
+
279
+ status = rack_options[:status] || 200
280
+ content_type = rack_options[:content_type] || "application/json"
281
+ headers = {
282
+ "Content-Type" => content_type
283
+ }.merge(headers)
284
+ Rack::Builder.new {
285
+ use Committee::Middleware::ResponseValidation, options
286
+ run lambda { |_|
287
+ [options.fetch(:app_status, status), headers, [response]]
288
+ }
289
+ }
290
+ end
291
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ describe Committee::Middleware::ResponseValidation do
6
+ include Rack::Test::Methods
7
+
8
+ def app
9
+ @app
10
+ end
11
+
12
+ it "passes through a valid response" do
13
+ @app = new_rack_app(JSON.generate([ValidApp]), {}, schema: hyper_schema)
14
+ get "/apps"
15
+ assert_equal 200, last_response.status
16
+ end
17
+
18
+ # TODO: remove 5.0.0
19
+ it "passes through a valid response" do
20
+ # will show deprecated message
21
+ @app = new_rack_app(JSON.generate([ValidApp]), {}, schema: hyper_schema, strict: true)
22
+ get "/apps"
23
+ assert_equal 200, last_response.status
24
+ end
25
+
26
+ it "doesn't call error_handler (has a arg) when response is valid" do
27
+ called = false
28
+ pr = ->(_e) { called = true }
29
+ @app = new_rack_app(JSON.generate([ValidApp]), {}, schema: hyper_schema, error_handler: pr)
30
+ get "/apps"
31
+ assert !called, "error_handler is called"
32
+ end
33
+
34
+ it "doesn't call error_handler (has two args) when response is valid" do
35
+ called = false
36
+ pr = ->(_e, _env) { called = true }
37
+ @app = new_rack_app(JSON.generate([ValidApp]), {}, schema: hyper_schema, error_handler: pr)
38
+ get "/apps"
39
+ assert !called, "error_handler is called"
40
+ end
41
+
42
+ it "detects a response invalid due to schema" do
43
+ @app = new_rack_app("{}", {}, schema: hyper_schema)
44
+ get "/apps"
45
+ assert_equal 500, last_response.status
46
+ assert_match(/{} is not an array/i, last_response.body)
47
+ end
48
+
49
+ it "detects a response invalid due to schema with ignore_error option" do
50
+ @app = new_rack_app("{}", {}, schema: hyper_schema, ignore_error: true)
51
+ get "/apps"
52
+ assert_equal 200, last_response.status
53
+ end
54
+
55
+ it "detects a response invalid due to not being JSON" do
56
+ @app = new_rack_app("{_}", {}, schema: hyper_schema)
57
+ get "/apps"
58
+ assert_equal 500, last_response.status
59
+ assert_match(/valid JSON/i, last_response.body)
60
+ end
61
+
62
+ it "ignores a non-2xx invalid response" do
63
+ @app = new_rack_app("[]", {}, app_status: 404, schema: hyper_schema)
64
+ get "/apps"
65
+ assert_equal 404, last_response.status
66
+ end
67
+
68
+ it "optionally validates non-2xx invalid responses" do
69
+ @app = new_rack_app("", {}, app_status: 404, validate_success_only: false, schema: hyper_schema)
70
+ get "/apps"
71
+ assert_equal 500, last_response.status
72
+ assert_match(/Invalid response/i, last_response.body)
73
+ end
74
+
75
+ it "optionally validates non-2xx invalid responses with invalid json" do
76
+ @app = new_rack_app("{_}", {}, app_status: 404, validate_success_only: false, schema: hyper_schema)
77
+ get "/apps"
78
+ assert_equal 500, last_response.status
79
+ assert_match(/valid JSON/i, last_response.body)
80
+ end
81
+
82
+ it "passes through a 204 (no content) response" do
83
+ @app = new_rack_app("", {}, app_status: 204, schema: hyper_schema)
84
+ get "/apps"
85
+ assert_equal 204, last_response.status
86
+ end
87
+
88
+ it "passes through a 304 (not modified) response" do
89
+ @app = new_rack_app("", {}, app_status: 304, schema: hyper_schema)
90
+ get "/apps"
91
+ assert_equal 304, last_response.status
92
+ end
93
+
94
+ it "skip validation when 4xx" do
95
+ @app = new_rack_app("[{x:y}]", {}, schema: hyper_schema, validate_success_only: true, app_status: 400)
96
+ get "/apps"
97
+ assert_equal 400, last_response.status
98
+ assert_match("[{x:y}]", last_response.body)
99
+ end
100
+
101
+ it "rescues JSON errors" do
102
+ @app = new_rack_app("[{x:y}]", {}, schema: hyper_schema, validate_success_only: false)
103
+ get "/apps"
104
+ assert_equal 500, last_response.status
105
+ assert_match(/valid json/i, last_response.body)
106
+ end
107
+
108
+ it "calls error_handler (has two args) when it rescues JSON errors" do
109
+ called_err = nil
110
+ pr = ->(e, _env) { called_err = e }
111
+ @app = new_rack_app("[{x:y}]", {}, schema: hyper_schema, error_handler: pr)
112
+ get "/apps"
113
+ assert_kind_of JSON::ParserError, called_err
114
+ end
115
+
116
+ it "takes a prefix" do
117
+ @app = new_rack_app(JSON.generate([ValidApp]), {}, prefix: "/v1",
118
+ schema: hyper_schema)
119
+ get "/v1/apps"
120
+ assert_equal 200, last_response.status
121
+ end
122
+
123
+ it "rescues JSON errors" do
124
+ @app = new_rack_app("[{x:y}]", {}, raise: true, schema: hyper_schema)
125
+ assert_raises(Committee::InvalidResponse) do
126
+ get "/apps"
127
+ end
128
+ end
129
+
130
+ it "calls error_handler (has two args) when it rescues JSON errors" do
131
+ called_err = nil
132
+ pr = ->(e, _env) { called_err = e }
133
+ @app = new_rack_app("[{x:y}]", {}, raise: true, schema: hyper_schema, error_handler: pr)
134
+ assert_raises(Committee::InvalidResponse) do
135
+ get "/apps"
136
+ assert_kind_of JSON::ParserError, called_err
137
+ end
138
+ end
139
+
140
+ it "passes through a valid response for OpenAPI" do
141
+ @app = new_rack_app(JSON.generate([ValidPet]), {},
142
+ schema: open_api_2_schema)
143
+ get "/api/pets"
144
+ assert_equal 200, last_response.status
145
+ end
146
+
147
+ it "detects an invalid response for OpenAPI" do
148
+ @app = new_rack_app("{_}", {}, schema: open_api_2_schema)
149
+ get "/api/pets"
150
+ assert_equal 500, last_response.status
151
+ assert_match(/valid JSON/i, last_response.body)
152
+ end
153
+
154
+ describe ':accept_request_filter' do
155
+ [
156
+ { description: 'when not specified, includes everything', accept_request_filter: nil, expected: { status: 500 } },
157
+ { description: 'when predicate matches, performs validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/a') }, expected: { status: 500 } },
158
+ { description: 'when predicate does not match, skips validation', accept_request_filter: -> (request) { request.path.start_with?('/v1/x') }, expected: { status: 200 } },
159
+ ].each do |h|
160
+ description = h[:description]
161
+ accept_request_filter = h[:accept_request_filter]
162
+ expected = h[:expected]
163
+ it description do
164
+ @app = new_rack_app('not_json', {}, schema: hyper_schema, prefix: '/v1', accept_request_filter: accept_request_filter)
165
+
166
+ get '/v1/apps'
167
+
168
+ assert_equal expected[:status], last_response.status
169
+ end
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def new_rack_app(response, headers = {}, options = {})
176
+ # TODO: delete when 5.0.0 released because default value changed
177
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
178
+
179
+ headers = {
180
+ "Content-Type" => "application/json"
181
+ }.merge(headers)
182
+ Rack::Builder.new {
183
+ use Committee::Middleware::ResponseValidation, options
184
+ run lambda { |_|
185
+ [options.fetch(:app_status, 200), headers, [response]]
186
+ }
187
+ }
188
+ end
189
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ describe Committee::Middleware::Stub do
6
+ include Rack::Test::Methods
7
+
8
+ def app
9
+ @app
10
+ end
11
+
12
+ it "responds with a stubbed response" do
13
+ @app = new_rack_app(schema: hyper_schema)
14
+ get "/apps/heroku-api"
15
+ assert_equal 200, last_response.status
16
+ data = JSON.parse(last_response.body)
17
+ assert_equal ValidApp.keys.sort, data.keys.sort
18
+ end
19
+
20
+ it "responds with 201 on create actions" do
21
+ @app = new_rack_app(schema: hyper_schema)
22
+ post "/apps"
23
+ assert_equal 201, last_response.status
24
+ end
25
+
26
+ it "optionally calls into application" do
27
+ @app = new_rack_app(call: true, schema: hyper_schema)
28
+ get "/apps/heroku-api"
29
+ assert_equal 200, last_response.status
30
+ assert_equal ValidApp,
31
+ JSON.parse(last_response.headers["Committee-Response"])
32
+ end
33
+
34
+ it "optionally returns the application's response" do
35
+ @app = new_rack_app(call: true, schema: hyper_schema, suppress: true)
36
+ get "/apps/heroku-api"
37
+ assert_equal 429, last_response.status
38
+ assert_equal ValidApp,
39
+ JSON.parse(last_response.headers["Committee-Response"])
40
+ assert_equal "", last_response.body
41
+ end
42
+
43
+ it "optionally returns the application's response schema" do
44
+ @app = new_rack_app(call: true, schema: hyper_schema, suppress: true)
45
+ get "/apps/heroku-api"
46
+ assert_equal 429, last_response.status
47
+ assert_equal "#/definitions/app/links/2/targetSchema",
48
+ last_response.headers["Committee-Response-Schema"]
49
+ assert_equal "", last_response.body
50
+ end
51
+
52
+ it "takes a prefix" do
53
+ @app = new_rack_app(prefix: "/v1", schema: hyper_schema)
54
+ get "/v1/apps/heroku-api"
55
+ assert_equal 200, last_response.status
56
+ data = JSON.parse(last_response.body)
57
+ assert_equal ValidApp.keys.sort, data.keys.sort
58
+ end
59
+
60
+ it "allows the stub's response to be replaced" do
61
+ response = { replaced: true }
62
+ @app = new_rack_app(call: true, response: response, schema: hyper_schema)
63
+ get "/apps/heroku-api"
64
+ assert_equal 200, last_response.status
65
+ assert_equal response, JSON.parse(last_response.body, symbolize_names: true)
66
+ end
67
+
68
+ it "responds with a stubbed response for OpenAPI" do
69
+ @app = new_rack_app(schema: open_api_2_schema)
70
+ get "/api/pets/fido"
71
+ assert_equal 200, last_response.status
72
+ data = JSON.parse(last_response.body)
73
+ assert_equal ValidPet.keys.sort, data.keys.sort
74
+ end
75
+
76
+ it "calls into app for links that are undefined" do
77
+ @app = new_rack_app(call: false, schema: hyper_schema)
78
+ post "/foos"
79
+ assert_equal 429, last_response.status
80
+ end
81
+
82
+ it "caches the response if called multiple times" do
83
+ cache = {}
84
+ @app = new_rack_app(cache: cache, schema: hyper_schema)
85
+
86
+ get "/apps/heroku-api"
87
+ assert_equal 200, last_response.status
88
+
89
+ data, _schema = cache[cache.first[0]]
90
+ assert_equal ValidApp.keys.sort, data.keys.sort
91
+
92
+ # replace what we have in the cache
93
+ cache[cache.first[0]] = { "cached" => true }
94
+
95
+ get "/apps/heroku-api"
96
+ assert_equal 200, last_response.status
97
+ data = JSON.parse(last_response.body)
98
+ assert_equal({ "cached" => true }, data)
99
+ end
100
+
101
+ it "caches the response if called multiple times" do
102
+ cache = {}
103
+ @app = new_rack_app(cache: cache, schema: open_api_3_schema)
104
+
105
+ assert_raises(Committee::OpenAPI3Unsupported) do
106
+ get "/characters"
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def new_rack_app(options = {})
113
+ response = options.delete(:response)
114
+ suppress = options.delete(:suppress)
115
+
116
+ # TODO: delete when 5.0.0 released because default value changed
117
+ options[:parse_response_by_content_type] = true if options[:parse_response_by_content_type] == nil
118
+
119
+ Rack::Builder.new {
120
+ use Committee::Middleware::Stub, options
121
+ run lambda { |env|
122
+ if response
123
+ env["committee.response"] = response
124
+ end
125
+
126
+ headers = {}
127
+ if res = env["committee.response"]
128
+ headers.merge!({
129
+ "Committee-Response" => JSON.generate(res)
130
+ })
131
+ end
132
+
133
+ if res = env["committee.response_schema"]
134
+ headers.merge!({
135
+ "Committee-Response-Schema" => res.pointer,
136
+ })
137
+ end
138
+
139
+ env["committee.suppress"] = suppress
140
+
141
+ [429, headers, []]
142
+ }
143
+ }
144
+ end
145
+ end