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.
- checksums.yaml +7 -0
- data/bin/committee-stub +23 -0
- data/lib/committee/bin/committee_stub.rb +67 -0
- data/lib/committee/drivers/driver.rb +47 -0
- data/lib/committee/drivers/hyper_schema/driver.rb +105 -0
- data/lib/committee/drivers/hyper_schema/link.rb +68 -0
- data/lib/committee/drivers/hyper_schema/schema.rb +22 -0
- data/lib/committee/drivers/hyper_schema.rb +12 -0
- data/lib/committee/drivers/open_api_2/driver.rb +252 -0
- data/lib/committee/drivers/open_api_2/header_schema_builder.rb +33 -0
- data/lib/committee/drivers/open_api_2/link.rb +36 -0
- data/lib/committee/drivers/open_api_2/parameter_schema_builder.rb +83 -0
- data/lib/committee/drivers/open_api_2/schema.rb +26 -0
- data/lib/committee/drivers/open_api_2/schema_builder.rb +33 -0
- data/lib/committee/drivers/open_api_2.rb +13 -0
- data/lib/committee/drivers/open_api_3/driver.rb +51 -0
- data/lib/committee/drivers/open_api_3/schema.rb +41 -0
- data/lib/committee/drivers/open_api_3.rb +11 -0
- data/lib/committee/drivers/schema.rb +23 -0
- data/lib/committee/drivers.rb +84 -0
- data/lib/committee/errors.rb +36 -0
- data/lib/committee/middleware/base.rb +57 -0
- data/lib/committee/middleware/request_validation.rb +41 -0
- data/lib/committee/middleware/response_validation.rb +58 -0
- data/lib/committee/middleware/stub.rb +75 -0
- data/lib/committee/middleware.rb +11 -0
- data/lib/committee/request_unpacker.rb +91 -0
- data/lib/committee/schema_validator/hyper_schema/parameter_coercer.rb +79 -0
- data/lib/committee/schema_validator/hyper_schema/request_validator.rb +55 -0
- data/lib/committee/schema_validator/hyper_schema/response_generator.rb +102 -0
- data/lib/committee/schema_validator/hyper_schema/response_validator.rb +89 -0
- data/lib/committee/schema_validator/hyper_schema/router.rb +46 -0
- data/lib/committee/schema_validator/hyper_schema/string_params_coercer.rb +105 -0
- data/lib/committee/schema_validator/hyper_schema.rb +119 -0
- data/lib/committee/schema_validator/open_api_3/operation_wrapper.rb +139 -0
- data/lib/committee/schema_validator/open_api_3/request_validator.rb +52 -0
- data/lib/committee/schema_validator/open_api_3/response_validator.rb +29 -0
- data/lib/committee/schema_validator/open_api_3/router.rb +45 -0
- data/lib/committee/schema_validator/open_api_3.rb +120 -0
- data/lib/committee/schema_validator/option.rb +60 -0
- data/lib/committee/schema_validator.rb +23 -0
- data/lib/committee/test/methods.rb +84 -0
- data/lib/committee/test/schema_coverage.rb +101 -0
- data/lib/committee/utils.rb +28 -0
- data/lib/committee/validation_error.rb +26 -0
- data/lib/committee/version.rb +5 -0
- data/lib/committee.rb +40 -0
- data/test/bin/committee_stub_test.rb +57 -0
- data/test/bin_test.rb +25 -0
- data/test/committee_test.rb +77 -0
- data/test/drivers/hyper_schema/driver_test.rb +49 -0
- data/test/drivers/hyper_schema/link_test.rb +56 -0
- data/test/drivers/open_api_2/driver_test.rb +156 -0
- data/test/drivers/open_api_2/header_schema_builder_test.rb +26 -0
- data/test/drivers/open_api_2/link_test.rb +52 -0
- data/test/drivers/open_api_2/parameter_schema_builder_test.rb +195 -0
- data/test/drivers/open_api_3/driver_test.rb +84 -0
- data/test/drivers_test.rb +154 -0
- data/test/middleware/base_test.rb +130 -0
- data/test/middleware/request_validation_open_api_3_test.rb +626 -0
- data/test/middleware/request_validation_test.rb +516 -0
- data/test/middleware/response_validation_open_api_3_test.rb +291 -0
- data/test/middleware/response_validation_test.rb +189 -0
- data/test/middleware/stub_test.rb +145 -0
- data/test/request_unpacker_test.rb +200 -0
- data/test/schema_validator/hyper_schema/parameter_coercer_test.rb +111 -0
- data/test/schema_validator/hyper_schema/request_validator_test.rb +151 -0
- data/test/schema_validator/hyper_schema/response_generator_test.rb +142 -0
- data/test/schema_validator/hyper_schema/response_validator_test.rb +118 -0
- data/test/schema_validator/hyper_schema/router_test.rb +88 -0
- data/test/schema_validator/hyper_schema/string_params_coercer_test.rb +137 -0
- data/test/schema_validator/open_api_3/operation_wrapper_test.rb +218 -0
- data/test/schema_validator/open_api_3/request_validator_test.rb +110 -0
- data/test/schema_validator/open_api_3/response_validator_test.rb +92 -0
- data/test/test/methods_new_version_test.rb +97 -0
- data/test/test/methods_test.rb +363 -0
- data/test/test/schema_coverage_test.rb +216 -0
- data/test/test_helper.rb +120 -0
- data/test/validation_error_test.rb +25 -0
- 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
|