grape-security 0.8.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/.gitignore +45 -0
- data/.rspec +2 -0
- data/.rubocop.yml +70 -0
- data/.travis.yml +18 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +314 -0
- data/CONTRIBUTING.md +118 -0
- data/Gemfile +21 -0
- data/Guardfile +14 -0
- data/LICENSE +20 -0
- data/README.md +1777 -0
- data/RELEASING.md +105 -0
- data/Rakefile +69 -0
- data/UPGRADING.md +124 -0
- data/grape-security.gemspec +39 -0
- data/grape.png +0 -0
- data/lib/grape.rb +99 -0
- data/lib/grape/api.rb +646 -0
- data/lib/grape/cookies.rb +39 -0
- data/lib/grape/endpoint.rb +533 -0
- data/lib/grape/error_formatter/base.rb +31 -0
- data/lib/grape/error_formatter/json.rb +15 -0
- data/lib/grape/error_formatter/txt.rb +16 -0
- data/lib/grape/error_formatter/xml.rb +15 -0
- data/lib/grape/exceptions/base.rb +66 -0
- data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
- data/lib/grape/exceptions/invalid_formatter.rb +10 -0
- data/lib/grape/exceptions/invalid_versioner_option.rb +10 -0
- data/lib/grape/exceptions/invalid_with_option_for_represent.rb +10 -0
- data/lib/grape/exceptions/missing_mime_type.rb +10 -0
- data/lib/grape/exceptions/missing_option.rb +10 -0
- data/lib/grape/exceptions/missing_vendor_option.rb +10 -0
- data/lib/grape/exceptions/unknown_options.rb +10 -0
- data/lib/grape/exceptions/unknown_validator.rb +10 -0
- data/lib/grape/exceptions/validation.rb +26 -0
- data/lib/grape/exceptions/validation_errors.rb +43 -0
- data/lib/grape/formatter/base.rb +31 -0
- data/lib/grape/formatter/json.rb +12 -0
- data/lib/grape/formatter/serializable_hash.rb +35 -0
- data/lib/grape/formatter/txt.rb +11 -0
- data/lib/grape/formatter/xml.rb +12 -0
- data/lib/grape/http/request.rb +26 -0
- data/lib/grape/locale/en.yml +32 -0
- data/lib/grape/middleware/auth/base.rb +30 -0
- data/lib/grape/middleware/auth/basic.rb +13 -0
- data/lib/grape/middleware/auth/digest.rb +13 -0
- data/lib/grape/middleware/auth/oauth2.rb +83 -0
- data/lib/grape/middleware/base.rb +62 -0
- data/lib/grape/middleware/error.rb +89 -0
- data/lib/grape/middleware/filter.rb +17 -0
- data/lib/grape/middleware/formatter.rb +150 -0
- data/lib/grape/middleware/globals.rb +13 -0
- data/lib/grape/middleware/versioner.rb +32 -0
- data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
- data/lib/grape/middleware/versioner/header.rb +132 -0
- data/lib/grape/middleware/versioner/param.rb +42 -0
- data/lib/grape/middleware/versioner/path.rb +52 -0
- data/lib/grape/namespace.rb +23 -0
- data/lib/grape/parser/base.rb +29 -0
- data/lib/grape/parser/json.rb +11 -0
- data/lib/grape/parser/xml.rb +11 -0
- data/lib/grape/path.rb +70 -0
- data/lib/grape/route.rb +27 -0
- data/lib/grape/util/content_types.rb +18 -0
- data/lib/grape/util/deep_merge.rb +23 -0
- data/lib/grape/util/hash_stack.rb +120 -0
- data/lib/grape/validations.rb +322 -0
- data/lib/grape/validations/coerce.rb +63 -0
- data/lib/grape/validations/default.rb +25 -0
- data/lib/grape/validations/exactly_one_of.rb +26 -0
- data/lib/grape/validations/mutual_exclusion.rb +25 -0
- data/lib/grape/validations/presence.rb +16 -0
- data/lib/grape/validations/regexp.rb +12 -0
- data/lib/grape/validations/values.rb +23 -0
- data/lib/grape/version.rb +3 -0
- data/spec/grape/api_spec.rb +2571 -0
- data/spec/grape/endpoint_spec.rb +784 -0
- data/spec/grape/entity_spec.rb +324 -0
- data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
- data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
- data/spec/grape/exceptions/missing_mime_type_spec.rb +18 -0
- data/spec/grape/exceptions/missing_option_spec.rb +18 -0
- data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
- data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
- data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
- data/spec/grape/middleware/auth/basic_spec.rb +31 -0
- data/spec/grape/middleware/auth/digest_spec.rb +47 -0
- data/spec/grape/middleware/auth/oauth2_spec.rb +135 -0
- data/spec/grape/middleware/base_spec.rb +58 -0
- data/spec/grape/middleware/error_spec.rb +45 -0
- data/spec/grape/middleware/exception_spec.rb +184 -0
- data/spec/grape/middleware/formatter_spec.rb +258 -0
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
- data/spec/grape/middleware/versioner/header_spec.rb +302 -0
- data/spec/grape/middleware/versioner/param_spec.rb +58 -0
- data/spec/grape/middleware/versioner/path_spec.rb +44 -0
- data/spec/grape/middleware/versioner_spec.rb +22 -0
- data/spec/grape/path_spec.rb +229 -0
- data/spec/grape/util/hash_stack_spec.rb +132 -0
- data/spec/grape/validations/coerce_spec.rb +208 -0
- data/spec/grape/validations/default_spec.rb +123 -0
- data/spec/grape/validations/exactly_one_of_spec.rb +71 -0
- data/spec/grape/validations/mutual_exclusion_spec.rb +61 -0
- data/spec/grape/validations/presence_spec.rb +142 -0
- data/spec/grape/validations/regexp_spec.rb +40 -0
- data/spec/grape/validations/values_spec.rb +152 -0
- data/spec/grape/validations/zh-CN.yml +10 -0
- data/spec/grape/validations_spec.rb +994 -0
- data/spec/shared/versioning_examples.rb +121 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/basic_auth_encode_helpers.rb +3 -0
- data/spec/support/content_type_helpers.rb +11 -0
- data/spec/support/versioned_helpers.rb +50 -0
- metadata +421 -0
@@ -0,0 +1,784 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Endpoint do
|
4
|
+
subject { Class.new(Grape::API) }
|
5
|
+
|
6
|
+
def app
|
7
|
+
subject
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.before_each' do
|
11
|
+
after { Grape::Endpoint.before_each(nil) }
|
12
|
+
|
13
|
+
it 'should be settable via block' do
|
14
|
+
block = lambda { |endpoint| "noop" }
|
15
|
+
Grape::Endpoint.before_each(&block)
|
16
|
+
expect(Grape::Endpoint.before_each).to eq(block)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should be settable via reference' do
|
20
|
+
block = lambda { |endpoint| "noop" }
|
21
|
+
Grape::Endpoint.before_each block
|
22
|
+
expect(Grape::Endpoint.before_each).to eq(block)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should be able to override a helper' do
|
26
|
+
subject.get("/") { current_user }
|
27
|
+
expect { get '/' }.to raise_error(NameError)
|
28
|
+
|
29
|
+
Grape::Endpoint.before_each do |endpoint|
|
30
|
+
endpoint.stub(:current_user).and_return("Bob")
|
31
|
+
end
|
32
|
+
|
33
|
+
get '/'
|
34
|
+
expect(last_response.body).to eq("Bob")
|
35
|
+
|
36
|
+
Grape::Endpoint.before_each(nil)
|
37
|
+
expect { get '/' }.to raise_error(NameError)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#initialize' do
|
42
|
+
it 'takes a settings stack, options, and a block' do
|
43
|
+
p = proc {}
|
44
|
+
expect {
|
45
|
+
Grape::Endpoint.new(Grape::Util::HashStack.new, {
|
46
|
+
path: '/',
|
47
|
+
method: :get
|
48
|
+
}, &p)
|
49
|
+
}.not_to raise_error
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'sets itself in the env upon call' do
|
54
|
+
subject.get('/') { "Hello world." }
|
55
|
+
get '/'
|
56
|
+
expect(last_request.env['api.endpoint']).to be_kind_of(Grape::Endpoint)
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#status' do
|
60
|
+
it 'is callable from within a block' do
|
61
|
+
subject.get('/home') do
|
62
|
+
status 206
|
63
|
+
"Hello"
|
64
|
+
end
|
65
|
+
|
66
|
+
get '/home'
|
67
|
+
expect(last_response.status).to eq(206)
|
68
|
+
expect(last_response.body).to eq("Hello")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#header' do
|
73
|
+
it 'is callable from within a block' do
|
74
|
+
subject.get('/hey') do
|
75
|
+
header 'X-Awesome', 'true'
|
76
|
+
"Awesome"
|
77
|
+
end
|
78
|
+
|
79
|
+
get '/hey'
|
80
|
+
expect(last_response.headers['X-Awesome']).to eq('true')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#headers' do
|
85
|
+
before do
|
86
|
+
subject.get('/headers') do
|
87
|
+
headers.to_json
|
88
|
+
end
|
89
|
+
end
|
90
|
+
it 'includes request headers' do
|
91
|
+
get '/headers'
|
92
|
+
expect(JSON.parse(last_response.body)).to eq(
|
93
|
+
"Host" => "example.org",
|
94
|
+
"Cookie" => ""
|
95
|
+
)
|
96
|
+
end
|
97
|
+
it 'includes additional request headers' do
|
98
|
+
get '/headers', nil, "HTTP_X_GRAPE_CLIENT" => "1"
|
99
|
+
expect(JSON.parse(last_response.body)["X-Grape-Client"]).to eq("1")
|
100
|
+
end
|
101
|
+
it 'includes headers passed as symbols' do
|
102
|
+
env = Rack::MockRequest.env_for("/headers")
|
103
|
+
env["HTTP_SYMBOL_HEADER".to_sym] = "Goliath passes symbols"
|
104
|
+
body = subject.call(env)[2].body.first
|
105
|
+
expect(JSON.parse(body)["Symbol-Header"]).to eq("Goliath passes symbols")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe '#cookies' do
|
110
|
+
it 'is callable from within a block' do
|
111
|
+
subject.get('/get/cookies') do
|
112
|
+
cookies['my-awesome-cookie1'] = 'is cool'
|
113
|
+
cookies['my-awesome-cookie2'] = {
|
114
|
+
value: 'is cool too',
|
115
|
+
domain: 'my.example.com',
|
116
|
+
path: '/',
|
117
|
+
secure: true
|
118
|
+
}
|
119
|
+
cookies[:cookie3] = 'symbol'
|
120
|
+
cookies['cookie4'] = 'secret code here'
|
121
|
+
end
|
122
|
+
|
123
|
+
get('/get/cookies')
|
124
|
+
|
125
|
+
expect(last_response.headers['Set-Cookie'].split("\n").sort).to eql [
|
126
|
+
"cookie3=symbol",
|
127
|
+
"cookie4=secret+code+here",
|
128
|
+
"my-awesome-cookie1=is+cool",
|
129
|
+
"my-awesome-cookie2=is+cool+too; domain=my.example.com; path=/; secure"
|
130
|
+
]
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'sets browser cookies and does not set response cookies' do
|
134
|
+
subject.get('/username') do
|
135
|
+
cookies[:username]
|
136
|
+
end
|
137
|
+
get('/username', {}, 'HTTP_COOKIE' => 'username=mrplum; sandbox=true')
|
138
|
+
|
139
|
+
expect(last_response.body).to eq('mrplum')
|
140
|
+
expect(last_response.headers['Set-Cookie']).to be_nil
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'sets and update browser cookies' do
|
144
|
+
subject.get('/username') do
|
145
|
+
cookies[:sandbox] = true if cookies[:sandbox] == 'false'
|
146
|
+
cookies[:username] += "_test"
|
147
|
+
end
|
148
|
+
get('/username', {}, 'HTTP_COOKIE' => 'username=user; sandbox=false')
|
149
|
+
expect(last_response.body).to eq('user_test')
|
150
|
+
expect(last_response.headers['Set-Cookie']).to match(/username=user_test/)
|
151
|
+
expect(last_response.headers['Set-Cookie']).to match(/sandbox=true/)
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'deletes cookie' do
|
155
|
+
subject.get('/test') do
|
156
|
+
sum = 0
|
157
|
+
cookies.each do |name, val|
|
158
|
+
sum += val.to_i
|
159
|
+
cookies.delete name
|
160
|
+
end
|
161
|
+
sum
|
162
|
+
end
|
163
|
+
get '/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2'
|
164
|
+
expect(last_response.body).to eq('3')
|
165
|
+
cookies = Hash[last_response.headers['Set-Cookie'].split("\n").map do |set_cookie|
|
166
|
+
cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie
|
167
|
+
[cookie.name, cookie]
|
168
|
+
end]
|
169
|
+
expect(cookies.size).to eq(2)
|
170
|
+
["and_this", "delete_this_cookie"].each do |cookie_name|
|
171
|
+
cookie = cookies[cookie_name]
|
172
|
+
expect(cookie).not_to be_nil
|
173
|
+
expect(cookie.value).to eq("deleted")
|
174
|
+
expect(cookie.expired?).to be true
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'deletes cookies with path' do
|
179
|
+
subject.get('/test') do
|
180
|
+
sum = 0
|
181
|
+
cookies.each do |name, val|
|
182
|
+
sum += val.to_i
|
183
|
+
cookies.delete name, path: '/test'
|
184
|
+
end
|
185
|
+
sum
|
186
|
+
end
|
187
|
+
get('/test', {}, 'HTTP_COOKIE' => 'delete_this_cookie=1; and_this=2')
|
188
|
+
expect(last_response.body).to eq('3')
|
189
|
+
cookies = Hash[last_response.headers['Set-Cookie'].split("\n").map do |set_cookie|
|
190
|
+
cookie = CookieJar::Cookie.from_set_cookie 'http://localhost/test', set_cookie
|
191
|
+
[cookie.name, cookie]
|
192
|
+
end]
|
193
|
+
expect(cookies.size).to eq(2)
|
194
|
+
["and_this", "delete_this_cookie"].each do |cookie_name|
|
195
|
+
cookie = cookies[cookie_name]
|
196
|
+
expect(cookie).not_to be_nil
|
197
|
+
expect(cookie.value).to eq("deleted")
|
198
|
+
expect(cookie.path).to eq("/test")
|
199
|
+
expect(cookie.expired?).to be true
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe '#declared' do
|
205
|
+
before do
|
206
|
+
subject.params do
|
207
|
+
requires :first
|
208
|
+
optional :second
|
209
|
+
optional :third, default: 'third-default'
|
210
|
+
optional :nested, type: Hash do
|
211
|
+
optional :fourth
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'has as many keys as there are declared params' do
|
217
|
+
subject.get '/declared' do
|
218
|
+
declared(params).keys.size.should == 4
|
219
|
+
""
|
220
|
+
end
|
221
|
+
|
222
|
+
get '/declared?first=present'
|
223
|
+
expect(last_response.status).to eq(200)
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'has a optional param with default value all the time' do
|
227
|
+
subject.get '/declared' do
|
228
|
+
params[:third].should == 'third-default'
|
229
|
+
""
|
230
|
+
end
|
231
|
+
|
232
|
+
get '/declared?first=one'
|
233
|
+
expect(last_response.status).to eq(200)
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'builds nested params' do
|
237
|
+
subject.get '/declared' do
|
238
|
+
declared(params)[:nested].keys.size.should == 1
|
239
|
+
""
|
240
|
+
end
|
241
|
+
|
242
|
+
get '/declared?first=present&nested[fourth]=1'
|
243
|
+
expect(last_response.status).to eq(200)
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'builds nested params when given array' do
|
247
|
+
subject.get '/dummy' do
|
248
|
+
end
|
249
|
+
subject.params do
|
250
|
+
requires :first
|
251
|
+
optional :second
|
252
|
+
optional :third, default: 'third-default'
|
253
|
+
optional :nested, type: Array do
|
254
|
+
optional :fourth
|
255
|
+
end
|
256
|
+
end
|
257
|
+
subject.get '/declared' do
|
258
|
+
declared(params)[:nested].size.should == 2
|
259
|
+
""
|
260
|
+
end
|
261
|
+
|
262
|
+
get '/declared?first=present&nested[][fourth]=1&nested[][fourth]=2'
|
263
|
+
expect(last_response.status).to eq(200)
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'filters out any additional params that are given' do
|
267
|
+
subject.get '/declared' do
|
268
|
+
declared(params).key?(:other).should == false
|
269
|
+
""
|
270
|
+
end
|
271
|
+
|
272
|
+
get '/declared?first=one&other=two'
|
273
|
+
expect(last_response.status).to eq(200)
|
274
|
+
end
|
275
|
+
|
276
|
+
it 'stringifies if that option is passed' do
|
277
|
+
subject.get '/declared' do
|
278
|
+
declared(params, stringify: true)["first"].should == "one"
|
279
|
+
""
|
280
|
+
end
|
281
|
+
|
282
|
+
get '/declared?first=one&other=two'
|
283
|
+
expect(last_response.status).to eq(200)
|
284
|
+
end
|
285
|
+
|
286
|
+
it 'does not include missing attributes if that option is passed' do
|
287
|
+
subject.get '/declared' do
|
288
|
+
error! 400, "expected nil" if declared(params, include_missing: false)[:second]
|
289
|
+
""
|
290
|
+
end
|
291
|
+
|
292
|
+
get '/declared?first=one&other=two'
|
293
|
+
expect(last_response.status).to eq(200)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
describe '#declared; call from child namespace' do
|
298
|
+
before do
|
299
|
+
subject.format :json
|
300
|
+
subject.namespace :something do
|
301
|
+
params do
|
302
|
+
requires :id, type: Integer
|
303
|
+
end
|
304
|
+
resource ':id' do
|
305
|
+
params do
|
306
|
+
requires :foo
|
307
|
+
optional :bar
|
308
|
+
end
|
309
|
+
get do
|
310
|
+
{
|
311
|
+
params: params,
|
312
|
+
declared_params: declared(params)
|
313
|
+
}
|
314
|
+
end
|
315
|
+
params do
|
316
|
+
requires :happy
|
317
|
+
optional :days
|
318
|
+
end
|
319
|
+
get '/test' do
|
320
|
+
{
|
321
|
+
params: params,
|
322
|
+
declared_params: declared(params, include_parent_namespaces: false)
|
323
|
+
}
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
it 'should include params defined in the parent namespace' do
|
330
|
+
get '/something/123', foo: 'test', extra: 'hello'
|
331
|
+
expect(last_response.status).to eq 200
|
332
|
+
json = JSON.parse(last_response.body, symbolize_names: true)
|
333
|
+
expect(json[:params][:id]).to eq 123
|
334
|
+
expect(json[:declared_params].keys).to match_array [:foo, :bar, :id]
|
335
|
+
end
|
336
|
+
|
337
|
+
it 'does not include params defined in the parent namespace with include_parent_namespaces: false' do
|
338
|
+
get '/something/123/test', happy: 'test', extra: 'hello'
|
339
|
+
expect(last_response.status).to eq 200
|
340
|
+
json = JSON.parse(last_response.body, symbolize_names: true)
|
341
|
+
expect(json[:params][:id]).to eq 123
|
342
|
+
expect(json[:declared_params].keys).to match_array [:happy, :days]
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
describe '#params' do
|
347
|
+
it 'is available to the caller' do
|
348
|
+
subject.get('/hey') do
|
349
|
+
params[:howdy]
|
350
|
+
end
|
351
|
+
|
352
|
+
get '/hey?howdy=hey'
|
353
|
+
expect(last_response.body).to eq('hey')
|
354
|
+
end
|
355
|
+
|
356
|
+
it 'parses from path segments' do
|
357
|
+
subject.get('/hey/:id') do
|
358
|
+
params[:id]
|
359
|
+
end
|
360
|
+
|
361
|
+
get '/hey/12'
|
362
|
+
expect(last_response.body).to eq('12')
|
363
|
+
end
|
364
|
+
|
365
|
+
it 'deeply converts nested params' do
|
366
|
+
subject.get '/location' do
|
367
|
+
params[:location][:city]
|
368
|
+
end
|
369
|
+
get '/location?location[city]=Dallas'
|
370
|
+
expect(last_response.body).to eq('Dallas')
|
371
|
+
end
|
372
|
+
|
373
|
+
context 'with special requirements' do
|
374
|
+
it 'parses email param with provided requirements for params' do
|
375
|
+
subject.get('/:person_email', requirements: { person_email: /.*/ }) do
|
376
|
+
params[:person_email]
|
377
|
+
end
|
378
|
+
|
379
|
+
get '/someone@example.com'
|
380
|
+
expect(last_response.body).to eq('someone@example.com')
|
381
|
+
|
382
|
+
get 'someone@example.com.pl'
|
383
|
+
expect(last_response.body).to eq('someone@example.com.pl')
|
384
|
+
end
|
385
|
+
|
386
|
+
it 'parses many params with provided regexps' do
|
387
|
+
subject.get('/:person_email/test/:number', requirements: { person_email: /someone@(.*).com/, number: /[0-9]/ }) do
|
388
|
+
params[:person_email] << params[:number]
|
389
|
+
end
|
390
|
+
|
391
|
+
get '/someone@example.com/test/1'
|
392
|
+
expect(last_response.body).to eq('someone@example.com1')
|
393
|
+
|
394
|
+
get '/someone@testing.wrong/test/1'
|
395
|
+
expect(last_response.status).to eq(404)
|
396
|
+
|
397
|
+
get 'someone@test.com/test/wrong_number'
|
398
|
+
expect(last_response.status).to eq(404)
|
399
|
+
|
400
|
+
get 'someone@test.com/wrong_middle/1'
|
401
|
+
expect(last_response.status).to eq(404)
|
402
|
+
end
|
403
|
+
|
404
|
+
context 'namespace requirements' do
|
405
|
+
before :each do
|
406
|
+
subject.namespace :outer, requirements: { person_email: /abc@(.*).com/ } do
|
407
|
+
get('/:person_email') do
|
408
|
+
params[:person_email]
|
409
|
+
end
|
410
|
+
|
411
|
+
namespace :inner, requirements: { number: /[0-9]/, person_email: /someone@(.*).com/ }do
|
412
|
+
get '/:person_email/test/:number' do
|
413
|
+
params[:person_email] << params[:number]
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
it "parse email param with provided requirements for params" do
|
419
|
+
get '/outer/abc@example.com'
|
420
|
+
expect(last_response.body).to eq('abc@example.com')
|
421
|
+
end
|
422
|
+
|
423
|
+
it "should override outer namespace's requirements" do
|
424
|
+
get '/outer/inner/someone@testing.wrong/test/1'
|
425
|
+
expect(last_response.status).to eq(404)
|
426
|
+
|
427
|
+
get '/outer/inner/someone@testing.com/test/1'
|
428
|
+
expect(last_response.status).to eq(200)
|
429
|
+
expect(last_response.body).to eq('someone@testing.com1')
|
430
|
+
end
|
431
|
+
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
context 'from body parameters' do
|
436
|
+
before(:each) do
|
437
|
+
subject.post '/request_body' do
|
438
|
+
params[:user]
|
439
|
+
end
|
440
|
+
subject.put '/request_body' do
|
441
|
+
params[:user]
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
it 'converts JSON bodies to params' do
|
446
|
+
post '/request_body', MultiJson.dump(user: 'Bobby T.'), 'CONTENT_TYPE' => 'application/json'
|
447
|
+
expect(last_response.body).to eq('Bobby T.')
|
448
|
+
end
|
449
|
+
|
450
|
+
it 'does not convert empty JSON bodies to params' do
|
451
|
+
put '/request_body', '', 'CONTENT_TYPE' => 'application/json'
|
452
|
+
expect(last_response.body).to eq('')
|
453
|
+
end
|
454
|
+
|
455
|
+
it 'converts XML bodies to params' do
|
456
|
+
post '/request_body', '<user>Bobby T.</user>', 'CONTENT_TYPE' => 'application/xml'
|
457
|
+
expect(last_response.body).to eq('Bobby T.')
|
458
|
+
end
|
459
|
+
|
460
|
+
it 'converts XML bodies to params' do
|
461
|
+
put '/request_body', '<user>Bobby T.</user>', 'CONTENT_TYPE' => 'application/xml'
|
462
|
+
expect(last_response.body).to eq('Bobby T.')
|
463
|
+
end
|
464
|
+
|
465
|
+
it 'does not include parameters not defined by the body' do
|
466
|
+
subject.post '/omitted_params' do
|
467
|
+
error! 400, "expected nil" if params[:version]
|
468
|
+
params[:user]
|
469
|
+
end
|
470
|
+
post '/omitted_params', MultiJson.dump(user: 'Bob'), 'CONTENT_TYPE' => 'application/json'
|
471
|
+
expect(last_response.status).to eq(201)
|
472
|
+
expect(last_response.body).to eq("Bob")
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
it "responds with a 406 for an unsupported content-type" do
|
477
|
+
subject.format :json
|
478
|
+
# subject.content_type :json, "application/json"
|
479
|
+
subject.put '/request_body' do
|
480
|
+
params[:user]
|
481
|
+
end
|
482
|
+
put '/request_body', '<user>Bobby T.</user>', 'CONTENT_TYPE' => 'application/xml'
|
483
|
+
expect(last_response.status).to eq(406)
|
484
|
+
expect(last_response.body).to eq('{"error":"The requested content-type \'application/xml\' is not supported."}')
|
485
|
+
end
|
486
|
+
|
487
|
+
context 'content type with params' do
|
488
|
+
before do
|
489
|
+
subject.format :json
|
490
|
+
subject.content_type :json, 'application/json; charset=utf-8'
|
491
|
+
|
492
|
+
subject.post do
|
493
|
+
params[:data]
|
494
|
+
end
|
495
|
+
post '/', MultiJson.dump(data: { some: 'payload' }), 'CONTENT_TYPE' => 'application/json'
|
496
|
+
end
|
497
|
+
|
498
|
+
it "should not response with 406 for same type without params" do
|
499
|
+
expect(last_response.status).not_to be 406
|
500
|
+
end
|
501
|
+
|
502
|
+
it "should response with given content type in headers" do
|
503
|
+
expect(last_response.headers['Content-Type']).to eq 'application/json; charset=utf-8'
|
504
|
+
end
|
505
|
+
|
506
|
+
end
|
507
|
+
|
508
|
+
context 'precedence' do
|
509
|
+
|
510
|
+
before do
|
511
|
+
subject.format :json
|
512
|
+
subject.namespace '/:id' do
|
513
|
+
get do
|
514
|
+
{
|
515
|
+
params: params[:id]
|
516
|
+
}
|
517
|
+
end
|
518
|
+
post do
|
519
|
+
{
|
520
|
+
params: params[:id]
|
521
|
+
}
|
522
|
+
end
|
523
|
+
put do
|
524
|
+
{
|
525
|
+
params: params[:id]
|
526
|
+
}
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
it 'route string params have higher precedence than body params' do
|
532
|
+
post '/123', { id: 456 }.to_json
|
533
|
+
expect(JSON.parse(last_response.body)['params']).to eq '123'
|
534
|
+
put '/123', { id: 456 }.to_json
|
535
|
+
expect(JSON.parse(last_response.body)['params']).to eq '123'
|
536
|
+
end
|
537
|
+
|
538
|
+
it 'route string params have higher precedence than URL params' do
|
539
|
+
get '/123?id=456'
|
540
|
+
expect(JSON.parse(last_response.body)['params']).to eq '123'
|
541
|
+
post '/123?id=456'
|
542
|
+
expect(JSON.parse(last_response.body)['params']).to eq '123'
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
end
|
547
|
+
|
548
|
+
describe '#error!' do
|
549
|
+
it 'accepts a message' do
|
550
|
+
subject.get('/hey') do
|
551
|
+
error! "This is not valid."
|
552
|
+
"This is valid."
|
553
|
+
end
|
554
|
+
|
555
|
+
get '/hey'
|
556
|
+
expect(last_response.status).to eq(500)
|
557
|
+
expect(last_response.body).to eq("This is not valid.")
|
558
|
+
end
|
559
|
+
|
560
|
+
it 'accepts a code' do
|
561
|
+
subject.get('/hey') do
|
562
|
+
error! "Unauthorized.", 401
|
563
|
+
end
|
564
|
+
|
565
|
+
get '/hey'
|
566
|
+
expect(last_response.status).to eq(401)
|
567
|
+
expect(last_response.body).to eq("Unauthorized.")
|
568
|
+
end
|
569
|
+
|
570
|
+
it 'accepts an object and render it in format' do
|
571
|
+
subject.get '/hey' do
|
572
|
+
error!({ 'dude' => 'rad' }, 403)
|
573
|
+
end
|
574
|
+
|
575
|
+
get '/hey.json'
|
576
|
+
expect(last_response.status).to eq(403)
|
577
|
+
expect(last_response.body).to eq('{"dude":"rad"}')
|
578
|
+
end
|
579
|
+
|
580
|
+
it 'can specifiy headers' do
|
581
|
+
subject.get '/hey' do
|
582
|
+
error!({ 'dude' => 'rad' }, 403, 'X-Custom' => 'value')
|
583
|
+
end
|
584
|
+
|
585
|
+
get '/hey.json'
|
586
|
+
expect(last_response.status).to eq(403)
|
587
|
+
expect(last_response.headers['X-Custom']).to eq('value')
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
describe '#redirect' do
|
592
|
+
it 'redirects to a url with status 302' do
|
593
|
+
subject.get('/hey') do
|
594
|
+
redirect "/ha"
|
595
|
+
end
|
596
|
+
get '/hey'
|
597
|
+
expect(last_response.status).to eq 302
|
598
|
+
expect(last_response.headers['Location']).to eq "/ha"
|
599
|
+
expect(last_response.body).to eq ""
|
600
|
+
end
|
601
|
+
|
602
|
+
it 'has status code 303 if it is not get request and it is http 1.1' do
|
603
|
+
subject.post('/hey') do
|
604
|
+
redirect "/ha"
|
605
|
+
end
|
606
|
+
post '/hey', {}, 'HTTP_VERSION' => 'HTTP/1.1'
|
607
|
+
expect(last_response.status).to eq 303
|
608
|
+
expect(last_response.headers['Location']).to eq "/ha"
|
609
|
+
end
|
610
|
+
|
611
|
+
it 'support permanent redirect' do
|
612
|
+
subject.get('/hey') do
|
613
|
+
redirect "/ha", permanent: true
|
614
|
+
end
|
615
|
+
get '/hey'
|
616
|
+
expect(last_response.status).to eq 301
|
617
|
+
expect(last_response.headers['Location']).to eq "/ha"
|
618
|
+
expect(last_response.body).to eq ""
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
it 'does not persist params between calls' do
|
623
|
+
subject.post('/new') do
|
624
|
+
params[:text]
|
625
|
+
end
|
626
|
+
|
627
|
+
post '/new', text: 'abc'
|
628
|
+
expect(last_response.body).to eq('abc')
|
629
|
+
|
630
|
+
post '/new', text: 'def'
|
631
|
+
expect(last_response.body).to eq('def')
|
632
|
+
end
|
633
|
+
|
634
|
+
it 'resets all instance variables (except block) between calls' do
|
635
|
+
subject.helpers do
|
636
|
+
def memoized
|
637
|
+
@memoized ||= params[:howdy]
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
subject.get('/hello') do
|
642
|
+
memoized
|
643
|
+
end
|
644
|
+
|
645
|
+
get '/hello?howdy=hey'
|
646
|
+
expect(last_response.body).to eq('hey')
|
647
|
+
get '/hello?howdy=yo'
|
648
|
+
expect(last_response.body).to eq('yo')
|
649
|
+
end
|
650
|
+
|
651
|
+
it 'allows explicit return calls' do
|
652
|
+
subject.get('/home') do
|
653
|
+
return "Hello"
|
654
|
+
end
|
655
|
+
|
656
|
+
get '/home'
|
657
|
+
expect(last_response.status).to eq(200)
|
658
|
+
expect(last_response.body).to eq("Hello")
|
659
|
+
end
|
660
|
+
|
661
|
+
describe '.generate_api_method' do
|
662
|
+
it 'raises NameError if the method name is already in use' do
|
663
|
+
expect {
|
664
|
+
Grape::Endpoint.generate_api_method("version", &proc {})
|
665
|
+
}.to raise_error(NameError)
|
666
|
+
end
|
667
|
+
it 'raises ArgumentError if a block is not given' do
|
668
|
+
expect {
|
669
|
+
Grape::Endpoint.generate_api_method("GET without a block method")
|
670
|
+
}.to raise_error(ArgumentError)
|
671
|
+
end
|
672
|
+
it 'returns a Proc' do
|
673
|
+
expect(Grape::Endpoint.generate_api_method("GET test for a proc", &proc {})).to be_a Proc
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
context 'filters' do
|
678
|
+
describe 'before filters' do
|
679
|
+
it 'runs the before filter if set' do
|
680
|
+
subject.before { env['before_test'] = "OK" }
|
681
|
+
subject.get('/before_test') { env['before_test'] }
|
682
|
+
|
683
|
+
get '/before_test'
|
684
|
+
expect(last_response.body).to eq("OK")
|
685
|
+
end
|
686
|
+
end
|
687
|
+
|
688
|
+
describe 'after filters' do
|
689
|
+
it 'overrides the response body if it sets it' do
|
690
|
+
subject.after { body "after" }
|
691
|
+
subject.get('/after_test') { "during" }
|
692
|
+
get '/after_test'
|
693
|
+
expect(last_response.body).to eq('after')
|
694
|
+
end
|
695
|
+
|
696
|
+
it 'does not override the response body with its return' do
|
697
|
+
subject.after { "after" }
|
698
|
+
subject.get('/after_test') { "body" }
|
699
|
+
get '/after_test'
|
700
|
+
expect(last_response.body).to eq("body")
|
701
|
+
end
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
context 'anchoring' do
|
706
|
+
verbs = %w(post get head delete put options patch)
|
707
|
+
|
708
|
+
verbs.each do |verb|
|
709
|
+
it 'allows for the anchoring option with a #{verb.upcase} method' do
|
710
|
+
subject.send(verb, '/example', anchor: true) do
|
711
|
+
verb
|
712
|
+
end
|
713
|
+
send(verb, '/example/and/some/more')
|
714
|
+
expect(last_response.status).to eql 404
|
715
|
+
end
|
716
|
+
|
717
|
+
it 'anchors paths by default for the #{verb.upcase} method' do
|
718
|
+
subject.send(verb, '/example') do
|
719
|
+
verb
|
720
|
+
end
|
721
|
+
send(verb, '/example/and/some/more')
|
722
|
+
expect(last_response.status).to eql 404
|
723
|
+
end
|
724
|
+
|
725
|
+
it 'responds to /example/and/some/more for the non-anchored #{verb.upcase} method' do
|
726
|
+
subject.send(verb, '/example', anchor: false) do
|
727
|
+
verb
|
728
|
+
end
|
729
|
+
send(verb, '/example/and/some/more')
|
730
|
+
expect(last_response.status).to eql verb == "post" ? 201 : 200
|
731
|
+
expect(last_response.body).to eql verb == 'head' ? '' : verb
|
732
|
+
end
|
733
|
+
end
|
734
|
+
end
|
735
|
+
|
736
|
+
context 'request' do
|
737
|
+
it 'should be set to the url requested' do
|
738
|
+
subject.get('/url') do
|
739
|
+
request.url
|
740
|
+
end
|
741
|
+
get '/url'
|
742
|
+
expect(last_response.body).to eq("http://example.org/url")
|
743
|
+
end
|
744
|
+
['v1', :v1].each do |version|
|
745
|
+
it 'should include version #{version}' do
|
746
|
+
subject.version version, using: :path
|
747
|
+
subject.get('/url') do
|
748
|
+
request.url
|
749
|
+
end
|
750
|
+
get "/#{version}/url"
|
751
|
+
expect(last_response.body).to eq("http://example.org/#{version}/url")
|
752
|
+
end
|
753
|
+
end
|
754
|
+
it 'should include prefix' do
|
755
|
+
subject.version 'v1', using: :path
|
756
|
+
subject.prefix 'api'
|
757
|
+
subject.get('/url') do
|
758
|
+
request.url
|
759
|
+
end
|
760
|
+
get '/api/v1/url'
|
761
|
+
expect(last_response.body).to eq("http://example.org/api/v1/url")
|
762
|
+
end
|
763
|
+
end
|
764
|
+
|
765
|
+
context 'version headers' do
|
766
|
+
before do
|
767
|
+
# NOTE: a 404 is returned instead of the 406 if cascade: false is not set.
|
768
|
+
subject.version 'v1', using: :header, vendor: 'ohanapi', cascade: false
|
769
|
+
subject.get '/test' do
|
770
|
+
"Hello!"
|
771
|
+
end
|
772
|
+
end
|
773
|
+
|
774
|
+
it 'result in a 406 response if they are invalid' do
|
775
|
+
get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json'
|
776
|
+
expect(last_response.status).to eq(406)
|
777
|
+
end
|
778
|
+
|
779
|
+
it 'result in a 406 response if they cannot be parsed by rack-accept' do
|
780
|
+
get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json; version=1'
|
781
|
+
expect(last_response.status).to eq(406)
|
782
|
+
end
|
783
|
+
end
|
784
|
+
end
|