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.
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