grape-security 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +45 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +70 -0
  5. data/.travis.yml +18 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG.md +314 -0
  8. data/CONTRIBUTING.md +118 -0
  9. data/Gemfile +21 -0
  10. data/Guardfile +14 -0
  11. data/LICENSE +20 -0
  12. data/README.md +1777 -0
  13. data/RELEASING.md +105 -0
  14. data/Rakefile +69 -0
  15. data/UPGRADING.md +124 -0
  16. data/grape-security.gemspec +39 -0
  17. data/grape.png +0 -0
  18. data/lib/grape.rb +99 -0
  19. data/lib/grape/api.rb +646 -0
  20. data/lib/grape/cookies.rb +39 -0
  21. data/lib/grape/endpoint.rb +533 -0
  22. data/lib/grape/error_formatter/base.rb +31 -0
  23. data/lib/grape/error_formatter/json.rb +15 -0
  24. data/lib/grape/error_formatter/txt.rb +16 -0
  25. data/lib/grape/error_formatter/xml.rb +15 -0
  26. data/lib/grape/exceptions/base.rb +66 -0
  27. data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
  28. data/lib/grape/exceptions/invalid_formatter.rb +10 -0
  29. data/lib/grape/exceptions/invalid_versioner_option.rb +10 -0
  30. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +10 -0
  31. data/lib/grape/exceptions/missing_mime_type.rb +10 -0
  32. data/lib/grape/exceptions/missing_option.rb +10 -0
  33. data/lib/grape/exceptions/missing_vendor_option.rb +10 -0
  34. data/lib/grape/exceptions/unknown_options.rb +10 -0
  35. data/lib/grape/exceptions/unknown_validator.rb +10 -0
  36. data/lib/grape/exceptions/validation.rb +26 -0
  37. data/lib/grape/exceptions/validation_errors.rb +43 -0
  38. data/lib/grape/formatter/base.rb +31 -0
  39. data/lib/grape/formatter/json.rb +12 -0
  40. data/lib/grape/formatter/serializable_hash.rb +35 -0
  41. data/lib/grape/formatter/txt.rb +11 -0
  42. data/lib/grape/formatter/xml.rb +12 -0
  43. data/lib/grape/http/request.rb +26 -0
  44. data/lib/grape/locale/en.yml +32 -0
  45. data/lib/grape/middleware/auth/base.rb +30 -0
  46. data/lib/grape/middleware/auth/basic.rb +13 -0
  47. data/lib/grape/middleware/auth/digest.rb +13 -0
  48. data/lib/grape/middleware/auth/oauth2.rb +83 -0
  49. data/lib/grape/middleware/base.rb +62 -0
  50. data/lib/grape/middleware/error.rb +89 -0
  51. data/lib/grape/middleware/filter.rb +17 -0
  52. data/lib/grape/middleware/formatter.rb +150 -0
  53. data/lib/grape/middleware/globals.rb +13 -0
  54. data/lib/grape/middleware/versioner.rb +32 -0
  55. data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
  56. data/lib/grape/middleware/versioner/header.rb +132 -0
  57. data/lib/grape/middleware/versioner/param.rb +42 -0
  58. data/lib/grape/middleware/versioner/path.rb +52 -0
  59. data/lib/grape/namespace.rb +23 -0
  60. data/lib/grape/parser/base.rb +29 -0
  61. data/lib/grape/parser/json.rb +11 -0
  62. data/lib/grape/parser/xml.rb +11 -0
  63. data/lib/grape/path.rb +70 -0
  64. data/lib/grape/route.rb +27 -0
  65. data/lib/grape/util/content_types.rb +18 -0
  66. data/lib/grape/util/deep_merge.rb +23 -0
  67. data/lib/grape/util/hash_stack.rb +120 -0
  68. data/lib/grape/validations.rb +322 -0
  69. data/lib/grape/validations/coerce.rb +63 -0
  70. data/lib/grape/validations/default.rb +25 -0
  71. data/lib/grape/validations/exactly_one_of.rb +26 -0
  72. data/lib/grape/validations/mutual_exclusion.rb +25 -0
  73. data/lib/grape/validations/presence.rb +16 -0
  74. data/lib/grape/validations/regexp.rb +12 -0
  75. data/lib/grape/validations/values.rb +23 -0
  76. data/lib/grape/version.rb +3 -0
  77. data/spec/grape/api_spec.rb +2571 -0
  78. data/spec/grape/endpoint_spec.rb +784 -0
  79. data/spec/grape/entity_spec.rb +324 -0
  80. data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
  81. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
  82. data/spec/grape/exceptions/missing_mime_type_spec.rb +18 -0
  83. data/spec/grape/exceptions/missing_option_spec.rb +18 -0
  84. data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
  85. data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
  86. data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
  87. data/spec/grape/middleware/auth/basic_spec.rb +31 -0
  88. data/spec/grape/middleware/auth/digest_spec.rb +47 -0
  89. data/spec/grape/middleware/auth/oauth2_spec.rb +135 -0
  90. data/spec/grape/middleware/base_spec.rb +58 -0
  91. data/spec/grape/middleware/error_spec.rb +45 -0
  92. data/spec/grape/middleware/exception_spec.rb +184 -0
  93. data/spec/grape/middleware/formatter_spec.rb +258 -0
  94. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
  95. data/spec/grape/middleware/versioner/header_spec.rb +302 -0
  96. data/spec/grape/middleware/versioner/param_spec.rb +58 -0
  97. data/spec/grape/middleware/versioner/path_spec.rb +44 -0
  98. data/spec/grape/middleware/versioner_spec.rb +22 -0
  99. data/spec/grape/path_spec.rb +229 -0
  100. data/spec/grape/util/hash_stack_spec.rb +132 -0
  101. data/spec/grape/validations/coerce_spec.rb +208 -0
  102. data/spec/grape/validations/default_spec.rb +123 -0
  103. data/spec/grape/validations/exactly_one_of_spec.rb +71 -0
  104. data/spec/grape/validations/mutual_exclusion_spec.rb +61 -0
  105. data/spec/grape/validations/presence_spec.rb +142 -0
  106. data/spec/grape/validations/regexp_spec.rb +40 -0
  107. data/spec/grape/validations/values_spec.rb +152 -0
  108. data/spec/grape/validations/zh-CN.yml +10 -0
  109. data/spec/grape/validations_spec.rb +994 -0
  110. data/spec/shared/versioning_examples.rb +121 -0
  111. data/spec/spec_helper.rb +26 -0
  112. data/spec/support/basic_auth_encode_helpers.rb +3 -0
  113. data/spec/support/content_type_helpers.rb +11 -0
  114. data/spec/support/versioned_helpers.rb +50 -0
  115. 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