grape 1.7.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -1
  3. data/CONTRIBUTING.md +1 -1
  4. data/README.md +30 -25
  5. data/UPGRADING.md +35 -0
  6. data/grape.gemspec +3 -6
  7. data/lib/grape/api.rb +2 -2
  8. data/lib/grape/content_types.rb +2 -8
  9. data/lib/grape/dsl/desc.rb +1 -1
  10. data/lib/grape/dsl/inside_route.rb +11 -11
  11. data/lib/grape/dsl/request_response.rb +2 -1
  12. data/lib/grape/dsl/settings.rb +2 -6
  13. data/lib/grape/endpoint.rb +28 -18
  14. data/lib/grape/error_formatter/base.rb +1 -1
  15. data/lib/grape/exceptions/base.rb +2 -2
  16. data/lib/grape/exceptions/missing_group_type.rb +1 -6
  17. data/lib/grape/exceptions/unsupported_group_type.rb +1 -6
  18. data/lib/grape/exceptions/validation_errors.rb +1 -6
  19. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +3 -3
  20. data/lib/grape/extensions/hash.rb +4 -7
  21. data/lib/grape/extensions/hashie/mash.rb +3 -3
  22. data/lib/grape/formatter/serializable_hash.rb +7 -7
  23. data/lib/grape/http/headers.rb +12 -2
  24. data/lib/grape/middleware/auth/base.rb +1 -1
  25. data/lib/grape/middleware/auth/strategies.rb +1 -2
  26. data/lib/grape/middleware/error.rb +5 -5
  27. data/lib/grape/middleware/formatter.rb +6 -6
  28. data/lib/grape/middleware/versioner/header.rb +11 -19
  29. data/lib/grape/railtie.rb +9 -0
  30. data/lib/grape/request.rb +8 -2
  31. data/lib/grape/router/route.rb +1 -3
  32. data/lib/grape/util/lazy_value.rb +3 -11
  33. data/lib/grape/util/strict_hash_configuration.rb +3 -4
  34. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  35. data/lib/grape/validations/params_scope.rb +8 -2
  36. data/lib/grape/validations/single_attribute_iterator.rb +3 -1
  37. data/lib/grape/validations/types/custom_type_coercer.rb +2 -16
  38. data/lib/grape/validations/validators/base.rb +9 -20
  39. data/lib/grape/validations/validators/default_validator.rb +2 -20
  40. data/lib/grape/validations/validators/multiple_params_base.rb +4 -8
  41. data/lib/grape/validations/validators/values_validator.rb +14 -5
  42. data/lib/grape/version.rb +1 -1
  43. data/lib/grape.rb +26 -5
  44. metadata +13 -253
  45. data/lib/grape/config.rb +0 -34
  46. data/lib/grape/extensions/deep_mergeable_hash.rb +0 -21
  47. data/lib/grape/extensions/deep_symbolize_hash.rb +0 -32
  48. data/spec/grape/api/custom_validations_spec.rb +0 -256
  49. data/spec/grape/api/deeply_included_options_spec.rb +0 -56
  50. data/spec/grape/api/defines_boolean_in_params_spec.rb +0 -38
  51. data/spec/grape/api/documentation_spec.rb +0 -59
  52. data/spec/grape/api/inherited_helpers_spec.rb +0 -114
  53. data/spec/grape/api/instance_spec.rb +0 -103
  54. data/spec/grape/api/invalid_format_spec.rb +0 -45
  55. data/spec/grape/api/namespace_parameters_in_route_spec.rb +0 -38
  56. data/spec/grape/api/nested_helpers_spec.rb +0 -50
  57. data/spec/grape/api/optional_parameters_in_route_spec.rb +0 -43
  58. data/spec/grape/api/parameters_modification_spec.rb +0 -41
  59. data/spec/grape/api/patch_method_helpers_spec.rb +0 -79
  60. data/spec/grape/api/recognize_path_spec.rb +0 -21
  61. data/spec/grape/api/required_parameters_in_route_spec.rb +0 -37
  62. data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +0 -26
  63. data/spec/grape/api/routes_with_requirements_spec.rb +0 -59
  64. data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +0 -41
  65. data/spec/grape/api/shared_helpers_spec.rb +0 -36
  66. data/spec/grape/api_remount_spec.rb +0 -473
  67. data/spec/grape/api_spec.rb +0 -4347
  68. data/spec/grape/config_spec.rb +0 -17
  69. data/spec/grape/dsl/callbacks_spec.rb +0 -45
  70. data/spec/grape/dsl/desc_spec.rb +0 -101
  71. data/spec/grape/dsl/headers_spec.rb +0 -62
  72. data/spec/grape/dsl/helpers_spec.rb +0 -100
  73. data/spec/grape/dsl/inside_route_spec.rb +0 -535
  74. data/spec/grape/dsl/logger_spec.rb +0 -24
  75. data/spec/grape/dsl/middleware_spec.rb +0 -60
  76. data/spec/grape/dsl/parameters_spec.rb +0 -180
  77. data/spec/grape/dsl/request_response_spec.rb +0 -206
  78. data/spec/grape/dsl/routing_spec.rb +0 -275
  79. data/spec/grape/dsl/settings_spec.rb +0 -261
  80. data/spec/grape/dsl/validations_spec.rb +0 -55
  81. data/spec/grape/endpoint/declared_spec.rb +0 -846
  82. data/spec/grape/endpoint_spec.rb +0 -1085
  83. data/spec/grape/entity_spec.rb +0 -336
  84. data/spec/grape/exceptions/base_spec.rb +0 -81
  85. data/spec/grape/exceptions/body_parse_errors_spec.rb +0 -145
  86. data/spec/grape/exceptions/invalid_accept_header_spec.rb +0 -358
  87. data/spec/grape/exceptions/invalid_formatter_spec.rb +0 -15
  88. data/spec/grape/exceptions/invalid_response_spec.rb +0 -11
  89. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +0 -15
  90. data/spec/grape/exceptions/missing_group_type_spec.rb +0 -21
  91. data/spec/grape/exceptions/missing_mime_type_spec.rb +0 -17
  92. data/spec/grape/exceptions/missing_option_spec.rb +0 -15
  93. data/spec/grape/exceptions/unknown_options_spec.rb +0 -15
  94. data/spec/grape/exceptions/unknown_validator_spec.rb +0 -15
  95. data/spec/grape/exceptions/unsupported_group_type_spec.rb +0 -23
  96. data/spec/grape/exceptions/validation_errors_spec.rb +0 -92
  97. data/spec/grape/exceptions/validation_spec.rb +0 -19
  98. data/spec/grape/extensions/param_builders/hash_spec.rb +0 -83
  99. data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +0 -105
  100. data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +0 -79
  101. data/spec/grape/integration/global_namespace_function_spec.rb +0 -29
  102. data/spec/grape/integration/rack_sendfile_spec.rb +0 -48
  103. data/spec/grape/integration/rack_spec.rb +0 -51
  104. data/spec/grape/loading_spec.rb +0 -44
  105. data/spec/grape/middleware/auth/base_spec.rb +0 -31
  106. data/spec/grape/middleware/auth/dsl_spec.rb +0 -60
  107. data/spec/grape/middleware/auth/strategies_spec.rb +0 -120
  108. data/spec/grape/middleware/base_spec.rb +0 -221
  109. data/spec/grape/middleware/error_spec.rb +0 -85
  110. data/spec/grape/middleware/exception_spec.rb +0 -294
  111. data/spec/grape/middleware/formatter_spec.rb +0 -461
  112. data/spec/grape/middleware/globals_spec.rb +0 -30
  113. data/spec/grape/middleware/stack_spec.rb +0 -155
  114. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +0 -122
  115. data/spec/grape/middleware/versioner/header_spec.rb +0 -345
  116. data/spec/grape/middleware/versioner/param_spec.rb +0 -171
  117. data/spec/grape/middleware/versioner/path_spec.rb +0 -62
  118. data/spec/grape/middleware/versioner_spec.rb +0 -21
  119. data/spec/grape/named_api_spec.rb +0 -19
  120. data/spec/grape/parser_spec.rb +0 -86
  121. data/spec/grape/path_spec.rb +0 -252
  122. data/spec/grape/presenters/presenter_spec.rb +0 -71
  123. data/spec/grape/request_spec.rb +0 -136
  124. data/spec/grape/util/inheritable_setting_spec.rb +0 -242
  125. data/spec/grape/util/inheritable_values_spec.rb +0 -79
  126. data/spec/grape/util/reverse_stackable_values_spec.rb +0 -134
  127. data/spec/grape/util/stackable_values_spec.rb +0 -128
  128. data/spec/grape/util/strict_hash_configuration_spec.rb +0 -38
  129. data/spec/grape/validations/attributes_doc_spec.rb +0 -153
  130. data/spec/grape/validations/instance_behaivour_spec.rb +0 -43
  131. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +0 -40
  132. data/spec/grape/validations/params_scope_spec.rb +0 -1420
  133. data/spec/grape/validations/single_attribute_iterator_spec.rb +0 -57
  134. data/spec/grape/validations/types/array_coercer_spec.rb +0 -33
  135. data/spec/grape/validations/types/primitive_coercer_spec.rb +0 -150
  136. data/spec/grape/validations/types/set_coercer_spec.rb +0 -32
  137. data/spec/grape/validations/types_spec.rb +0 -111
  138. data/spec/grape/validations/validators/all_or_none_spec.rb +0 -162
  139. data/spec/grape/validations/validators/allow_blank_spec.rb +0 -575
  140. data/spec/grape/validations/validators/at_least_one_of_spec.rb +0 -205
  141. data/spec/grape/validations/validators/coerce_spec.rb +0 -1261
  142. data/spec/grape/validations/validators/default_spec.rb +0 -463
  143. data/spec/grape/validations/validators/exactly_one_of_spec.rb +0 -233
  144. data/spec/grape/validations/validators/except_values_spec.rb +0 -192
  145. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +0 -214
  146. data/spec/grape/validations/validators/presence_spec.rb +0 -315
  147. data/spec/grape/validations/validators/regexp_spec.rb +0 -161
  148. data/spec/grape/validations/validators/same_as_spec.rb +0 -57
  149. data/spec/grape/validations/validators/values_spec.rb +0 -696
  150. data/spec/grape/validations/validators/zh-CN.yml +0 -10
  151. data/spec/grape/validations_spec.rb +0 -2029
  152. data/spec/integration/eager_load/eager_load_spec.rb +0 -15
  153. data/spec/integration/multi_json/json_spec.rb +0 -7
  154. data/spec/integration/multi_xml/xml_spec.rb +0 -7
  155. data/spec/shared/versioning_examples.rb +0 -215
  156. data/spec/spec_helper.rb +0 -52
  157. data/spec/support/basic_auth_encode_helpers.rb +0 -11
  158. data/spec/support/chunks.rb +0 -14
  159. data/spec/support/content_type_helpers.rb +0 -15
  160. data/spec/support/endpoint_faker.rb +0 -25
  161. data/spec/support/file_streamer.rb +0 -13
  162. data/spec/support/integer_helpers.rb +0 -13
  163. data/spec/support/versioned_helpers.rb +0 -55
@@ -1,4347 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'shared/versioning_examples'
4
-
5
- describe Grape::API do
6
- subject do
7
- puts described_class
8
- Class.new(described_class)
9
- end
10
-
11
- def app
12
- subject
13
- end
14
-
15
- describe '.prefix' do
16
- it 'routes root through with the prefix' do
17
- subject.prefix 'awesome/sauce'
18
- subject.get do
19
- 'Hello there.'
20
- end
21
-
22
- get 'awesome/sauce/'
23
- expect(last_response.status).to be 200
24
- expect(last_response.body).to eql 'Hello there.'
25
- end
26
-
27
- it 'routes through with the prefix' do
28
- subject.prefix 'awesome/sauce'
29
- subject.get :hello do
30
- 'Hello there.'
31
- end
32
-
33
- get 'awesome/sauce/hello'
34
- expect(last_response.body).to eql 'Hello there.'
35
-
36
- get '/hello'
37
- expect(last_response.status).to be 404
38
- end
39
-
40
- it 'supports OPTIONS' do
41
- subject.prefix 'awesome/sauce'
42
- subject.get do
43
- 'Hello there.'
44
- end
45
-
46
- options 'awesome/sauce'
47
- expect(last_response.status).to be 204
48
- expect(last_response.body).to be_blank
49
- end
50
-
51
- it 'disallows POST' do
52
- subject.prefix 'awesome/sauce'
53
- subject.get
54
-
55
- post 'awesome/sauce'
56
- expect(last_response.status).to be 405
57
- end
58
- end
59
-
60
- describe '.version' do
61
- context 'when defined' do
62
- it 'returns version value' do
63
- subject.version 'v1'
64
- expect(subject.version).to eq('v1')
65
- end
66
- end
67
-
68
- context 'when not defined' do
69
- it 'returns nil' do
70
- expect(subject.version).to be_nil
71
- end
72
- end
73
- end
74
-
75
- describe '.version using path' do
76
- it_behaves_like 'versioning' do
77
- let(:macro_options) do
78
- {
79
- using: :path
80
- }
81
- end
82
- end
83
- end
84
-
85
- describe '.version using param' do
86
- it_behaves_like 'versioning' do
87
- let(:macro_options) do
88
- {
89
- using: :param,
90
- parameter: 'apiver'
91
- }
92
- end
93
- end
94
- end
95
-
96
- describe '.version using header' do
97
- it_behaves_like 'versioning' do
98
- let(:macro_options) do
99
- {
100
- using: :header,
101
- vendor: 'mycompany',
102
- format: 'json'
103
- }
104
- end
105
- end
106
- end
107
-
108
- describe '.version using accept_version_header' do
109
- it_behaves_like 'versioning' do
110
- let(:macro_options) do
111
- {
112
- using: :accept_version_header
113
- }
114
- end
115
- end
116
- end
117
-
118
- describe '.represent' do
119
- it 'requires a :with option' do
120
- expect { subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent)
121
- end
122
-
123
- it 'adds the association to the :representations setting' do
124
- klass = Class.new
125
- subject.represent Object, with: klass
126
- expect(subject.namespace_stackable_with_hash(:representations)[Object]).to eq(klass)
127
- end
128
- end
129
-
130
- describe '.namespace' do
131
- it 'is retrievable and converted to a path' do
132
- internal_namespace = nil
133
- subject.namespace :awesome do
134
- internal_namespace = namespace
135
- end
136
- expect(internal_namespace).to eql('/awesome')
137
- end
138
-
139
- it 'comes after the prefix and version' do
140
- subject.prefix :rad
141
- subject.version 'v1', using: :path
142
-
143
- subject.namespace :awesome do
144
- get('/hello') { 'worked' }
145
- end
146
-
147
- get '/rad/v1/awesome/hello'
148
- expect(last_response.body).to eq('worked')
149
- end
150
-
151
- it 'cancels itself after the block is over' do
152
- internal_namespace = nil
153
- subject.namespace :awesome do
154
- internal_namespace = namespace
155
- end
156
- expect(subject.namespace).to eql('/')
157
- end
158
-
159
- it 'is stackable' do
160
- internal_namespace = nil
161
- internal_second_namespace = nil
162
- subject.namespace :awesome do
163
- internal_namespace = namespace
164
- namespace :rad do
165
- internal_second_namespace = namespace
166
- end
167
- end
168
- expect(internal_namespace).to eq('/awesome')
169
- expect(internal_second_namespace).to eq('/awesome/rad')
170
- end
171
-
172
- it 'accepts path segments correctly' do
173
- inner_namespace = nil
174
- subject.namespace :members do
175
- namespace '/:member_id' do
176
- inner_namespace = namespace
177
- get '/' do
178
- params[:member_id]
179
- end
180
- end
181
- end
182
- get '/members/23'
183
- expect(last_response.body).to eq('23')
184
- expect(inner_namespace).to eq('/members/:member_id')
185
- end
186
-
187
- it 'is callable with nil just to push onto the stack' do
188
- subject.namespace do
189
- version 'v2', using: :path
190
- get('/hello') { 'inner' }
191
- end
192
- subject.get('/hello') { 'outer' }
193
-
194
- get '/v2/hello'
195
- expect(last_response.body).to eq('inner')
196
- get '/hello'
197
- expect(last_response.body).to eq('outer')
198
- end
199
-
200
- %w[group resource resources segment].each do |als|
201
- it "`.#{als}` is an alias" do
202
- inner_namespace = nil
203
- subject.send(als, :awesome) do
204
- inner_namespace = namespace
205
- end
206
- expect(inner_namespace).to eq '/awesome'
207
- end
208
- end
209
- end
210
-
211
- describe '.call' do
212
- context 'it does not add to the app setup' do
213
- it 'calls the app' do
214
- expect(subject).not_to receive(:add_setup)
215
- subject.call({})
216
- end
217
- end
218
- end
219
-
220
- describe '.route_param' do
221
- it 'adds a parameterized route segment namespace' do
222
- subject.namespace :users do
223
- route_param :id do
224
- get do
225
- params[:id]
226
- end
227
- end
228
- end
229
-
230
- get '/users/23'
231
- expect(last_response.body).to eq('23')
232
- end
233
-
234
- it 'defines requirements with a single hash' do
235
- subject.namespace :users do
236
- route_param :id, requirements: /[0-9]+/ do
237
- get do
238
- params[:id]
239
- end
240
- end
241
- end
242
-
243
- get '/users/michael'
244
- expect(last_response.status).to eq(404)
245
- get '/users/23'
246
- expect(last_response.status).to eq(200)
247
- end
248
-
249
- context 'with param type definitions' do
250
- it 'is used by passing to options' do
251
- subject.namespace :route_param do
252
- route_param :foo, type: Integer do
253
- get { params.to_json }
254
- end
255
- end
256
- get '/route_param/1234'
257
- expect(last_response.body).to eq('{"foo":1234}')
258
- end
259
- end
260
- end
261
-
262
- describe '.route' do
263
- it 'allows for no path' do
264
- subject.namespace :votes do
265
- get do
266
- 'Votes'
267
- end
268
- post do
269
- 'Created a Vote'
270
- end
271
- end
272
-
273
- get '/votes'
274
- expect(last_response.body).to eql 'Votes'
275
- post '/votes'
276
- expect(last_response.body).to eql 'Created a Vote'
277
- end
278
-
279
- it 'handles empty calls' do
280
- subject.get '/'
281
- get '/'
282
- expect(last_response.body).to eql ''
283
- end
284
-
285
- describe 'root routes should work with' do
286
- before do
287
- subject.format :txt
288
- subject.content_type :json, 'application/json'
289
- subject.formatter :json, ->(object, _env) { object }
290
- def subject.enable_root_route!
291
- get('/') { 'root' }
292
- end
293
- end
294
-
295
- after do
296
- expect(last_response.body).to eql 'root'
297
- end
298
-
299
- describe 'path versioned APIs' do
300
- before do
301
- subject.version version, using: :path
302
- subject.enable_root_route!
303
- end
304
-
305
- context 'when a single version provided' do
306
- let(:version) { 'v1' }
307
-
308
- it 'without a format' do
309
- versioned_get '/', 'v1', using: :path
310
- end
311
-
312
- it 'with a format' do
313
- get '/v1/.json'
314
- end
315
- end
316
-
317
- context 'when array of versions provided' do
318
- let(:version) { %w[v1 v2] }
319
-
320
- it { versioned_get '/', 'v1', using: :path }
321
- it { versioned_get '/', 'v2', using: :path }
322
- end
323
- end
324
-
325
- it 'header versioned APIs' do
326
- subject.version 'v1', using: :header, vendor: 'test'
327
- subject.enable_root_route!
328
-
329
- versioned_get '/', 'v1', using: :header, vendor: 'test'
330
- end
331
-
332
- it 'header versioned APIs with multiple headers' do
333
- subject.version %w[v1 v2], using: :header, vendor: 'test'
334
- subject.enable_root_route!
335
-
336
- versioned_get '/', 'v1', using: :header, vendor: 'test'
337
- versioned_get '/', 'v2', using: :header, vendor: 'test'
338
- end
339
-
340
- it 'param versioned APIs' do
341
- subject.version 'v1', using: :param
342
- subject.enable_root_route!
343
-
344
- versioned_get '/', 'v1', using: :param
345
- end
346
-
347
- it 'Accept-Version header versioned APIs' do
348
- subject.version 'v1', using: :accept_version_header
349
- subject.enable_root_route!
350
-
351
- versioned_get '/', 'v1', using: :accept_version_header
352
- end
353
-
354
- it 'unversioned APIs' do
355
- subject.enable_root_route!
356
-
357
- get '/'
358
- end
359
- end
360
-
361
- it 'allows for multiple paths' do
362
- subject.get(['/abc', '/def']) do
363
- 'foo'
364
- end
365
-
366
- get '/abc'
367
- expect(last_response.body).to eql 'foo'
368
- get '/def'
369
- expect(last_response.body).to eql 'foo'
370
- end
371
-
372
- context 'format' do
373
- module ApiSpec
374
- class DummyFormatClass
375
- end
376
- end
377
-
378
- before do
379
- allow_any_instance_of(ApiSpec::DummyFormatClass).to receive(:to_json).and_return('abc')
380
- allow_any_instance_of(ApiSpec::DummyFormatClass).to receive(:to_txt).and_return('def')
381
-
382
- subject.get('/abc') do
383
- ApiSpec::DummyFormatClass.new
384
- end
385
- end
386
-
387
- it 'allows .json' do
388
- get '/abc.json'
389
- expect(last_response.status).to eq(200)
390
- expect(last_response.body).to eql 'abc' # json-encoded symbol
391
- end
392
-
393
- it 'allows .txt' do
394
- get '/abc.txt'
395
- expect(last_response.status).to eq(200)
396
- expect(last_response.body).to eql 'def' # raw text
397
- end
398
- end
399
-
400
- it 'allows for format without corrupting a param' do
401
- subject.get('/:id') do
402
- { 'id' => params[:id] }
403
- end
404
-
405
- get '/awesome.json'
406
- expect(last_response.body).to eql '{"id":"awesome"}'
407
- end
408
-
409
- it 'allows for format in namespace with no path' do
410
- subject.namespace :abc do
411
- get do
412
- ['json']
413
- end
414
- end
415
-
416
- get '/abc.json'
417
- expect(last_response.body).to eql '["json"]'
418
- end
419
-
420
- it 'allows for multiple verbs' do
421
- subject.route(%i[get post], '/abc') do
422
- 'hiya'
423
- end
424
-
425
- subject.endpoints.first.routes.each do |route|
426
- expect(route.path).to eql '/abc(.:format)'
427
- end
428
-
429
- get '/abc'
430
- expect(last_response.body).to eql 'hiya'
431
- post '/abc'
432
- expect(last_response.body).to eql 'hiya'
433
- end
434
-
435
- objects = ['string', :symbol, 1, -1.1, {}, [], true, false, nil].freeze
436
- %i[put post].each do |verb|
437
- context verb.to_s do
438
- objects.each do |object|
439
- it "allows a(n) #{object.class} json object in params" do
440
- subject.format :json
441
- subject.send(verb) do
442
- env['api.request.body']
443
- end
444
- send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json'
445
- expect(last_response.status).to eq(verb == :post ? 201 : 200)
446
- expect(last_response.body).to eql ::Grape::Json.dump(object)
447
- expect(last_request.params).to eql({})
448
- end
449
-
450
- it 'stores input in api.request.input' do
451
- subject.format :json
452
- subject.send(verb) do
453
- env['api.request.input']
454
- end
455
- send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json'
456
- expect(last_response.status).to eq(verb == :post ? 201 : 200)
457
- expect(last_response.body).to eql ::Grape::Json.dump(object).to_json
458
- end
459
-
460
- context 'chunked transfer encoding' do
461
- it 'stores input in api.request.input' do
462
- subject.format :json
463
- subject.send(verb) do
464
- env['api.request.input']
465
- end
466
- send verb, '/', ::Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked', 'CONTENT_LENGTH' => nil
467
- expect(last_response.status).to eq(verb == :post ? 201 : 200)
468
- expect(last_response.body).to eql ::Grape::Json.dump(object).to_json
469
- end
470
- end
471
- end
472
- end
473
- end
474
-
475
- it 'allows for multipart paths' do
476
- subject.route(%i[get post], '/:id/first') do
477
- 'first'
478
- end
479
-
480
- subject.route(%i[get post], '/:id') do
481
- 'ola'
482
- end
483
- subject.route(%i[get post], '/:id/first/second') do
484
- 'second'
485
- end
486
-
487
- get '/1'
488
- expect(last_response.body).to eql 'ola'
489
- post '/1'
490
- expect(last_response.body).to eql 'ola'
491
- get '/1/first'
492
- expect(last_response.body).to eql 'first'
493
- post '/1/first'
494
- expect(last_response.body).to eql 'first'
495
- get '/1/first/second'
496
- expect(last_response.body).to eql 'second'
497
- end
498
-
499
- it 'allows for :any as a verb' do
500
- subject.route(:any, '/abc') do
501
- 'lol'
502
- end
503
-
504
- %w[get post put delete options patch].each do |m|
505
- send(m, '/abc')
506
- expect(last_response.body).to eql 'lol'
507
- end
508
- end
509
-
510
- it 'allows for catch-all in a namespace' do
511
- subject.namespace :nested do
512
- get do
513
- 'root'
514
- end
515
-
516
- get 'something' do
517
- 'something'
518
- end
519
-
520
- route :any, '*path' do
521
- 'catch-all'
522
- end
523
- end
524
-
525
- get 'nested'
526
- expect(last_response.body).to eql 'root'
527
-
528
- get 'nested/something'
529
- expect(last_response.body).to eql 'something'
530
-
531
- get 'nested/missing'
532
- expect(last_response.body).to eql 'catch-all'
533
-
534
- post 'nested'
535
- expect(last_response.body).to eql 'catch-all'
536
-
537
- post 'nested/something'
538
- expect(last_response.body).to eql 'catch-all'
539
- end
540
-
541
- verbs = %w[post get head delete put options patch]
542
- verbs.each do |verb|
543
- it "allows and properly constrain a #{verb.upcase} method" do
544
- subject.send(verb, '/example') do
545
- verb
546
- end
547
- send(verb, '/example')
548
- expect(last_response.body).to eql verb == 'head' ? '' : verb
549
- # Call it with all methods other than the properly constrained one.
550
- (verbs - [verb]).each do |other_verb|
551
- send(other_verb, '/example')
552
- expected_rc = if other_verb == 'options' then 204
553
- elsif other_verb == 'head' && verb == 'get' then 200
554
- else
555
- 405
556
- end
557
- expect(last_response.status).to eql expected_rc
558
- end
559
- end
560
- end
561
-
562
- it 'returns a 201 response code for POST by default' do
563
- subject.post('example') do
564
- 'Created'
565
- end
566
-
567
- post '/example'
568
- expect(last_response.status).to be 201
569
- expect(last_response.body).to eql 'Created'
570
- end
571
-
572
- it 'returns a 405 for an unsupported method with an X-Custom-Header' do
573
- subject.before { header 'X-Custom-Header', 'foo' }
574
- subject.get 'example' do
575
- 'example'
576
- end
577
- put '/example'
578
- expect(last_response.status).to be 405
579
- expect(last_response.body).to eql '405 Not Allowed'
580
- expect(last_response.headers['X-Custom-Header']).to eql 'foo'
581
- end
582
-
583
- it 'runs only the before filter on 405 bad method' do
584
- subject.namespace :example do
585
- before { header 'X-Custom-Header', 'foo' }
586
-
587
- before_validation { raise 'before_validation filter should not run' }
588
- after_validation { raise 'after_validation filter should not run' }
589
- after { raise 'after filter should not run' }
590
-
591
- params { requires :only_for_get }
592
- get
593
- end
594
-
595
- post '/example'
596
- expect(last_response.status).to be 405
597
- expect(last_response.headers['X-Custom-Header']).to eql 'foo'
598
- end
599
-
600
- it 'runs before filter exactly once on 405 bad method' do
601
- already_run = false
602
- subject.namespace :example do
603
- before do
604
- raise 'before filter ran twice' if already_run
605
-
606
- already_run = true
607
- header 'X-Custom-Header', 'foo'
608
- end
609
-
610
- get
611
- end
612
-
613
- post '/example'
614
- expect(last_response.status).to be 405
615
- expect(last_response.headers['X-Custom-Header']).to eql 'foo'
616
- end
617
-
618
- it 'runs all filters and body with a custom OPTIONS method' do
619
- subject.namespace :example do
620
- before { header 'X-Custom-Header-1', 'foo' }
621
-
622
- before_validation { header 'X-Custom-Header-2', 'foo' }
623
- after_validation { header 'X-Custom-Header-3', 'foo' }
624
- after { header 'X-Custom-Header-4', 'foo' }
625
-
626
- options { 'yup' }
627
- get
628
- end
629
-
630
- options '/example'
631
- expect(last_response.status).to be 200
632
- expect(last_response.body).to eql 'yup'
633
- expect(last_response.headers['Allow']).to be_nil
634
- expect(last_response.headers['X-Custom-Header-1']).to eql 'foo'
635
- expect(last_response.headers['X-Custom-Header-2']).to eql 'foo'
636
- expect(last_response.headers['X-Custom-Header-3']).to eql 'foo'
637
- expect(last_response.headers['X-Custom-Header-4']).to eql 'foo'
638
- end
639
-
640
- context 'when format is xml' do
641
- it 'returns a 405 for an unsupported method' do
642
- subject.format :xml
643
- subject.get 'example' do
644
- 'example'
645
- end
646
-
647
- put '/example'
648
- expect(last_response.status).to be 405
649
- expect(last_response.body).to eq <<~XML
650
- <?xml version="1.0" encoding="UTF-8"?>
651
- <error>
652
- <message>405 Not Allowed</message>
653
- </error>
654
- XML
655
- end
656
- end
657
-
658
- context 'when accessing env' do
659
- it 'returns a 405 for an unsupported method' do
660
- subject.before do
661
- _customheader1 = headers['X-Custom-Header']
662
- _customheader2 = env['HTTP_X_CUSTOM_HEADER']
663
- end
664
- subject.get 'example' do
665
- 'example'
666
- end
667
- put '/example'
668
- expect(last_response.status).to be 405
669
- expect(last_response.body).to eql '405 Not Allowed'
670
- end
671
- end
672
-
673
- specify '405 responses includes an Allow header specifying supported methods' do
674
- subject.get 'example' do
675
- 'example'
676
- end
677
- subject.post 'example' do
678
- 'example'
679
- end
680
- put '/example'
681
- expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, POST, HEAD'
682
- end
683
-
684
- specify '405 responses includes an Content-Type header' do
685
- subject.get 'example' do
686
- 'example'
687
- end
688
- subject.post 'example' do
689
- 'example'
690
- end
691
- put '/example'
692
- expect(last_response.headers['Content-Type']).to eql 'text/plain'
693
- end
694
-
695
- describe 'adds an OPTIONS route that' do
696
- before do
697
- subject.before { header 'X-Custom-Header', 'foo' }
698
- subject.before_validation { header 'X-Custom-Header-2', 'bar' }
699
- subject.after_validation { header 'X-Custom-Header-3', 'baz' }
700
- subject.after { header 'X-Custom-Header-4', 'bing' }
701
- subject.params { requires :only_for_get }
702
- subject.get 'example' do
703
- 'example'
704
- end
705
- subject.route :any, '*path' do
706
- error! :not_found, 404
707
- end
708
- options '/example'
709
- end
710
-
711
- it 'returns a 204' do
712
- expect(last_response.status).to be 204
713
- end
714
-
715
- it 'has an empty body' do
716
- expect(last_response.body).to be_blank
717
- end
718
-
719
- it 'has an Allow header' do
720
- expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD'
721
- end
722
-
723
- it 'calls before hook' do
724
- expect(last_response.headers['X-Custom-Header']).to eql 'foo'
725
- end
726
-
727
- it 'does not call before_validation hook' do
728
- expect(last_response.headers.key?('X-Custom-Header-2')).to be false
729
- end
730
-
731
- it 'does not call after_validation hook' do
732
- expect(last_response.headers.key?('X-Custom-Header-3')).to be false
733
- end
734
-
735
- it 'calls after hook' do
736
- expect(last_response.headers['X-Custom-Header-4']).to eq 'bing'
737
- end
738
-
739
- it 'has no Content-Type' do
740
- expect(last_response.content_type).to be_nil
741
- end
742
-
743
- it 'has no Content-Length' do
744
- expect(last_response.content_length).to be_nil
745
- end
746
- end
747
-
748
- describe 'when a resource routes by POST, GET, PATCH, PUT, and DELETE' do
749
- before do
750
- subject.namespace :example do
751
- get do
752
- 'example'
753
- end
754
-
755
- patch do
756
- 'example'
757
- end
758
-
759
- post do
760
- 'example'
761
- end
762
-
763
- delete do
764
- 'example'
765
- end
766
-
767
- put do
768
- 'example'
769
- end
770
- end
771
- options '/example'
772
- end
773
-
774
- describe 'it adds an OPTIONS route for namespaced endpoints that' do
775
- it 'returns a 204' do
776
- expect(last_response.status).to be 204
777
- end
778
-
779
- it 'has an empty body' do
780
- expect(last_response.body).to be_blank
781
- end
782
-
783
- it 'has an Allow header' do
784
- expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, PATCH, POST, DELETE, PUT, HEAD'
785
- end
786
- end
787
- end
788
-
789
- describe 'adds an OPTIONS route for namespaced endpoints that' do
790
- before do
791
- subject.before { header 'X-Custom-Header', 'foo' }
792
- subject.namespace :example do
793
- before { header 'X-Custom-Header-2', 'foo' }
794
-
795
- get :inner do
796
- 'example/inner'
797
- end
798
- end
799
- options '/example/inner'
800
- end
801
-
802
- it 'returns a 204' do
803
- expect(last_response.status).to be 204
804
- end
805
-
806
- it 'has an empty body' do
807
- expect(last_response.body).to be_blank
808
- end
809
-
810
- it 'has an Allow header' do
811
- expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD'
812
- end
813
-
814
- it 'calls the outer before filter' do
815
- expect(last_response.headers['X-Custom-Header']).to eql 'foo'
816
- end
817
-
818
- it 'calls the inner before filter' do
819
- expect(last_response.headers['X-Custom-Header-2']).to eql 'foo'
820
- end
821
-
822
- it 'has no Content-Type' do
823
- expect(last_response.content_type).to be_nil
824
- end
825
-
826
- it 'has no Content-Length' do
827
- expect(last_response.content_length).to be_nil
828
- end
829
- end
830
-
831
- describe 'adds a 405 Not Allowed route that' do
832
- before do
833
- subject.before { header 'X-Custom-Header', 'foo' }
834
- subject.post :example do
835
- 'example'
836
- end
837
- get '/example'
838
- end
839
-
840
- it 'returns a 405' do
841
- expect(last_response.status).to be 405
842
- end
843
-
844
- it 'contains error message in body' do
845
- expect(last_response.body).to eq '405 Not Allowed'
846
- end
847
-
848
- it 'has an Allow header' do
849
- expect(last_response.headers['Allow']).to eql 'OPTIONS, POST'
850
- end
851
-
852
- it 'has a X-Custom-Header' do
853
- expect(last_response.headers['X-Custom-Header']).to eql 'foo'
854
- end
855
- end
856
-
857
- describe 'when hook behaviour is controlled by attributes on the route' do
858
- before do
859
- subject.before do
860
- error!('Access Denied', 401) unless route.options[:secret] == params[:secret]
861
- end
862
-
863
- subject.namespace 'example' do
864
- before do
865
- error!('Access Denied', 401) unless route.options[:namespace_secret] == params[:namespace_secret]
866
- end
867
-
868
- desc 'it gets with secret', secret: 'password'
869
- get { status(params[:id] == '504' ? 200 : 404) }
870
-
871
- desc 'it post with secret', secret: 'password', namespace_secret: 'namespace_password'
872
- post {}
873
- end
874
- end
875
-
876
- context 'when HTTP method is not defined' do
877
- let(:response) { delete('/example') }
878
-
879
- it 'responds with a 405 status' do
880
- expect(response.status).to be 405
881
- end
882
- end
883
-
884
- context 'when HTTP method is defined with attribute' do
885
- let(:response) { post('/example?secret=incorrect_password') }
886
-
887
- it 'responds with the defined error in the before hook' do
888
- expect(response.status).to be 401
889
- end
890
- end
891
-
892
- context 'when HTTP method is defined and the underlying before hook expectation is not met' do
893
- let(:response) { post('/example?secret=password&namespace_secret=wrong_namespace_password') }
894
-
895
- it 'ends up in the endpoint' do
896
- expect(response.status).to be 401
897
- end
898
- end
899
-
900
- context 'when HTTP method is defined and everything is like the before hooks expect' do
901
- let(:response) { post('/example?secret=password&namespace_secret=namespace_password') }
902
-
903
- it 'ends up in the endpoint' do
904
- expect(response.status).to be 201
905
- end
906
- end
907
-
908
- context 'when HEAD is called for the defined GET' do
909
- let(:response) { head('/example?id=504') }
910
-
911
- it 'responds with 401 because before expectations in before hooks are not met' do
912
- expect(response.status).to be 401
913
- end
914
- end
915
-
916
- context 'when HEAD is called for the defined GET' do
917
- let(:response) { head('/example?id=504&secret=password') }
918
-
919
- it 'responds with 200 because before hooks are not called' do
920
- expect(response.status).to be 200
921
- end
922
- end
923
- end
924
-
925
- context 'allows HEAD on a GET request that' do
926
- before do
927
- subject.get 'example' do
928
- 'example'
929
- end
930
- subject.route :any, '*path' do
931
- error! :not_found, 404
932
- end
933
- head '/example'
934
- end
935
-
936
- it 'returns a 200' do
937
- expect(last_response.status).to be 200
938
- end
939
-
940
- it 'has an empty body' do
941
- expect(last_response.body).to eql ''
942
- end
943
- end
944
-
945
- it 'overwrites the default HEAD request' do
946
- subject.head 'example' do
947
- error! 'nothing to see here', 400
948
- end
949
- subject.get 'example' do
950
- 'example'
951
- end
952
- head '/example'
953
- expect(last_response.status).to be 400
954
- end
955
- end
956
-
957
- context 'do_not_route_head!' do
958
- before do
959
- subject.do_not_route_head!
960
- subject.get 'example' do
961
- 'example'
962
- end
963
- end
964
-
965
- it 'options does not contain HEAD' do
966
- options '/example'
967
- expect(last_response.status).to be 204
968
- expect(last_response.body).to eql ''
969
- expect(last_response.headers['Allow']).to eql 'OPTIONS, GET'
970
- end
971
-
972
- it 'does not allow HEAD on a GET request' do
973
- head '/example'
974
- expect(last_response.status).to be 405
975
- end
976
- end
977
-
978
- context 'do_not_route_options!' do
979
- before do
980
- subject.do_not_route_options!
981
- subject.get 'example' do
982
- 'example'
983
- end
984
- end
985
-
986
- it 'does not create an OPTIONS route' do
987
- options '/example'
988
- expect(last_response.status).to be 405
989
- end
990
-
991
- it 'does not include OPTIONS in Allow header' do
992
- options '/example'
993
- expect(last_response.status).to be 405
994
- expect(last_response.headers['Allow']).to eql 'GET, HEAD'
995
- end
996
- end
997
-
998
- describe '.compile!' do
999
- it 'requires the grape/eager_load file' do
1000
- expect(app).to receive(:require).with('grape/eager_load').and_return(nil)
1001
- app.compile!
1002
- end
1003
-
1004
- it 'compiles the instance for rack!' do
1005
- stubbed_object = double(:instance_for_rack)
1006
- allow(app).to receive(:instance_for_rack) { stubbed_object }
1007
- end
1008
- end
1009
-
1010
- # NOTE: this method is required to preserve the ability of pre-mounting
1011
- # the root API into a namespace, it may be deprecated in the future.
1012
- describe 'instance_for_rack' do
1013
- context 'when the app was not mounted' do
1014
- it 'returns the base_instance' do
1015
- expect(app.send(:instance_for_rack)).to eq app.base_instance
1016
- end
1017
- end
1018
-
1019
- context 'when the app was mounted' do
1020
- it 'returns the first mounted instance' do
1021
- mounted_app = app
1022
- Class.new(described_class) do
1023
- namespace 'new_namespace' do
1024
- mount mounted_app
1025
- end
1026
- end
1027
- expect(app.send(:instance_for_rack)).to eq app.send(:mounted_instances).first
1028
- end
1029
- end
1030
- end
1031
-
1032
- describe 'filters' do
1033
- it 'adds a before filter' do
1034
- subject.before { @foo = 'first' }
1035
- subject.before { @bar = 'second' }
1036
- subject.get '/' do
1037
- "#{@foo} #{@bar}"
1038
- end
1039
-
1040
- get '/'
1041
- expect(last_response.body).to eql 'first second'
1042
- end
1043
-
1044
- it 'adds a before filter to current and child namespaces only' do
1045
- subject.get '/' do
1046
- "root - #{instance_variable_defined?(:@foo) ? @foo : nil}"
1047
- end
1048
- subject.namespace :blah do
1049
- before { @foo = 'foo' }
1050
-
1051
- get '/' do
1052
- "blah - #{@foo}"
1053
- end
1054
-
1055
- namespace :bar do
1056
- get '/' do
1057
- "blah - bar - #{@foo}"
1058
- end
1059
- end
1060
- end
1061
-
1062
- get '/'
1063
- expect(last_response.body).to eql 'root - '
1064
- get '/blah'
1065
- expect(last_response.body).to eql 'blah - foo'
1066
- get '/blah/bar'
1067
- expect(last_response.body).to eql 'blah - bar - foo'
1068
- end
1069
-
1070
- it 'adds a after_validation filter' do
1071
- subject.after_validation { @foo = "first #{params[:id]}:#{params[:id].class}" }
1072
- subject.after_validation { @bar = 'second' }
1073
- subject.params do
1074
- requires :id, type: Integer
1075
- end
1076
- subject.get '/' do
1077
- "#{@foo} #{@bar}"
1078
- end
1079
-
1080
- get '/', id: '32'
1081
- expect(last_response.body).to eql "first 32:#{integer_class_name} second"
1082
- end
1083
-
1084
- it 'adds a after filter' do
1085
- m = double('after mock')
1086
- subject.after { m.do_something! }
1087
- subject.after { m.do_something! }
1088
- subject.get '/' do
1089
- @var ||= 'default'
1090
- end
1091
-
1092
- expect(m).to receive(:do_something!).twice
1093
- get '/'
1094
- expect(last_response.body).to eql 'default'
1095
- end
1096
-
1097
- it 'calls all filters when validation passes' do
1098
- a = double('before mock')
1099
- b = double('before_validation mock')
1100
- c = double('after_validation mock')
1101
- d = double('after mock')
1102
-
1103
- subject.params do
1104
- requires :id, type: Integer
1105
- end
1106
- subject.resource ':id' do
1107
- before { a.do_something! }
1108
-
1109
- before_validation { b.do_something! }
1110
- after_validation { c.do_something! }
1111
- after { d.do_something! }
1112
-
1113
- get do
1114
- 'got it'
1115
- end
1116
- end
1117
-
1118
- expect(a).to receive(:do_something!).once
1119
- expect(b).to receive(:do_something!).once
1120
- expect(c).to receive(:do_something!).once
1121
- expect(d).to receive(:do_something!).once
1122
-
1123
- get '/123'
1124
- expect(last_response.status).to be 200
1125
- expect(last_response.body).to eql 'got it'
1126
- end
1127
-
1128
- it 'calls only before filters when validation fails' do
1129
- a = double('before mock')
1130
- b = double('before_validation mock')
1131
- c = double('after_validation mock')
1132
- d = double('after mock')
1133
-
1134
- subject.params do
1135
- requires :id, type: Integer
1136
- end
1137
- subject.resource ':id' do
1138
- before { a.do_something! }
1139
-
1140
- before_validation { b.do_something! }
1141
- after_validation { c.do_something! }
1142
- after { d.do_something! }
1143
-
1144
- get do
1145
- 'got it'
1146
- end
1147
- end
1148
-
1149
- expect(a).to receive(:do_something!).once
1150
- expect(b).to receive(:do_something!).once
1151
- expect(c).to receive(:do_something!).exactly(0).times
1152
- expect(d).to receive(:do_something!).exactly(0).times
1153
-
1154
- get '/abc'
1155
- expect(last_response.status).to be 400
1156
- expect(last_response.body).to eql 'id is invalid'
1157
- end
1158
-
1159
- it 'calls filters in the correct order' do
1160
- i = 0
1161
- a = double('before mock')
1162
- b = double('before_validation mock')
1163
- c = double('after_validation mock')
1164
- d = double('after mock')
1165
-
1166
- subject.params do
1167
- requires :id, type: Integer
1168
- end
1169
- subject.resource ':id' do
1170
- before { a.here(i += 1) }
1171
-
1172
- before_validation { b.here(i += 1) }
1173
- after_validation { c.here(i += 1) }
1174
- after { d.here(i += 1) }
1175
-
1176
- get do
1177
- 'got it'
1178
- end
1179
- end
1180
-
1181
- expect(a).to receive(:here).with(1).once
1182
- expect(b).to receive(:here).with(2).once
1183
- expect(c).to receive(:here).with(3).once
1184
- expect(d).to receive(:here).with(4).once
1185
-
1186
- get '/123'
1187
- expect(last_response.status).to be 200
1188
- expect(last_response.body).to eql 'got it'
1189
- end
1190
- end
1191
-
1192
- context 'format' do
1193
- before do
1194
- subject.get('/foo') { 'bar' }
1195
- end
1196
-
1197
- it 'sets content type for txt format' do
1198
- get '/foo'
1199
- expect(last_response.headers['Content-Type']).to eq('text/plain')
1200
- end
1201
-
1202
- it 'does not set Cache-Control' do
1203
- get '/foo'
1204
- expect(last_response.headers['Cache-Control']).to be_nil
1205
- end
1206
-
1207
- it 'sets content type for xml' do
1208
- get '/foo.xml'
1209
- expect(last_response.headers['Content-Type']).to eq('application/xml')
1210
- end
1211
-
1212
- it 'sets content type for json' do
1213
- get '/foo.json'
1214
- expect(last_response.headers['Content-Type']).to eq('application/json')
1215
- end
1216
-
1217
- it 'sets content type for serializable hash format' do
1218
- get '/foo.serializable_hash'
1219
- expect(last_response.headers['Content-Type']).to eq('application/json')
1220
- end
1221
-
1222
- it 'sets content type for binary format' do
1223
- get '/foo.binary'
1224
- expect(last_response.headers['Content-Type']).to eq('application/octet-stream')
1225
- end
1226
-
1227
- it 'returns raw data when content type binary' do
1228
- image_filename = 'grape.png'
1229
- file = File.binread(image_filename)
1230
- subject.format :binary
1231
- subject.get('/binary_file') { File.binread(image_filename) }
1232
- get '/binary_file'
1233
- expect(last_response.headers['Content-Type']).to eq('application/octet-stream')
1234
- expect(last_response.body).to eq(file)
1235
- end
1236
-
1237
- it 'returns the content of the file with file' do
1238
- file_content = 'This is some file content'
1239
- test_file = Tempfile.new('test')
1240
- test_file.write file_content
1241
- test_file.rewind
1242
-
1243
- subject.get('/file') { file test_file }
1244
- get '/file'
1245
- expect(last_response.headers['Content-Length']).to eq('25')
1246
- expect(last_response.headers['Content-Type']).to eq('text/plain')
1247
- expect(last_response.body).to eq(file_content)
1248
- end
1249
-
1250
- it 'streams the content of the file with stream' do
1251
- test_stream = Enumerator.new do |blk|
1252
- blk.yield 'This is some'
1253
- blk.yield ' file content'
1254
- end
1255
-
1256
- subject.use Rack::Chunked
1257
- subject.get('/stream') { stream test_stream }
1258
- get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1', 'SERVER_PROTOCOL' => 'HTTP/1.1'
1259
-
1260
- expect(last_response.headers['Content-Type']).to eq('text/plain')
1261
- expect(last_response.headers['Content-Length']).to be_nil
1262
- expect(last_response.headers['Cache-Control']).to eq('no-cache')
1263
- expect(last_response.headers['Transfer-Encoding']).to eq('chunked')
1264
-
1265
- expect(last_response.body).to eq("c\r\nThis is some\r\nd\r\n file content\r\n0\r\n\r\n")
1266
- end
1267
-
1268
- it 'sets content type for error' do
1269
- subject.get('/error') { error!('error in plain text', 500) }
1270
- get '/error'
1271
- expect(last_response.headers['Content-Type']).to eql 'text/plain'
1272
- end
1273
-
1274
- it 'sets content type for json error' do
1275
- subject.format :json
1276
- subject.get('/error') { error!('error in json', 500) }
1277
- get '/error.json'
1278
- expect(last_response.status).to be 500
1279
- expect(last_response.headers['Content-Type']).to eql 'application/json'
1280
- end
1281
-
1282
- it 'sets content type for xml error' do
1283
- subject.format :xml
1284
- subject.get('/error') { error!('error in xml', 500) }
1285
- get '/error'
1286
- expect(last_response.status).to be 500
1287
- expect(last_response.headers['Content-Type']).to eql 'application/xml'
1288
- end
1289
-
1290
- it 'includes extension in format' do
1291
- subject.get(':id') { params[:format] }
1292
-
1293
- get '/baz.bar'
1294
- expect(last_response.status).to eq 200
1295
- expect(last_response.body).to eq 'bar'
1296
- end
1297
-
1298
- it 'does not include extension in id' do
1299
- subject.format :json
1300
- subject.get(':id') { params }
1301
-
1302
- get '/baz.bar'
1303
- expect(last_response.status).to eq 404
1304
- end
1305
-
1306
- context 'with a custom content_type' do
1307
- before do
1308
- subject.content_type :custom, 'application/custom'
1309
- subject.formatter :custom, ->(_object, _env) { 'custom' }
1310
-
1311
- subject.get('/custom') { 'bar' }
1312
- subject.get('/error') { error!('error in custom', 500) }
1313
- end
1314
-
1315
- it 'sets content type' do
1316
- get '/custom.custom'
1317
- expect(last_response.headers['Content-Type']).to eql 'application/custom'
1318
- end
1319
-
1320
- it 'sets content type for error' do
1321
- get '/error.custom'
1322
- expect(last_response.headers['Content-Type']).to eql 'application/custom'
1323
- end
1324
- end
1325
-
1326
- context 'env["api.format"]' do
1327
- before do
1328
- subject.post 'attachment' do
1329
- filename = params[:file][:filename]
1330
- content_type MIME::Types.type_for(filename)[0].to_s
1331
- env['api.format'] = :binary # there's no formatter for :binary, data will be returned "as is"
1332
- header 'Content-Disposition', "attachment; filename*=UTF-8''#{CGI.escape(filename)}"
1333
- params[:file][:tempfile].read
1334
- end
1335
- end
1336
-
1337
- ['/attachment.png', 'attachment'].each do |url|
1338
- it "uploads and downloads a PNG file via #{url}" do
1339
- image_filename = 'grape.png'
1340
- post url, file: Rack::Test::UploadedFile.new(image_filename, 'image/png', true)
1341
- expect(last_response.status).to eq(201)
1342
- expect(last_response.headers['Content-Type']).to eq('image/png')
1343
- expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png")
1344
- File.open(image_filename, 'rb') do |io|
1345
- expect(last_response.body).to eq io.read
1346
- end
1347
- end
1348
- end
1349
-
1350
- it 'uploads and downloads a Ruby file' do
1351
- filename = __FILE__
1352
- post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, 'application/x-ruby', true)
1353
- expect(last_response.status).to eq(201)
1354
- expect(last_response.headers['Content-Type']).to eq('application/x-ruby')
1355
- expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb")
1356
- File.open(filename, 'rb') do |io|
1357
- expect(last_response.body).to eq io.read
1358
- end
1359
- end
1360
- end
1361
- end
1362
-
1363
- context 'custom middleware' do
1364
- module ApiSpec
1365
- class PhonyMiddleware
1366
- def initialize(app, *args)
1367
- @args = args
1368
- @app = app
1369
- @block = block_given? ? true : nil
1370
- end
1371
-
1372
- def call(env)
1373
- env['phony.args'] ||= []
1374
- env['phony.args'] << @args
1375
- env['phony.block'] = true if @block
1376
- @app.call(env)
1377
- end
1378
- end
1379
- end
1380
-
1381
- describe '.middleware' do
1382
- it 'includes middleware arguments from settings' do
1383
- subject.use ApiSpec::PhonyMiddleware, 'abc', 123
1384
- expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 'abc', 123]]
1385
- end
1386
-
1387
- it 'includes all middleware from stacked settings' do
1388
- subject.use ApiSpec::PhonyMiddleware, 123
1389
- subject.use ApiSpec::PhonyMiddleware, 'abc'
1390
- subject.use ApiSpec::PhonyMiddleware, 'foo'
1391
-
1392
- expect(subject.middleware).to eql [
1393
- [:use, ApiSpec::PhonyMiddleware, 123],
1394
- [:use, ApiSpec::PhonyMiddleware, 'abc'],
1395
- [:use, ApiSpec::PhonyMiddleware, 'foo']
1396
- ]
1397
- end
1398
- end
1399
-
1400
- describe '.use' do
1401
- it 'adds middleware' do
1402
- subject.use ApiSpec::PhonyMiddleware, 123
1403
- expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123]]
1404
- end
1405
-
1406
- it 'does not show up outside the namespace' do
1407
- inner_middleware = nil
1408
- subject.use ApiSpec::PhonyMiddleware, 123
1409
- subject.namespace :awesome do
1410
- use ApiSpec::PhonyMiddleware, 'abc'
1411
- inner_middleware = middleware
1412
- end
1413
-
1414
- expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123]]
1415
- expect(inner_middleware).to eql [[:use, ApiSpec::PhonyMiddleware, 123], [:use, ApiSpec::PhonyMiddleware, 'abc']]
1416
- end
1417
-
1418
- it 'calls the middleware' do
1419
- subject.use ApiSpec::PhonyMiddleware, 'hello'
1420
- subject.get '/' do
1421
- env['phony.args'].first.first
1422
- end
1423
-
1424
- get '/'
1425
- expect(last_response.body).to eql 'hello'
1426
- end
1427
-
1428
- it 'adds a block if one is given' do
1429
- block = -> {}
1430
- subject.use ApiSpec::PhonyMiddleware, &block
1431
- expect(subject.middleware).to eql [[:use, ApiSpec::PhonyMiddleware, block]]
1432
- end
1433
-
1434
- it 'uses a block if one is given' do
1435
- block = -> {}
1436
- subject.use ApiSpec::PhonyMiddleware, &block
1437
- subject.get '/' do
1438
- env['phony.block'].inspect
1439
- end
1440
-
1441
- get '/'
1442
- expect(last_response.body).to eq('true')
1443
- end
1444
-
1445
- it 'does not destroy the middleware settings on multiple runs' do
1446
- block = -> {}
1447
- subject.use ApiSpec::PhonyMiddleware, &block
1448
- subject.get '/' do
1449
- env['phony.block'].inspect
1450
- end
1451
-
1452
- 2.times do
1453
- get '/'
1454
- expect(last_response.body).to eq('true')
1455
- end
1456
- end
1457
-
1458
- it 'mounts behind error middleware' do
1459
- m = Class.new(Grape::Middleware::Base) do
1460
- def before
1461
- throw :error, message: 'Caught in the Net', status: 400
1462
- end
1463
- end
1464
- subject.use m
1465
- subject.get '/' do
1466
- end
1467
- get '/'
1468
- expect(last_response.status).to eq(400)
1469
- expect(last_response.body).to eq('Caught in the Net')
1470
- end
1471
- end
1472
-
1473
- describe '.insert_before' do
1474
- it 'runs before a given middleware' do
1475
- m = Class.new(Grape::Middleware::Base) do
1476
- def call(env)
1477
- env['phony.args'] ||= []
1478
- env['phony.args'] << @options[:message]
1479
- @app.call(env)
1480
- end
1481
- end
1482
-
1483
- subject.use ApiSpec::PhonyMiddleware, 'hello'
1484
- subject.insert_before ApiSpec::PhonyMiddleware, m, message: 'bye'
1485
- subject.get '/' do
1486
- env['phony.args'].join(' ')
1487
- end
1488
-
1489
- get '/'
1490
- expect(last_response.body).to eql 'bye hello'
1491
- end
1492
- end
1493
-
1494
- describe '.insert_after' do
1495
- it 'runs after a given middleware' do
1496
- m = Class.new(Grape::Middleware::Base) do
1497
- def call(env)
1498
- env['phony.args'] ||= []
1499
- env['phony.args'] << @options[:message]
1500
- @app.call(env)
1501
- end
1502
- end
1503
-
1504
- subject.use ApiSpec::PhonyMiddleware, 'hello'
1505
- subject.insert_after ApiSpec::PhonyMiddleware, m, message: 'bye'
1506
- subject.get '/' do
1507
- env['phony.args'].join(' ')
1508
- end
1509
-
1510
- get '/'
1511
- expect(last_response.body).to eql 'hello bye'
1512
- end
1513
- end
1514
- end
1515
-
1516
- describe '.insert' do
1517
- it 'inserts middleware in a specific location in the stack' do
1518
- m = Class.new(Grape::Middleware::Base) do
1519
- def call(env)
1520
- env['phony.args'] ||= []
1521
- env['phony.args'] << @options[:message]
1522
- @app.call(env)
1523
- end
1524
- end
1525
-
1526
- subject.use ApiSpec::PhonyMiddleware, 'bye'
1527
- subject.insert 0, m, message: 'good'
1528
- subject.insert 0, m, message: 'hello'
1529
- subject.get '/' do
1530
- env['phony.args'].join(' ')
1531
- end
1532
-
1533
- get '/'
1534
- expect(last_response.body).to eql 'hello good bye'
1535
- end
1536
- end
1537
-
1538
- describe '.http_basic' do
1539
- it 'protects any resources on the same scope' do
1540
- subject.http_basic do |u, _p|
1541
- u == 'allow'
1542
- end
1543
- subject.get(:hello) { 'Hello, world.' }
1544
- get '/hello'
1545
- expect(last_response.status).to be 401
1546
- get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
1547
- expect(last_response.status).to be 200
1548
- end
1549
-
1550
- it 'is scopable' do
1551
- subject.get(:hello) { 'Hello, world.' }
1552
- subject.namespace :admin do
1553
- http_basic do |u, _p|
1554
- u == 'allow'
1555
- end
1556
-
1557
- get(:hello) { 'Hello, world.' }
1558
- end
1559
-
1560
- get '/hello'
1561
- expect(last_response.status).to be 200
1562
- get '/admin/hello'
1563
- expect(last_response.status).to be 401
1564
- end
1565
-
1566
- it 'is callable via .auth as well' do
1567
- subject.auth :http_basic do |u, _p|
1568
- u == 'allow'
1569
- end
1570
-
1571
- subject.get(:hello) { 'Hello, world.' }
1572
- get '/hello'
1573
- expect(last_response.status).to be 401
1574
- get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
1575
- expect(last_response.status).to be 200
1576
- end
1577
-
1578
- it 'has access to the current endpoint' do
1579
- basic_auth_context = nil
1580
-
1581
- subject.http_basic do |u, _p|
1582
- basic_auth_context = self
1583
-
1584
- u == 'allow'
1585
- end
1586
-
1587
- subject.get(:hello) { 'Hello, world.' }
1588
- get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
1589
- expect(basic_auth_context).to be_a(Grape::Endpoint)
1590
- end
1591
-
1592
- it 'has access to helper methods' do
1593
- subject.helpers do
1594
- def authorize(u, p)
1595
- u == 'allow' && p == 'whatever'
1596
- end
1597
- end
1598
-
1599
- subject.http_basic do |u, p|
1600
- authorize(u, p)
1601
- end
1602
-
1603
- subject.get(:hello) { 'Hello, world.' }
1604
- get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
1605
- expect(last_response.status).to be 200
1606
- get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('disallow', 'whatever')
1607
- expect(last_response.status).to be 401
1608
- end
1609
-
1610
- it 'can set instance variables accessible to routes' do
1611
- subject.http_basic do |u, _p|
1612
- @hello = 'Hello, world.'
1613
-
1614
- u == 'allow'
1615
- end
1616
-
1617
- subject.get(:hello) { @hello }
1618
- get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
1619
- expect(last_response.status).to be 200
1620
- expect(last_response.body).to eql 'Hello, world.'
1621
- end
1622
- end
1623
-
1624
- describe '.logger' do
1625
- it 'returns an instance of Logger class by default' do
1626
- expect(subject.logger.class).to eql Logger
1627
- end
1628
-
1629
- context 'with a custom logger' do
1630
- subject do
1631
- Class.new(described_class) do
1632
- def self.io
1633
- @io ||= StringIO.new
1634
- end
1635
- logger ::Logger.new(io)
1636
- end
1637
- end
1638
-
1639
- it 'exposes its interaface' do
1640
- message = 'this will be logged'
1641
- subject.logger.info message
1642
- expect(subject.io.string).to include(message)
1643
- end
1644
- end
1645
-
1646
- it 'does not unnecessarily retain duplicate setup blocks' do
1647
- subject.logger
1648
- expect { subject.logger }.not_to change(subject.instance_variable_get(:@setup), :size)
1649
- end
1650
- end
1651
-
1652
- describe '.helpers' do
1653
- it 'is accessible from the endpoint' do
1654
- subject.helpers do
1655
- def hello
1656
- 'Hello, world.'
1657
- end
1658
- end
1659
-
1660
- subject.get '/howdy' do
1661
- hello
1662
- end
1663
-
1664
- get '/howdy'
1665
- expect(last_response.body).to eql 'Hello, world.'
1666
- end
1667
-
1668
- it 'is scopable' do
1669
- subject.helpers do
1670
- def generic
1671
- 'always there'
1672
- end
1673
- end
1674
-
1675
- subject.namespace :admin do
1676
- helpers do
1677
- def secret
1678
- 'only in admin'
1679
- end
1680
- end
1681
-
1682
- get '/secret' do
1683
- [generic, secret].join ':'
1684
- end
1685
- end
1686
-
1687
- subject.get '/generic' do
1688
- [generic, respond_to?(:secret)].join ':'
1689
- end
1690
-
1691
- get '/generic'
1692
- expect(last_response.body).to eql 'always there:false'
1693
- get '/admin/secret'
1694
- expect(last_response.body).to eql 'always there:only in admin'
1695
- end
1696
-
1697
- it 'is reopenable' do
1698
- subject.helpers do
1699
- def one
1700
- 1
1701
- end
1702
- end
1703
-
1704
- subject.helpers do
1705
- def two
1706
- 2
1707
- end
1708
- end
1709
-
1710
- subject.get 'howdy' do
1711
- [one, two]
1712
- end
1713
-
1714
- expect { get '/howdy' }.not_to raise_error
1715
- end
1716
-
1717
- it 'allows for modules' do
1718
- mod = Module.new do
1719
- def hello
1720
- 'Hello, world.'
1721
- end
1722
- end
1723
- subject.helpers mod
1724
-
1725
- subject.get '/howdy' do
1726
- hello
1727
- end
1728
-
1729
- get '/howdy'
1730
- expect(last_response.body).to eql 'Hello, world.'
1731
- end
1732
-
1733
- it 'allows multiple calls with modules and blocks' do
1734
- subject.helpers Module.new do
1735
- def one
1736
- 1
1737
- end
1738
- end
1739
- subject.helpers Module.new do
1740
- def two
1741
- 2
1742
- end
1743
- end
1744
- subject.helpers do
1745
- def three
1746
- 3
1747
- end
1748
- end
1749
- subject.get 'howdy' do
1750
- [one, two, three]
1751
- end
1752
- expect { get '/howdy' }.not_to raise_error
1753
- end
1754
- end
1755
-
1756
- describe '.scope' do
1757
- # TODO: refactor this to not be tied to versioning. How about a generic
1758
- # .setting macro?
1759
- it 'scopes the various settings' do
1760
- subject.prefix 'new'
1761
-
1762
- subject.scope :legacy do
1763
- prefix 'legacy'
1764
- get '/abc' do
1765
- 'abc'
1766
- end
1767
- end
1768
-
1769
- subject.get '/def' do
1770
- 'def'
1771
- end
1772
-
1773
- get '/new/abc'
1774
- expect(last_response.status).to be 404
1775
- get '/legacy/abc'
1776
- expect(last_response.status).to be 200
1777
- get '/legacy/def'
1778
- expect(last_response.status).to be 404
1779
- get '/new/def'
1780
- expect(last_response.status).to be 200
1781
- end
1782
- end
1783
-
1784
- describe 'lifecycle' do
1785
- let!(:lifecycle) { [] }
1786
- let!(:standard_cycle) do
1787
- %i[before before_validation after_validation api_call after finally]
1788
- end
1789
-
1790
- let!(:validation_error) do
1791
- %i[before before_validation finally]
1792
- end
1793
-
1794
- let!(:errored_cycle) do
1795
- %i[before before_validation after_validation api_call finally]
1796
- end
1797
-
1798
- before do
1799
- current_cycle = lifecycle
1800
-
1801
- subject.before do
1802
- current_cycle << :before
1803
- end
1804
-
1805
- subject.before_validation do
1806
- current_cycle << :before_validation
1807
- end
1808
-
1809
- subject.after_validation do
1810
- current_cycle << :after_validation
1811
- end
1812
-
1813
- subject.after do
1814
- current_cycle << :after
1815
- end
1816
-
1817
- subject.finally do
1818
- current_cycle << :finally
1819
- end
1820
- end
1821
-
1822
- context 'when the api_call succeeds' do
1823
- before do
1824
- current_cycle = lifecycle
1825
-
1826
- subject.get 'api_call' do
1827
- current_cycle << :api_call
1828
- end
1829
- end
1830
-
1831
- it 'follows the standard life_cycle' do
1832
- get '/api_call'
1833
- expect(lifecycle).to eq standard_cycle
1834
- end
1835
- end
1836
-
1837
- context 'when the api_call has a controlled error' do
1838
- before do
1839
- current_cycle = lifecycle
1840
-
1841
- subject.get 'api_call' do
1842
- current_cycle << :api_call
1843
- error!(:some_error)
1844
- end
1845
- end
1846
-
1847
- it 'follows the errored life_cycle (skips after)' do
1848
- get '/api_call'
1849
- expect(lifecycle).to eq errored_cycle
1850
- end
1851
- end
1852
-
1853
- context 'when the api_call has an exception' do
1854
- before do
1855
- current_cycle = lifecycle
1856
-
1857
- subject.get 'api_call' do
1858
- current_cycle << :api_call
1859
- raise StandardError
1860
- end
1861
- end
1862
-
1863
- it 'follows the errored life_cycle (skips after)' do
1864
- expect { get '/api_call' }.to raise_error(StandardError)
1865
- expect(lifecycle).to eq errored_cycle
1866
- end
1867
- end
1868
-
1869
- context 'when the api_call fails validation' do
1870
- before do
1871
- current_cycle = lifecycle
1872
-
1873
- subject.params do
1874
- requires :some_param, type: String
1875
- end
1876
-
1877
- subject.get 'api_call' do
1878
- current_cycle << :api_call
1879
- end
1880
- end
1881
-
1882
- it 'follows the failed_validation cycle (skips after_validation, api_call & after)' do
1883
- get '/api_call'
1884
- expect(lifecycle).to eq validation_error
1885
- end
1886
- end
1887
- end
1888
-
1889
- describe '.finally' do
1890
- let!(:code) { { has_executed: false } }
1891
- let(:block_to_run) do
1892
- code_to_execute = code
1893
- proc do
1894
- code_to_execute[:has_executed] = true
1895
- end
1896
- end
1897
-
1898
- context 'when the ensure block has no exceptions' do
1899
- before { subject.finally(&block_to_run) }
1900
-
1901
- context 'when no API call is made' do
1902
- it 'has not executed the ensure code' do
1903
- expect(code[:has_executed]).to be false
1904
- end
1905
- end
1906
-
1907
- context 'when no errors occurs' do
1908
- before do
1909
- subject.get '/no_exceptions' do
1910
- 'success'
1911
- end
1912
- end
1913
-
1914
- it 'executes the ensure code' do
1915
- get '/no_exceptions'
1916
- expect(last_response.body).to eq 'success'
1917
- expect(code[:has_executed]).to be true
1918
- end
1919
-
1920
- context 'with a helper' do
1921
- let(:block_to_run) do
1922
- code_to_execute = code
1923
- proc do
1924
- code_to_execute[:value] = some_helper
1925
- end
1926
- end
1927
-
1928
- before do
1929
- subject.helpers do
1930
- def some_helper
1931
- 'some_value'
1932
- end
1933
- end
1934
-
1935
- subject.get '/with_helpers' do
1936
- 'success'
1937
- end
1938
- end
1939
-
1940
- it 'has access to the helper' do
1941
- get '/with_helpers'
1942
- expect(code[:value]).to eq 'some_value'
1943
- end
1944
- end
1945
- end
1946
-
1947
- context 'when an unhandled occurs inside the API call' do
1948
- before do
1949
- subject.get '/unhandled_exception' do
1950
- raise StandardError
1951
- end
1952
- end
1953
-
1954
- it 'executes the ensure code' do
1955
- expect { get '/unhandled_exception' }.to raise_error StandardError
1956
- expect(code[:has_executed]).to be true
1957
- end
1958
- end
1959
-
1960
- context 'when a handled error occurs inside the API call' do
1961
- before do
1962
- subject.rescue_from(StandardError) { error! 'handled' }
1963
- subject.get '/handled_exception' do
1964
- raise StandardError
1965
- end
1966
- end
1967
-
1968
- it 'executes the ensure code' do
1969
- get '/handled_exception'
1970
- expect(code[:has_executed]).to be true
1971
- expect(last_response.body).to eq 'handled'
1972
- end
1973
- end
1974
- end
1975
- end
1976
-
1977
- describe '.rescue_from' do
1978
- it 'does not rescue errors when rescue_from is not set' do
1979
- subject.get '/exception' do
1980
- raise 'rain!'
1981
- end
1982
- expect { get '/exception' }.to raise_error(RuntimeError, 'rain!')
1983
- end
1984
-
1985
- it 'uses custom helpers defined by using #helpers method' do
1986
- subject.helpers do
1987
- def custom_error!(name)
1988
- error! "hello #{name}"
1989
- end
1990
- end
1991
- subject.rescue_from(ArgumentError) { custom_error! :bob }
1992
- subject.get '/custom_error' do
1993
- raise ArgumentError
1994
- end
1995
- get '/custom_error'
1996
- expect(last_response.body).to eq 'hello bob'
1997
- end
1998
-
1999
- context 'with multiple apis' do
2000
- let(:a) { Class.new(described_class) }
2001
- let(:b) { Class.new(described_class) }
2002
-
2003
- before do
2004
- a.helpers do
2005
- def foo
2006
- error!('foo', 401)
2007
- end
2008
- end
2009
- a.rescue_from(:all) { foo }
2010
- a.get { raise 'boo' }
2011
- b.helpers do
2012
- def foo
2013
- error!('bar', 401)
2014
- end
2015
- end
2016
- b.rescue_from(:all) { foo }
2017
- b.get { raise 'boo' }
2018
- end
2019
-
2020
- it 'avoids polluting global namespace' do
2021
- env = Rack::MockRequest.env_for('/')
2022
-
2023
- expect(read_chunks(a.call(env)[2])).to eq(['foo'])
2024
- expect(read_chunks(b.call(env)[2])).to eq(['bar'])
2025
- expect(read_chunks(a.call(env)[2])).to eq(['foo'])
2026
- end
2027
- end
2028
-
2029
- it 'rescues all errors if rescue_from :all is called' do
2030
- subject.rescue_from :all
2031
- subject.get '/exception' do
2032
- raise 'rain!'
2033
- end
2034
- get '/exception'
2035
- expect(last_response.status).to be 500
2036
- expect(last_response.body).to eq 'rain!'
2037
- end
2038
-
2039
- it 'rescues all errors with a json formatter' do
2040
- subject.format :json
2041
- subject.default_format :json
2042
- subject.rescue_from :all
2043
- subject.get '/exception' do
2044
- raise 'rain!'
2045
- end
2046
- get '/exception'
2047
- expect(last_response.status).to be 500
2048
- expect(last_response.body).to eq({ error: 'rain!' }.to_json)
2049
- end
2050
-
2051
- it 'rescues only certain errors if rescue_from is called with specific errors' do
2052
- subject.rescue_from ArgumentError
2053
- subject.get('/rescued') { raise ArgumentError }
2054
- subject.get('/unrescued') { raise 'beefcake' }
2055
-
2056
- get '/rescued'
2057
- expect(last_response.status).to be 500
2058
-
2059
- expect { get '/unrescued' }.to raise_error(RuntimeError, 'beefcake')
2060
- end
2061
-
2062
- it 'mimics default ruby "rescue" handler' do
2063
- # The exception is matched to the rescue starting at the top, and matches only once
2064
-
2065
- subject.rescue_from ArgumentError do |e|
2066
- error!(e, 402)
2067
- end
2068
- subject.rescue_from StandardError do |e|
2069
- error!(e, 401)
2070
- end
2071
-
2072
- subject.get('/child_of_standard_error') { raise ArgumentError }
2073
- subject.get('/standard_error') { raise StandardError }
2074
-
2075
- get '/child_of_standard_error'
2076
- expect(last_response.status).to be 402
2077
-
2078
- get '/standard_error'
2079
- expect(last_response.status).to be 401
2080
- end
2081
-
2082
- context 'CustomError subclass of Grape::Exceptions::Base' do
2083
- before do
2084
- module ApiSpec
2085
- class CustomError < Grape::Exceptions::Base; end
2086
- end
2087
- end
2088
-
2089
- it 'does not re-raise exceptions of type Grape::Exceptions::Base' do
2090
- subject.get('/custom_exception') { raise ApiSpec::CustomError }
2091
-
2092
- expect { get '/custom_exception' }.not_to raise_error
2093
- end
2094
-
2095
- it 'rescues custom grape exceptions' do
2096
- subject.rescue_from ApiSpec::CustomError do |e|
2097
- rack_response('New Error', e.status)
2098
- end
2099
- subject.get '/custom_error' do
2100
- raise ApiSpec::CustomError.new(status: 400, message: 'Custom Error')
2101
- end
2102
-
2103
- get '/custom_error'
2104
- expect(last_response.status).to eq(400)
2105
- expect(last_response.body).to eq('New Error')
2106
- end
2107
- end
2108
-
2109
- it 'can rescue exceptions raised in the formatter' do
2110
- formatter = double(:formatter)
2111
- allow(formatter).to receive(:call) { raise StandardError }
2112
- allow(Grape::Formatter).to receive(:formatter_for) { formatter }
2113
-
2114
- subject.rescue_from :all do |_e|
2115
- rack_response('Formatter Error', 500)
2116
- end
2117
- subject.get('/formatter_exception') { 'Hello world' }
2118
-
2119
- get '/formatter_exception'
2120
- expect(last_response.status).to be 500
2121
- expect(last_response.body).to eq('Formatter Error')
2122
- end
2123
-
2124
- it 'uses default_rescue_handler to handle invalid response from rescue_from' do
2125
- subject.rescue_from(:all) { 'error' }
2126
- subject.get('/') { raise }
2127
-
2128
- expect_any_instance_of(Grape::Middleware::Error).to receive(:default_rescue_handler).and_call_original
2129
- get '/'
2130
- expect(last_response.status).to be 500
2131
- expect(last_response.body).to eql 'Invalid response'
2132
- end
2133
- end
2134
-
2135
- describe '.rescue_from klass, block' do
2136
- it 'rescues Exception' do
2137
- subject.rescue_from RuntimeError do |e|
2138
- rack_response("rescued from #{e.message}", 202)
2139
- end
2140
- subject.get '/exception' do
2141
- raise 'rain!'
2142
- end
2143
- get '/exception'
2144
- expect(last_response.status).to be 202
2145
- expect(last_response.body).to eq('rescued from rain!')
2146
- end
2147
-
2148
- context 'custom errors' do
2149
- before do
2150
- class ConnectionError < RuntimeError; end
2151
-
2152
- class DatabaseError < RuntimeError; end
2153
-
2154
- class CommunicationError < StandardError; end
2155
- end
2156
-
2157
- it 'rescues an error via rescue_from :all' do
2158
- subject.rescue_from :all do |e|
2159
- rack_response("rescued from #{e.class.name}", 500)
2160
- end
2161
- subject.get '/exception' do
2162
- raise ConnectionError
2163
- end
2164
- get '/exception'
2165
- expect(last_response.status).to be 500
2166
- expect(last_response.body).to eq('rescued from ConnectionError')
2167
- end
2168
-
2169
- it 'rescues a specific error' do
2170
- subject.rescue_from ConnectionError do |e|
2171
- rack_response("rescued from #{e.class.name}", 500)
2172
- end
2173
- subject.get '/exception' do
2174
- raise ConnectionError
2175
- end
2176
- get '/exception'
2177
- expect(last_response.status).to be 500
2178
- expect(last_response.body).to eq('rescued from ConnectionError')
2179
- end
2180
-
2181
- it 'rescues a subclass of an error by default' do
2182
- subject.rescue_from RuntimeError do |e|
2183
- rack_response("rescued from #{e.class.name}", 500)
2184
- end
2185
- subject.get '/exception' do
2186
- raise ConnectionError
2187
- end
2188
- get '/exception'
2189
- expect(last_response.status).to be 500
2190
- expect(last_response.body).to eq('rescued from ConnectionError')
2191
- end
2192
-
2193
- it 'rescues multiple specific errors' do
2194
- subject.rescue_from ConnectionError do |e|
2195
- rack_response("rescued from #{e.class.name}", 500)
2196
- end
2197
- subject.rescue_from DatabaseError do |e|
2198
- rack_response("rescued from #{e.class.name}", 500)
2199
- end
2200
- subject.get '/connection' do
2201
- raise ConnectionError
2202
- end
2203
- subject.get '/database' do
2204
- raise DatabaseError
2205
- end
2206
- get '/connection'
2207
- expect(last_response.status).to be 500
2208
- expect(last_response.body).to eq('rescued from ConnectionError')
2209
- get '/database'
2210
- expect(last_response.status).to be 500
2211
- expect(last_response.body).to eq('rescued from DatabaseError')
2212
- end
2213
-
2214
- it 'does not rescue a different error' do
2215
- subject.rescue_from RuntimeError do |e|
2216
- rack_response("rescued from #{e.class.name}", 500)
2217
- end
2218
- subject.get '/uncaught' do
2219
- raise CommunicationError
2220
- end
2221
- expect { get '/uncaught' }.to raise_error(CommunicationError)
2222
- end
2223
- end
2224
- end
2225
-
2226
- describe '.rescue_from klass, lambda' do
2227
- it 'rescues an error with the lambda' do
2228
- subject.rescue_from ArgumentError, lambda {
2229
- rack_response('rescued with a lambda', 400)
2230
- }
2231
- subject.get('/rescue_lambda') { raise ArgumentError }
2232
-
2233
- get '/rescue_lambda'
2234
- expect(last_response.status).to eq(400)
2235
- expect(last_response.body).to eq('rescued with a lambda')
2236
- end
2237
-
2238
- it 'can execute the lambda with an argument' do
2239
- subject.rescue_from ArgumentError, lambda { |e|
2240
- rack_response(e.message, 400)
2241
- }
2242
- subject.get('/rescue_lambda') { raise ArgumentError, 'lambda takes an argument' }
2243
-
2244
- get '/rescue_lambda'
2245
- expect(last_response.status).to eq(400)
2246
- expect(last_response.body).to eq('lambda takes an argument')
2247
- end
2248
- end
2249
-
2250
- describe '.rescue_from klass, with: :method_name' do
2251
- it 'rescues an error with the specified method name' do
2252
- subject.helpers do
2253
- def rescue_arg_error
2254
- error!('500 ArgumentError', 500)
2255
- end
2256
-
2257
- def rescue_no_method_error
2258
- error!('500 NoMethodError', 500)
2259
- end
2260
- end
2261
- subject.rescue_from ArgumentError, with: :rescue_arg_error
2262
- subject.rescue_from NoMethodError, with: :rescue_no_method_error
2263
- subject.get('/rescue_arg_error') { raise ArgumentError }
2264
- subject.get('/rescue_no_method_error') { raise NoMethodError }
2265
-
2266
- get '/rescue_arg_error'
2267
- expect(last_response.status).to eq(500)
2268
- expect(last_response.body).to eq('500 ArgumentError')
2269
-
2270
- get '/rescue_no_method_error'
2271
- expect(last_response.status).to eq(500)
2272
- expect(last_response.body).to eq('500 NoMethodError')
2273
- end
2274
-
2275
- it 'aborts if the specified method name does not exist' do
2276
- subject.rescue_from :all, with: :not_exist_method
2277
- subject.get('/rescue_method') { raise StandardError }
2278
-
2279
- expect { get '/rescue_method' }.to raise_error(NoMethodError, /^undefined method 'not_exist_method'/)
2280
- end
2281
-
2282
- it 'correctly chooses exception handler if :all handler is specified' do
2283
- subject.helpers do
2284
- def rescue_arg_error
2285
- error!('500 ArgumentError', 500)
2286
- end
2287
-
2288
- def rescue_all_errors
2289
- error!('500 AnotherError', 500)
2290
- end
2291
- end
2292
-
2293
- subject.rescue_from ArgumentError, with: :rescue_arg_error
2294
- subject.rescue_from :all, with: :rescue_all_errors
2295
- subject.get('/argument_error') { raise ArgumentError }
2296
- subject.get('/another_error') { raise NoMethodError }
2297
-
2298
- get '/argument_error'
2299
- expect(last_response.status).to eq(500)
2300
- expect(last_response.body).to eq('500 ArgumentError')
2301
-
2302
- get '/another_error'
2303
- expect(last_response.status).to eq(500)
2304
- expect(last_response.body).to eq('500 AnotherError')
2305
- end
2306
- end
2307
-
2308
- describe '.rescue_from klass, rescue_subclasses: boolean' do
2309
- before do
2310
- module ApiSpec
2311
- module APIErrors
2312
- class ParentError < StandardError; end
2313
-
2314
- class ChildError < ParentError; end
2315
- end
2316
- end
2317
- end
2318
-
2319
- it 'rescues error as well as subclass errors with rescue_subclasses option set' do
2320
- subject.rescue_from ApiSpec::APIErrors::ParentError, rescue_subclasses: true do |e|
2321
- rack_response("rescued from #{e.class.name}", 500)
2322
- end
2323
- subject.get '/caught_child' do
2324
- raise ApiSpec::APIErrors::ChildError
2325
- end
2326
- subject.get '/caught_parent' do
2327
- raise ApiSpec::APIErrors::ParentError
2328
- end
2329
- subject.get '/uncaught_parent' do
2330
- raise StandardError
2331
- end
2332
-
2333
- get '/caught_child'
2334
- expect(last_response.status).to be 500
2335
- get '/caught_parent'
2336
- expect(last_response.status).to be 500
2337
- expect { get '/uncaught_parent' }.to raise_error(StandardError)
2338
- end
2339
-
2340
- it 'sets rescue_subclasses to true by default' do
2341
- subject.rescue_from ApiSpec::APIErrors::ParentError do |e|
2342
- rack_response("rescued from #{e.class.name}", 500)
2343
- end
2344
- subject.get '/caught_child' do
2345
- raise ApiSpec::APIErrors::ChildError
2346
- end
2347
-
2348
- get '/caught_child'
2349
- expect(last_response.status).to be 500
2350
- end
2351
-
2352
- it 'does not rescue child errors if rescue_subclasses is false' do
2353
- subject.rescue_from ApiSpec::APIErrors::ParentError, rescue_subclasses: false do |e|
2354
- rack_response("rescued from #{e.class.name}", 500)
2355
- end
2356
- subject.get '/uncaught' do
2357
- raise ApiSpec::APIErrors::ChildError
2358
- end
2359
- expect { get '/uncaught' }.to raise_error(ApiSpec::APIErrors::ChildError)
2360
- end
2361
- end
2362
-
2363
- describe '.rescue_from :grape_exceptions' do
2364
- before do
2365
- subject.rescue_from :grape_exceptions
2366
- end
2367
-
2368
- let(:grape_exception) do
2369
- Grape::Exceptions::Base.new(status: 400, message: 'Grape Error')
2370
- end
2371
-
2372
- it 'rescues grape exceptions' do
2373
- exception = grape_exception
2374
- subject.get('/grape_exception') { raise exception }
2375
-
2376
- get '/grape_exception'
2377
-
2378
- expect(last_response.status).to eq(exception.status)
2379
- expect(last_response.body).to eq(exception.message)
2380
- end
2381
-
2382
- it 'rescues grape exceptions with a user-defined handler' do
2383
- subject.rescue_from grape_exception.class do |_error|
2384
- rack_response('Redefined Error', 403)
2385
- end
2386
-
2387
- exception = grape_exception
2388
- subject.get('/grape_exception') { raise exception }
2389
-
2390
- get '/grape_exception'
2391
-
2392
- expect(last_response.status).to eq(403)
2393
- expect(last_response.body).to eq('Redefined Error')
2394
- end
2395
- end
2396
-
2397
- describe '.error_format' do
2398
- it 'rescues all errors and return :txt' do
2399
- subject.rescue_from :all
2400
- subject.format :txt
2401
- subject.get '/exception' do
2402
- raise 'rain!'
2403
- end
2404
- get '/exception'
2405
- expect(last_response.body).to eql 'rain!'
2406
- end
2407
-
2408
- it 'rescues all errors and return :txt with backtrace' do
2409
- subject.rescue_from :all, backtrace: true
2410
- subject.format :txt
2411
- subject.get '/exception' do
2412
- raise 'rain!'
2413
- end
2414
- get '/exception'
2415
- expect(last_response.body.start_with?("rain!\r\n")).to be true
2416
- end
2417
-
2418
- it 'rescues all errors with a default formatter' do
2419
- subject.default_format :foo
2420
- subject.content_type :foo, 'text/foo'
2421
- subject.rescue_from :all
2422
- subject.get '/exception' do
2423
- raise 'rain!'
2424
- end
2425
- get '/exception.foo'
2426
- expect(last_response.body).to start_with 'rain!'
2427
- end
2428
-
2429
- it 'defaults the error formatter to format' do
2430
- subject.format :json
2431
- subject.rescue_from :all
2432
- subject.content_type :json, 'application/json'
2433
- subject.content_type :foo, 'text/foo'
2434
- subject.get '/exception' do
2435
- raise 'rain!'
2436
- end
2437
- get '/exception.json'
2438
- expect(last_response.body).to eq('{"error":"rain!"}')
2439
- get '/exception.foo'
2440
- expect(last_response.body).to eq('{"error":"rain!"}')
2441
- end
2442
-
2443
- context 'class' do
2444
- before do
2445
- module ApiSpec
2446
- class CustomErrorFormatter
2447
- def self.call(message, _backtrace, _options, _env, _original_exception)
2448
- "message: #{message} @backtrace"
2449
- end
2450
- end
2451
- end
2452
- end
2453
-
2454
- it 'returns a custom error format' do
2455
- subject.rescue_from :all, backtrace: true
2456
- subject.error_formatter :txt, ApiSpec::CustomErrorFormatter
2457
- subject.get '/exception' do
2458
- raise 'rain!'
2459
- end
2460
- get '/exception'
2461
- expect(last_response.body).to eq('message: rain! @backtrace')
2462
- end
2463
- end
2464
-
2465
- describe 'with' do
2466
- context 'class' do
2467
- before do
2468
- module ApiSpec
2469
- class CustomErrorFormatter
2470
- def self.call(message, _backtrace, _option, _env, _original_exception)
2471
- "message: #{message} @backtrace"
2472
- end
2473
- end
2474
- end
2475
- end
2476
-
2477
- it 'returns a custom error format' do
2478
- subject.rescue_from :all, backtrace: true
2479
- subject.error_formatter :txt, with: ApiSpec::CustomErrorFormatter
2480
- subject.get('/exception') { raise 'rain!' }
2481
-
2482
- get '/exception'
2483
- expect(last_response.body).to eq('message: rain! @backtrace')
2484
- end
2485
- end
2486
- end
2487
-
2488
- it 'rescues all errors and return :json' do
2489
- subject.rescue_from :all
2490
- subject.format :json
2491
- subject.get '/exception' do
2492
- raise 'rain!'
2493
- end
2494
- get '/exception'
2495
- expect(last_response.body).to eql '{"error":"rain!"}'
2496
- end
2497
-
2498
- it 'rescues all errors and return :json with backtrace' do
2499
- subject.rescue_from :all, backtrace: true
2500
- subject.format :json
2501
- subject.get '/exception' do
2502
- raise 'rain!'
2503
- end
2504
- get '/exception'
2505
- json = ::Grape::Json.load(last_response.body)
2506
- expect(json['error']).to eql 'rain!'
2507
- expect(json['backtrace'].length).to be > 0
2508
- end
2509
-
2510
- it 'rescues error! and return txt' do
2511
- subject.format :txt
2512
- subject.get '/error' do
2513
- error!('Access Denied', 401)
2514
- end
2515
- get '/error'
2516
- expect(last_response.body).to eql 'Access Denied'
2517
- end
2518
-
2519
- context 'with json format' do
2520
- before { subject.format :json }
2521
-
2522
- after do
2523
- get '/error'
2524
- expect(last_response.body).to eql('{"error":"failure"}')
2525
- end
2526
-
2527
- it 'rescues error! called with a string and returns json' do
2528
- subject.get('/error') { error!(:failure, 401) }
2529
- end
2530
-
2531
- it 'rescues error! called with a symbol and returns json' do
2532
- subject.get('/error') { error!(:failure, 401) }
2533
- end
2534
-
2535
- it 'rescues error! called with a hash and returns json' do
2536
- subject.get('/error') { error!({ error: :failure }, 401) }
2537
- end
2538
- end
2539
- end
2540
-
2541
- describe '.content_type' do
2542
- it 'sets additional content-type' do
2543
- subject.content_type :xls, 'application/vnd.ms-excel'
2544
- subject.get :excel do
2545
- 'some binary content'
2546
- end
2547
- get '/excel.xls'
2548
- expect(last_response.content_type).to eq('application/vnd.ms-excel')
2549
- end
2550
-
2551
- it 'allows to override content-type' do
2552
- subject.get :content do
2553
- content_type 'text/javascript'
2554
- 'var x = 1;'
2555
- end
2556
- get '/content'
2557
- expect(last_response.content_type).to eq('text/javascript')
2558
- end
2559
-
2560
- it 'removes existing content types' do
2561
- subject.content_type :xls, 'application/vnd.ms-excel'
2562
- subject.get :excel do
2563
- 'some binary content'
2564
- end
2565
- get '/excel.json'
2566
- expect(last_response.status).to eq(406)
2567
- if ActiveSupport::VERSION::MAJOR == 3
2568
- expect(last_response.body).to eq('The requested format &#x27;txt&#x27; is not supported.')
2569
- else
2570
- expect(last_response.body).to eq('The requested format &#39;txt&#39; is not supported.')
2571
- end
2572
- end
2573
- end
2574
-
2575
- describe '.formatter' do
2576
- context 'multiple formatters' do
2577
- before do
2578
- subject.formatter :json, ->(object, _env) { "{\"custom_formatter\":\"#{object[:some]}\"}" }
2579
- subject.formatter :txt, ->(object, _env) { "custom_formatter: #{object[:some]}" }
2580
- subject.get :simple do
2581
- { some: 'hash' }
2582
- end
2583
- end
2584
-
2585
- it 'sets one formatter' do
2586
- get '/simple.json'
2587
- expect(last_response.body).to eql '{"custom_formatter":"hash"}'
2588
- end
2589
-
2590
- it 'sets another formatter' do
2591
- get '/simple.txt'
2592
- expect(last_response.body).to eql 'custom_formatter: hash'
2593
- end
2594
- end
2595
-
2596
- context 'custom formatter' do
2597
- before do
2598
- subject.content_type :json, 'application/json'
2599
- subject.content_type :custom, 'application/custom'
2600
- subject.formatter :custom, ->(object, _env) { "{\"custom_formatter\":\"#{object[:some]}\"}" }
2601
- subject.get :simple do
2602
- { some: 'hash' }
2603
- end
2604
- end
2605
-
2606
- it 'uses json' do
2607
- get '/simple.json'
2608
- expect(last_response.body).to eql '{"some":"hash"}'
2609
- end
2610
-
2611
- it 'uses custom formatter' do
2612
- get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom'
2613
- expect(last_response.body).to eql '{"custom_formatter":"hash"}'
2614
- end
2615
- end
2616
-
2617
- context 'custom formatter class' do
2618
- module ApiSpec
2619
- module CustomFormatter
2620
- def self.call(object, _env)
2621
- "{\"custom_formatter\":\"#{object[:some]}\"}"
2622
- end
2623
- end
2624
- end
2625
- before do
2626
- subject.content_type :json, 'application/json'
2627
- subject.content_type :custom, 'application/custom'
2628
- subject.formatter :custom, ApiSpec::CustomFormatter
2629
- subject.get :simple do
2630
- { some: 'hash' }
2631
- end
2632
- end
2633
-
2634
- it 'uses json' do
2635
- get '/simple.json'
2636
- expect(last_response.body).to eql '{"some":"hash"}'
2637
- end
2638
-
2639
- it 'uses custom formatter' do
2640
- get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom'
2641
- expect(last_response.body).to eql '{"custom_formatter":"hash"}'
2642
- end
2643
- end
2644
- end
2645
-
2646
- describe '.parser' do
2647
- it 'parses data in format requested by content-type' do
2648
- subject.format :json
2649
- subject.post '/data' do
2650
- { x: params[:x] }
2651
- end
2652
- post '/data', '{"x":42}', 'CONTENT_TYPE' => 'application/json'
2653
- expect(last_response.status).to eq(201)
2654
- expect(last_response.body).to eq('{"x":42}')
2655
- end
2656
-
2657
- context 'lambda parser' do
2658
- before do
2659
- subject.content_type :txt, 'text/plain'
2660
- subject.content_type :custom, 'text/custom'
2661
- subject.parser :custom, ->(object, _env) { { object.to_sym => object.to_s.reverse } }
2662
- subject.put :simple do
2663
- params[:simple]
2664
- end
2665
- end
2666
-
2667
- ['text/custom', 'text/custom; charset=UTF-8'].each do |content_type|
2668
- it "uses parser for #{content_type}" do
2669
- put '/simple', 'simple', 'CONTENT_TYPE' => content_type
2670
- expect(last_response.status).to eq(200)
2671
- expect(last_response.body).to eql 'elpmis'
2672
- end
2673
- end
2674
- end
2675
-
2676
- context 'custom parser class' do
2677
- module ApiSpec
2678
- module CustomParser
2679
- def self.call(object, _env)
2680
- { object.to_sym => object.to_s.reverse }
2681
- end
2682
- end
2683
- end
2684
- before do
2685
- subject.content_type :txt, 'text/plain'
2686
- subject.content_type :custom, 'text/custom'
2687
- subject.parser :custom, ApiSpec::CustomParser
2688
- subject.put :simple do
2689
- params[:simple]
2690
- end
2691
- end
2692
-
2693
- it 'uses custom parser' do
2694
- put '/simple', 'simple', 'CONTENT_TYPE' => 'text/custom'
2695
- expect(last_response.status).to eq(200)
2696
- expect(last_response.body).to eql 'elpmis'
2697
- end
2698
- end
2699
-
2700
- if Object.const_defined? :MultiXml
2701
- context 'multi_xml' do
2702
- it "doesn't parse yaml" do
2703
- subject.put :yaml do
2704
- params[:tag]
2705
- end
2706
- put '/yaml', '<tag type="symbol">a123</tag>', 'CONTENT_TYPE' => 'application/xml'
2707
- expect(last_response.status).to eq(400)
2708
- expect(last_response.body).to eql 'Disallowed type attribute: "symbol"'
2709
- end
2710
- end
2711
- else
2712
- context 'default xml parser' do
2713
- it 'parses symbols' do
2714
- subject.put :yaml do
2715
- params[:tag]
2716
- end
2717
- put '/yaml', '<tag type="symbol">a123</tag>', 'CONTENT_TYPE' => 'application/xml'
2718
- expect(last_response.status).to eq(200)
2719
- expect(last_response.body).to eql '{"type"=>"symbol", "__content__"=>"a123"}'
2720
- end
2721
- end
2722
- end
2723
- context 'none parser class' do
2724
- before do
2725
- subject.parser :json, nil
2726
- subject.put 'data' do
2727
- "body: #{env['api.request.body']}"
2728
- end
2729
- end
2730
-
2731
- it 'does not parse data' do
2732
- put '/data', 'not valid json', 'CONTENT_TYPE' => 'application/json'
2733
- expect(last_response.status).to eq(200)
2734
- expect(last_response.body).to eq('body: not valid json')
2735
- end
2736
- end
2737
- end
2738
-
2739
- describe '.default_format' do
2740
- before do
2741
- subject.format :json
2742
- subject.default_format :json
2743
- end
2744
-
2745
- it 'returns data in default format' do
2746
- subject.get '/data' do
2747
- { x: 42 }
2748
- end
2749
- get '/data'
2750
- expect(last_response.status).to eq(200)
2751
- expect(last_response.body).to eq('{"x":42}')
2752
- end
2753
-
2754
- it 'parses data in default format' do
2755
- subject.post '/data' do
2756
- { x: params[:x] }
2757
- end
2758
- post '/data', '{"x":42}', 'CONTENT_TYPE' => ''
2759
- expect(last_response.status).to eq(201)
2760
- expect(last_response.body).to eq('{"x":42}')
2761
- end
2762
- end
2763
-
2764
- describe '.default_error_status' do
2765
- it 'allows setting default_error_status' do
2766
- subject.rescue_from :all
2767
- subject.default_error_status 200
2768
- subject.get '/exception' do
2769
- raise 'rain!'
2770
- end
2771
- get '/exception'
2772
- expect(last_response.status).to be 200
2773
- end
2774
-
2775
- it 'has a default error status' do
2776
- subject.rescue_from :all
2777
- subject.get '/exception' do
2778
- raise 'rain!'
2779
- end
2780
- get '/exception'
2781
- expect(last_response.status).to be 500
2782
- end
2783
-
2784
- it 'uses the default error status in error!' do
2785
- subject.rescue_from :all
2786
- subject.default_error_status 400
2787
- subject.get '/exception' do
2788
- error! 'rain!'
2789
- end
2790
- get '/exception'
2791
- expect(last_response.status).to be 400
2792
- end
2793
- end
2794
-
2795
- context 'http_codes' do
2796
- let(:error_presenter) do
2797
- Class.new(Grape::Entity) do
2798
- expose :code
2799
- expose :static
2800
-
2801
- def static
2802
- 'some static text'
2803
- end
2804
- end
2805
- end
2806
-
2807
- it 'is used as presenter' do
2808
- subject.desc 'some desc', http_codes: [
2809
- [408, 'Unauthorized', error_presenter]
2810
- ]
2811
-
2812
- subject.get '/exception' do
2813
- error!({ code: 408 }, 408)
2814
- end
2815
-
2816
- get '/exception'
2817
- expect(last_response.status).to be 408
2818
- expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json)
2819
- end
2820
-
2821
- it 'presented with' do
2822
- error = { code: 408, with: error_presenter }.freeze
2823
- subject.get '/exception' do
2824
- error! error, 408
2825
- end
2826
-
2827
- get '/exception'
2828
- expect(last_response.status).to be 408
2829
- expect(last_response.body).to eql({ code: 408, static: 'some static text' }.to_json)
2830
- end
2831
- end
2832
-
2833
- context 'routes' do
2834
- describe 'empty api structure' do
2835
- it 'returns an empty array of routes' do
2836
- expect(subject.routes).to eq([])
2837
- end
2838
- end
2839
-
2840
- describe 'single method api structure' do
2841
- before do
2842
- subject.get :ping do
2843
- 'pong'
2844
- end
2845
- end
2846
-
2847
- it 'returns one route' do
2848
- expect(subject.routes.size).to eq(1)
2849
- route = subject.routes[0]
2850
- expect(route.version).to be_nil
2851
- expect(route.path).to eq('/ping(.:format)')
2852
- expect(route.request_method).to eq('GET')
2853
- end
2854
- end
2855
-
2856
- describe 'api structure with two versions and a namespace' do
2857
- before do
2858
- subject.version 'v1', using: :path
2859
- subject.get 'version' do
2860
- api.version
2861
- end
2862
- # version v2
2863
- subject.version 'v2', using: :path
2864
- subject.prefix 'p'
2865
- subject.namespace 'n1' do
2866
- namespace 'n2' do
2867
- get 'version' do
2868
- api.version
2869
- end
2870
- end
2871
- end
2872
- end
2873
-
2874
- it 'returns the latest version set' do
2875
- expect(subject.version).to eq('v2')
2876
- end
2877
-
2878
- it 'returns versions' do
2879
- expect(subject.versions).to eq(%w[v1 v2])
2880
- end
2881
-
2882
- it 'sets route paths' do
2883
- expect(subject.routes.size).to be >= 2
2884
- expect(subject.routes[0].path).to eq('/:version/version(.:format)')
2885
- expect(subject.routes[1].path).to eq('/p/:version/n1/n2/version(.:format)')
2886
- end
2887
-
2888
- it 'sets route versions' do
2889
- expect(subject.routes[0].version).to eq('v1')
2890
- expect(subject.routes[1].version).to eq('v2')
2891
- end
2892
-
2893
- it 'sets a nested namespace' do
2894
- expect(subject.routes[1].namespace).to eq('/n1/n2')
2895
- end
2896
-
2897
- it 'sets prefix' do
2898
- expect(subject.routes[1].prefix).to eq('p')
2899
- end
2900
- end
2901
-
2902
- describe 'api structure with additional parameters' do
2903
- before do
2904
- subject.params do
2905
- requires :token, desc: 'a token'
2906
- optional :limit, desc: 'the limit'
2907
- end
2908
- subject.get 'split/:string' do
2909
- params[:string].split(params[:token], (params[:limit] || 0).to_i)
2910
- end
2911
- end
2912
-
2913
- it 'splits a string' do
2914
- get '/split/a,b,c.json', token: ','
2915
- expect(last_response.body).to eq('["a","b","c"]')
2916
- end
2917
-
2918
- it 'splits a string with limit' do
2919
- get '/split/a,b,c.json', token: ',', limit: '2'
2920
- expect(last_response.body).to eq('["a","b,c"]')
2921
- end
2922
-
2923
- it 'sets params' do
2924
- expect(subject.routes.map do |route|
2925
- { params: route.params }
2926
- end).to eq [
2927
- {
2928
- params: {
2929
- 'string' => '',
2930
- 'token' => { required: true, desc: 'a token' },
2931
- 'limit' => { required: false, desc: 'the limit' }
2932
- }
2933
- }
2934
- ]
2935
- end
2936
- end
2937
-
2938
- describe 'api structure with multiple apis' do
2939
- before do
2940
- subject.params do
2941
- requires :one, desc: 'a token'
2942
- optional :two, desc: 'the limit'
2943
- end
2944
- subject.get 'one' do
2945
- end
2946
-
2947
- subject.params do
2948
- requires :three, desc: 'a token'
2949
- optional :four, desc: 'the limit'
2950
- end
2951
- subject.get 'two' do
2952
- end
2953
- end
2954
-
2955
- it 'sets params' do
2956
- expect(subject.routes.map do |route|
2957
- { params: route.params }
2958
- end).to eq [
2959
- {
2960
- params: {
2961
- 'one' => { required: true, desc: 'a token' },
2962
- 'two' => { required: false, desc: 'the limit' }
2963
- }
2964
- },
2965
- {
2966
- params: {
2967
- 'three' => { required: true, desc: 'a token' },
2968
- 'four' => { required: false, desc: 'the limit' }
2969
- }
2970
- }
2971
- ]
2972
- end
2973
- end
2974
-
2975
- describe 'api structure with an api without params' do
2976
- before do
2977
- subject.params do
2978
- requires :one, desc: 'a token'
2979
- optional :two, desc: 'the limit'
2980
- end
2981
- subject.get 'one' do
2982
- end
2983
-
2984
- subject.get 'two' do
2985
- end
2986
- end
2987
-
2988
- it 'sets params' do
2989
- expect(subject.routes.map do |route|
2990
- { params: route.params }
2991
- end).to eq [
2992
- {
2993
- params: {
2994
- 'one' => { required: true, desc: 'a token' },
2995
- 'two' => { required: false, desc: 'the limit' }
2996
- }
2997
- },
2998
- {
2999
- params: {}
3000
- }
3001
- ]
3002
- end
3003
- end
3004
-
3005
- describe 'api with a custom route setting' do
3006
- before do
3007
- subject.route_setting :custom, key: 'value'
3008
- subject.get 'one'
3009
- end
3010
-
3011
- it 'exposed' do
3012
- expect(subject.routes.count).to eq 1
3013
- route = subject.routes.first
3014
- expect(route.settings[:custom]).to eq(key: 'value')
3015
- end
3016
- end
3017
-
3018
- describe 'status' do
3019
- it 'can be set to arbitrary Integer value' do
3020
- subject.get '/foo' do
3021
- status 210
3022
- end
3023
- get '/foo'
3024
- expect(last_response.status).to eq 210
3025
- end
3026
-
3027
- it 'can be set with a status code symbol' do
3028
- subject.get '/foo' do
3029
- status :see_other
3030
- end
3031
- get '/foo'
3032
- expect(last_response.status).to eq 303
3033
- end
3034
- end
3035
- end
3036
-
3037
- context 'desc' do
3038
- it 'empty array of routes' do
3039
- expect(subject.routes).to eq([])
3040
- end
3041
-
3042
- it 'empty array of routes' do
3043
- subject.desc 'grape api'
3044
- expect(subject.routes).to eq([])
3045
- end
3046
-
3047
- it 'describes a method' do
3048
- subject.desc 'first method'
3049
- subject.get :first
3050
- expect(subject.routes.length).to eq(1)
3051
- route = subject.routes.first
3052
- expect(route.description).to eq('first method')
3053
- expect(route.route_foo).to be_nil
3054
- expect(route.params).to eq({})
3055
- expect(route.options).to be_a(Hash)
3056
- end
3057
-
3058
- it 'has params which does not include format and version as named captures' do
3059
- subject.version :v1, using: :path
3060
- subject.get :first
3061
- param_keys = subject.routes.first.params.keys
3062
- expect(param_keys).not_to include('format')
3063
- expect(param_keys).not_to include('version')
3064
- end
3065
-
3066
- it 'describes methods separately' do
3067
- subject.desc 'first method'
3068
- subject.get :first
3069
- subject.desc 'second method'
3070
- subject.get :second
3071
- expect(subject.routes.count).to eq(2)
3072
- expect(subject.routes.map do |route|
3073
- { description: route.description, params: route.params }
3074
- end).to eq [
3075
- { description: 'first method', params: {} },
3076
- { description: 'second method', params: {} }
3077
- ]
3078
- end
3079
-
3080
- it 'resets desc' do
3081
- subject.desc 'first method'
3082
- subject.get :first
3083
- subject.get :second
3084
- expect(subject.routes.map do |route|
3085
- { description: route.description, params: route.params }
3086
- end).to eq [
3087
- { description: 'first method', params: {} },
3088
- { description: nil, params: {} }
3089
- ]
3090
- end
3091
-
3092
- it 'namespaces and describe arbitrary parameters' do
3093
- subject.namespace 'ns' do
3094
- desc 'ns second', foo: 'bar'
3095
- get 'second'
3096
- end
3097
- expect(subject.routes.map do |route|
3098
- { description: route.description, foo: route.route_foo, params: route.params }
3099
- end).to eq [
3100
- { description: 'ns second', foo: 'bar', params: {} }
3101
- ]
3102
- end
3103
-
3104
- it 'includes details' do
3105
- subject.desc 'method', details: 'method details'
3106
- subject.get 'method'
3107
- expect(subject.routes.map do |route|
3108
- { description: route.description, details: route.details, params: route.params }
3109
- end).to eq [
3110
- { description: 'method', details: 'method details', params: {} }
3111
- ]
3112
- end
3113
-
3114
- it 'describes a method with parameters' do
3115
- subject.desc 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } }
3116
- subject.get 'reverse' do
3117
- params[:s].reverse
3118
- end
3119
- expect(subject.routes.map do |route|
3120
- { description: route.description, params: route.params }
3121
- end).to eq [
3122
- { description: 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } }
3123
- ]
3124
- end
3125
-
3126
- it 'does not inherit param descriptions in consequent namespaces' do
3127
- subject.desc 'global description'
3128
- subject.params do
3129
- requires :param1
3130
- optional :param2
3131
- end
3132
- subject.namespace 'ns1' do
3133
- get { ; }
3134
- end
3135
- subject.params do
3136
- optional :param2
3137
- end
3138
- subject.namespace 'ns2' do
3139
- get { ; }
3140
- end
3141
- routes_doc = subject.routes.map do |route|
3142
- { description: route.description, params: route.params }
3143
- end
3144
- expect(routes_doc).to eq [
3145
- { description: 'global description',
3146
- params: {
3147
- 'param1' => { required: true },
3148
- 'param2' => { required: false }
3149
- } },
3150
- { description: 'global description',
3151
- params: {
3152
- 'param2' => { required: false }
3153
- } }
3154
- ]
3155
- end
3156
-
3157
- it 'merges the parameters of the namespace with the parameters of the method' do
3158
- subject.desc 'namespace'
3159
- subject.params do
3160
- requires :ns_param, desc: 'namespace parameter'
3161
- end
3162
- subject.namespace 'ns' do
3163
- desc 'method'
3164
- params do
3165
- optional :method_param, desc: 'method parameter'
3166
- end
3167
- get 'method'
3168
- end
3169
-
3170
- routes_doc = subject.routes.map do |route|
3171
- { description: route.description, params: route.params }
3172
- end
3173
- expect(routes_doc).to eq [
3174
- { description: 'method',
3175
- params: {
3176
- 'ns_param' => { required: true, desc: 'namespace parameter' },
3177
- 'method_param' => { required: false, desc: 'method parameter' }
3178
- } }
3179
- ]
3180
- end
3181
-
3182
- it 'merges the parameters of nested namespaces' do
3183
- subject.desc 'ns1'
3184
- subject.params do
3185
- optional :ns_param, desc: 'ns param 1'
3186
- requires :ns1_param, desc: 'ns1 param'
3187
- end
3188
- subject.namespace 'ns1' do
3189
- desc 'ns2'
3190
- params do
3191
- requires :ns_param, desc: 'ns param 2'
3192
- requires :ns2_param, desc: 'ns2 param'
3193
- end
3194
- namespace 'ns2' do
3195
- desc 'method'
3196
- params do
3197
- optional :method_param, desc: 'method param'
3198
- end
3199
- get 'method'
3200
- end
3201
- end
3202
- expect(subject.routes.map do |route|
3203
- { description: route.description, params: route.params }
3204
- end).to eq [
3205
- { description: 'method',
3206
- params: {
3207
- 'ns_param' => { required: true, desc: 'ns param 2' },
3208
- 'ns1_param' => { required: true, desc: 'ns1 param' },
3209
- 'ns2_param' => { required: true, desc: 'ns2 param' },
3210
- 'method_param' => { required: false, desc: 'method param' }
3211
- } }
3212
- ]
3213
- end
3214
-
3215
- it 'groups nested params and prevents overwriting of params with same name in different groups' do
3216
- subject.desc 'method'
3217
- subject.params do
3218
- group :group1, type: Array do
3219
- optional :param1, desc: 'group1 param1 desc'
3220
- requires :param2, desc: 'group1 param2 desc'
3221
- end
3222
- group :group2, type: Array do
3223
- optional :param1, desc: 'group2 param1 desc'
3224
- requires :param2, desc: 'group2 param2 desc'
3225
- end
3226
- end
3227
- subject.get 'method'
3228
-
3229
- expect(subject.routes.map(&:params)).to eq [{
3230
- 'group1' => { required: true, type: 'Array' },
3231
- 'group1[param1]' => { required: false, desc: 'group1 param1 desc' },
3232
- 'group1[param2]' => { required: true, desc: 'group1 param2 desc' },
3233
- 'group2' => { required: true, type: 'Array' },
3234
- 'group2[param1]' => { required: false, desc: 'group2 param1 desc' },
3235
- 'group2[param2]' => { required: true, desc: 'group2 param2 desc' }
3236
- }]
3237
- end
3238
-
3239
- it 'uses full name of parameters in nested groups' do
3240
- subject.desc 'nesting'
3241
- subject.params do
3242
- requires :root_param, desc: 'root param'
3243
- group :nested, type: Array do
3244
- requires :nested_param, desc: 'nested param'
3245
- end
3246
- end
3247
- subject.get 'method'
3248
- expect(subject.routes.map do |route|
3249
- { description: route.description, params: route.params }
3250
- end).to eq [
3251
- { description: 'nesting',
3252
- params: {
3253
- 'root_param' => { required: true, desc: 'root param' },
3254
- 'nested' => { required: true, type: 'Array' },
3255
- 'nested[nested_param]' => { required: true, desc: 'nested param' }
3256
- } }
3257
- ]
3258
- end
3259
-
3260
- it 'allows to set the type attribute on :group element' do
3261
- subject.params do
3262
- group :foo, type: Array do
3263
- optional :bar
3264
- end
3265
- end
3266
- end
3267
-
3268
- it 'parses parameters when no description is given' do
3269
- subject.params do
3270
- requires :one_param, desc: 'one param'
3271
- end
3272
- subject.get 'method'
3273
- expect(subject.routes.map do |route|
3274
- { description: route.description, params: route.params }
3275
- end).to eq [
3276
- { description: nil, params: { 'one_param' => { required: true, desc: 'one param' } } }
3277
- ]
3278
- end
3279
-
3280
- it 'does not symbolize params' do
3281
- subject.desc 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } }
3282
- subject.get 'reverse/:s' do
3283
- params[:s].reverse
3284
- end
3285
- expect(subject.routes.map do |route|
3286
- { description: route.description, params: route.params }
3287
- end).to eq [
3288
- { description: 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } }
3289
- ]
3290
- end
3291
- end
3292
-
3293
- describe '.mount' do
3294
- let(:mounted_app) { ->(_env) { [200, {}, ['MOUNTED']] } }
3295
-
3296
- context 'with a bare rack app' do
3297
- before do
3298
- subject.mount mounted_app => '/mounty'
3299
- end
3300
-
3301
- it 'makes a bare Rack app available at the endpoint' do
3302
- get '/mounty'
3303
- expect(last_response.body).to eq('MOUNTED')
3304
- end
3305
-
3306
- it 'anchors the routes, passing all subroutes to it' do
3307
- get '/mounty/awesome'
3308
- expect(last_response.body).to eq('MOUNTED')
3309
- end
3310
-
3311
- it 'is able to cascade' do
3312
- subject.mount lambda { |env|
3313
- headers = {}
3314
- headers['X-Cascade'] == 'pass' unless env['PATH_INFO'].include?('boo')
3315
- [200, headers, ['Farfegnugen']]
3316
- } => '/'
3317
-
3318
- get '/boo'
3319
- expect(last_response.body).to eq('Farfegnugen')
3320
- get '/mounty'
3321
- expect(last_response.body).to eq('MOUNTED')
3322
- end
3323
- end
3324
-
3325
- context 'without a hash' do
3326
- it 'calls through setting the route to "/"' do
3327
- subject.mount mounted_app
3328
- get '/'
3329
- expect(last_response.body).to eq('MOUNTED')
3330
- end
3331
- end
3332
-
3333
- context 'mounting an API' do
3334
- it 'applies the settings of the mounting api' do
3335
- subject.version 'v1', using: :path
3336
-
3337
- subject.namespace :cool do
3338
- app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass
3339
- app.get('/awesome') do
3340
- 'yo'
3341
- end
3342
-
3343
- mount app
3344
- end
3345
-
3346
- get '/v1/cool/awesome'
3347
- expect(last_response.body).to eq('yo')
3348
- end
3349
-
3350
- it 'applies the settings to nested mounted apis' do
3351
- subject.version 'v1', using: :path
3352
-
3353
- subject.namespace :cool do
3354
- inner_app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass
3355
- inner_app.get('/awesome') do
3356
- 'yo'
3357
- end
3358
-
3359
- app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass
3360
- app.mount inner_app
3361
- mount app
3362
- end
3363
-
3364
- get '/v1/cool/awesome'
3365
- expect(last_response.body).to eq('yo')
3366
- end
3367
-
3368
- context 'when some rescues are defined by mounted' do
3369
- it 'inherits parent rescues' do
3370
- subject.rescue_from :all do |e|
3371
- rack_response("rescued from #{e.message}", 202)
3372
- end
3373
-
3374
- app = Class.new(described_class)
3375
-
3376
- subject.namespace :mounted do
3377
- app.rescue_from ArgumentError
3378
- app.get('/fail') { raise 'doh!' }
3379
- mount app
3380
- end
3381
-
3382
- get '/mounted/fail'
3383
- expect(last_response.status).to be 202
3384
- expect(last_response.body).to eq('rescued from doh!')
3385
- end
3386
-
3387
- it 'prefers rescues defined by mounted if they rescue similar error class' do
3388
- subject.rescue_from StandardError do
3389
- rack_response('outer rescue')
3390
- end
3391
-
3392
- app = Class.new(described_class)
3393
-
3394
- subject.namespace :mounted do
3395
- rescue_from StandardError do
3396
- rack_response('inner rescue')
3397
- end
3398
- app.get('/fail') { raise 'doh!' }
3399
- mount app
3400
- end
3401
-
3402
- get '/mounted/fail'
3403
- expect(last_response.body).to eq('inner rescue')
3404
- end
3405
-
3406
- it 'prefers rescues defined by mounted even if outer is more specific' do
3407
- subject.rescue_from ArgumentError do
3408
- rack_response('outer rescue')
3409
- end
3410
-
3411
- app = Class.new(described_class)
3412
-
3413
- subject.namespace :mounted do
3414
- rescue_from StandardError do
3415
- rack_response('inner rescue')
3416
- end
3417
- app.get('/fail') { raise ArgumentError.new }
3418
- mount app
3419
- end
3420
-
3421
- get '/mounted/fail'
3422
- expect(last_response.body).to eq('inner rescue')
3423
- end
3424
-
3425
- it 'prefers more specific rescues defined by mounted' do
3426
- subject.rescue_from StandardError do
3427
- rack_response('outer rescue')
3428
- end
3429
-
3430
- app = Class.new(described_class)
3431
-
3432
- subject.namespace :mounted do
3433
- rescue_from ArgumentError do
3434
- rack_response('inner rescue')
3435
- end
3436
- app.get('/fail') { raise ArgumentError.new }
3437
- mount app
3438
- end
3439
-
3440
- get '/mounted/fail'
3441
- expect(last_response.body).to eq('inner rescue')
3442
- end
3443
- end
3444
-
3445
- it 'collects the routes of the mounted api' do
3446
- subject.namespace :cool do
3447
- app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass
3448
- app.get('/awesome') {}
3449
- app.post('/sauce') {}
3450
- mount app
3451
- end
3452
- expect(subject.routes.size).to eq(2)
3453
- expect(subject.routes.first.path).to match(%r{/cool/awesome})
3454
- expect(subject.routes.last.path).to match(%r{/cool/sauce})
3455
- end
3456
-
3457
- it 'mounts on a path' do
3458
- subject.namespace :cool do
3459
- app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass
3460
- app.get '/awesome' do
3461
- 'sauce'
3462
- end
3463
- mount app => '/mounted'
3464
- end
3465
- get '/mounted/cool/awesome'
3466
- expect(last_response.status).to eq(200)
3467
- expect(last_response.body).to eq('sauce')
3468
- end
3469
-
3470
- it 'mounts on a nested path' do
3471
- APP1 = Class.new(described_class)
3472
- APP2 = Class.new(described_class)
3473
- APP2.get '/nice' do
3474
- 'play'
3475
- end
3476
- # NOTE: that the reverse won't work, mount from outside-in
3477
- APP3 = subject
3478
- APP3.mount APP1 => '/app1'
3479
- APP1.mount APP2 => '/app2'
3480
- get '/app1/app2/nice'
3481
- expect(last_response.status).to eq(200)
3482
- expect(last_response.body).to eq('play')
3483
- options '/app1/app2/nice'
3484
- expect(last_response.status).to eq(204)
3485
- end
3486
-
3487
- it 'responds to options' do
3488
- app = Class.new(described_class)
3489
- app.get '/colour' do
3490
- 'red'
3491
- end
3492
- app.namespace :pears do
3493
- get '/colour' do
3494
- 'green'
3495
- end
3496
- end
3497
- subject.namespace :apples do
3498
- mount app
3499
- end
3500
-
3501
- get '/apples/colour'
3502
- expect(last_response.status).to be 200
3503
- expect(last_response.body).to eq('red')
3504
- options '/apples/colour'
3505
- expect(last_response.status).to be 204
3506
- get '/apples/pears/colour'
3507
- expect(last_response.status).to be 200
3508
- expect(last_response.body).to eq('green')
3509
- options '/apples/pears/colour'
3510
- expect(last_response.status).to be 204
3511
- end
3512
-
3513
- it 'responds to options with path versioning' do
3514
- subject.version 'v1', using: :path
3515
- subject.namespace :apples do
3516
- app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass
3517
- app.get('/colour') do
3518
- 'red'
3519
- end
3520
- mount app
3521
- end
3522
-
3523
- get '/v1/apples/colour'
3524
- expect(last_response.status).to be 200
3525
- expect(last_response.body).to eq('red')
3526
- options '/v1/apples/colour'
3527
- expect(last_response.status).to be 204
3528
- end
3529
-
3530
- it 'mounts a versioned API with nested resources' do
3531
- api = Class.new(described_class) do
3532
- version 'v1'
3533
- resources :users do
3534
- get :hello do
3535
- 'hello users'
3536
- end
3537
- end
3538
- end
3539
- subject.mount api
3540
-
3541
- get '/v1/users/hello'
3542
- expect(last_response.body).to eq('hello users')
3543
- end
3544
-
3545
- it 'mounts a prefixed API with nested resources' do
3546
- api = Class.new(described_class) do
3547
- prefix 'api'
3548
- resources :users do
3549
- get :hello do
3550
- 'hello users'
3551
- end
3552
- end
3553
- end
3554
- subject.mount api
3555
-
3556
- get '/api/users/hello'
3557
- expect(last_response.body).to eq('hello users')
3558
- end
3559
-
3560
- it 'applies format to a mounted API with nested resources' do
3561
- api = Class.new(described_class) do
3562
- format :json
3563
- resources :users do
3564
- get do
3565
- { users: true }
3566
- end
3567
- end
3568
- end
3569
- subject.mount api
3570
-
3571
- get '/users'
3572
- expect(last_response.body).to eq({ users: true }.to_json)
3573
- end
3574
-
3575
- it 'applies auth to a mounted API with nested resources' do
3576
- api = Class.new(described_class) do
3577
- format :json
3578
- http_basic do |username, password|
3579
- username == 'username' && password == 'password'
3580
- end
3581
- resources :users do
3582
- get do
3583
- { users: true }
3584
- end
3585
- end
3586
- end
3587
- subject.mount api
3588
-
3589
- get '/users'
3590
- expect(last_response.status).to eq(401)
3591
-
3592
- get '/users', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('username', 'password')
3593
- expect(last_response.body).to eq({ users: true }.to_json)
3594
- end
3595
-
3596
- it 'mounts multiple versioned APIs with nested resources' do
3597
- api1 = Class.new(described_class) do
3598
- version 'one', using: :header, vendor: 'test'
3599
- resources :users do
3600
- get :hello do
3601
- 'one'
3602
- end
3603
- end
3604
- end
3605
-
3606
- api2 = Class.new(described_class) do
3607
- version 'two', using: :header, vendor: 'test'
3608
- resources :users do
3609
- get :hello do
3610
- 'two'
3611
- end
3612
- end
3613
- end
3614
-
3615
- subject.mount api1
3616
- subject.mount api2
3617
-
3618
- versioned_get '/users/hello', 'one', using: :header, vendor: 'test'
3619
- expect(last_response.body).to eq('one')
3620
- versioned_get '/users/hello', 'two', using: :header, vendor: 'test'
3621
- expect(last_response.body).to eq('two')
3622
- end
3623
-
3624
- it 'recognizes potential versions with mounted path' do
3625
- a = Class.new(described_class) do
3626
- version :v1, using: :path
3627
-
3628
- get '/hello' do
3629
- 'hello'
3630
- end
3631
- end
3632
-
3633
- b = Class.new(described_class) do
3634
- version :v1, using: :path
3635
-
3636
- get '/world' do
3637
- 'world'
3638
- end
3639
- end
3640
-
3641
- subject.mount a => '/one'
3642
- subject.mount b => '/two'
3643
-
3644
- get '/one/v1/hello'
3645
- expect(last_response.status).to eq 200
3646
-
3647
- get '/two/v1/world'
3648
- expect(last_response.status).to eq 200
3649
- end
3650
-
3651
- context 'when mounting class extends a subclass of Grape::API' do
3652
- it 'mounts APIs with the same superclass' do
3653
- base_api = Class.new(described_class)
3654
- a = Class.new(base_api)
3655
- b = Class.new(base_api)
3656
-
3657
- expect { a.mount b }.not_to raise_error
3658
- end
3659
- end
3660
-
3661
- context 'when including a module' do
3662
- let(:included_module) do
3663
- Module.new do
3664
- def self.included(base)
3665
- base.extend(ClassMethods)
3666
- end
3667
-
3668
- module ClassMethods
3669
- def my_method
3670
- @test = true
3671
- end
3672
- end
3673
- end
3674
- end
3675
-
3676
- it 'correctlies include module in nested mount' do
3677
- module_to_include = included_module
3678
- v1 = Class.new(described_class) do
3679
- version :v1, using: :path
3680
- include module_to_include
3681
- my_method
3682
- end
3683
- v2 = Class.new(described_class) do
3684
- version :v2, using: :path
3685
- end
3686
- segment_base = Class.new(described_class) do
3687
- mount v1
3688
- mount v2
3689
- end
3690
-
3691
- Class.new(described_class) do
3692
- mount segment_base
3693
- end
3694
-
3695
- expect(v1.my_method).to be_truthy
3696
- end
3697
- end
3698
- end
3699
- end
3700
-
3701
- describe '.endpoints' do
3702
- it 'adds one for each route created' do
3703
- subject.get '/'
3704
- subject.post '/'
3705
- expect(subject.endpoints.size).to eq(2)
3706
- end
3707
- end
3708
-
3709
- describe '.compile' do
3710
- it 'sets the instance' do
3711
- expect(subject.instance).to be_nil
3712
- subject.compile
3713
- expect(subject.instance).to be_a(subject.base_instance)
3714
- end
3715
- end
3716
-
3717
- describe '.change!' do
3718
- it 'invalidates any compiled instance' do
3719
- subject.compile
3720
- subject.change!
3721
- expect(subject.instance).to be_nil
3722
- end
3723
- end
3724
-
3725
- describe '.endpoint' do
3726
- before do
3727
- subject.format :json
3728
- subject.get '/endpoint/options' do
3729
- {
3730
- path: options[:path],
3731
- source_location: source.source_location
3732
- }
3733
- end
3734
- end
3735
-
3736
- it 'path' do
3737
- get '/endpoint/options'
3738
- options = ::Grape::Json.load(last_response.body)
3739
- expect(options['path']).to eq(['/endpoint/options'])
3740
- expect(options['source_location'][0]).to include 'api_spec.rb'
3741
- expect(options['source_location'][1].to_i).to be > 0
3742
- end
3743
- end
3744
-
3745
- describe '.route' do
3746
- context 'plain' do
3747
- before do
3748
- subject.get '/' do
3749
- route.path
3750
- end
3751
- subject.get '/path' do
3752
- route.path
3753
- end
3754
- end
3755
-
3756
- it 'provides access to route info' do
3757
- get '/'
3758
- expect(last_response.body).to eq('/(.:format)')
3759
- get '/path'
3760
- expect(last_response.body).to eq('/path(.:format)')
3761
- end
3762
- end
3763
-
3764
- context 'with desc' do
3765
- before do
3766
- subject.desc 'returns description'
3767
- subject.get '/description' do
3768
- route.description
3769
- end
3770
- subject.desc 'returns parameters', params: { 'x' => 'y' }
3771
- subject.get '/params/:id' do
3772
- route.params[params[:id]]
3773
- end
3774
- end
3775
-
3776
- it 'returns route description' do
3777
- get '/description'
3778
- expect(last_response.body).to eq('returns description')
3779
- end
3780
-
3781
- it 'returns route parameters' do
3782
- get '/params/x'
3783
- expect(last_response.body).to eq('y')
3784
- end
3785
- end
3786
- end
3787
-
3788
- describe '.format' do
3789
- context ':txt' do
3790
- before do
3791
- subject.format :txt
3792
- subject.content_type :json, 'application/json'
3793
- subject.get '/meaning_of_life' do
3794
- { meaning_of_life: 42 }
3795
- end
3796
- end
3797
-
3798
- it 'forces txt without an extension' do
3799
- get '/meaning_of_life'
3800
- expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
3801
- end
3802
-
3803
- it 'does not force txt with an extension' do
3804
- get '/meaning_of_life.json'
3805
- expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json)
3806
- end
3807
-
3808
- it 'forces txt from a non-accepting header' do
3809
- get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json'
3810
- expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
3811
- end
3812
- end
3813
-
3814
- context ':txt only' do
3815
- before do
3816
- subject.format :txt
3817
- subject.get '/meaning_of_life' do
3818
- { meaning_of_life: 42 }
3819
- end
3820
- end
3821
-
3822
- it 'forces txt without an extension' do
3823
- get '/meaning_of_life'
3824
- expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
3825
- end
3826
-
3827
- it 'accepts specified extension' do
3828
- get '/meaning_of_life.txt'
3829
- expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
3830
- end
3831
-
3832
- it 'does not accept extensions other than specified' do
3833
- get '/meaning_of_life.json'
3834
- expect(last_response.status).to eq(404)
3835
- end
3836
-
3837
- it 'forces txt from a non-accepting header' do
3838
- get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json'
3839
- expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
3840
- end
3841
- end
3842
-
3843
- context ':json' do
3844
- before do
3845
- subject.format :json
3846
- subject.content_type :txt, 'text/plain'
3847
- subject.get '/meaning_of_life' do
3848
- { meaning_of_life: 42 }
3849
- end
3850
- end
3851
-
3852
- it 'forces json without an extension' do
3853
- get '/meaning_of_life'
3854
- expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json)
3855
- end
3856
-
3857
- it 'does not force json with an extension' do
3858
- get '/meaning_of_life.txt'
3859
- expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
3860
- end
3861
-
3862
- it 'forces json from a non-accepting header' do
3863
- get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html'
3864
- expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json)
3865
- end
3866
-
3867
- it 'can be overwritten with an explicit content type' do
3868
- subject.get '/meaning_of_life_with_content_type' do
3869
- content_type 'text/plain'
3870
- { meaning_of_life: 42 }.to_s
3871
- end
3872
- get '/meaning_of_life_with_content_type'
3873
- expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
3874
- end
3875
-
3876
- it 'raised :error from middleware' do
3877
- middleware = Class.new(Grape::Middleware::Base) do
3878
- def before
3879
- throw :error, message: 'Unauthorized', status: 42
3880
- end
3881
- end
3882
- subject.use middleware
3883
- subject.get do
3884
- end
3885
- get '/'
3886
- expect(last_response.status).to eq(42)
3887
- expect(last_response.body).to eq({ error: 'Unauthorized' }.to_json)
3888
- end
3889
- end
3890
-
3891
- context ':serializable_hash' do
3892
- class SerializableHashExample
3893
- def serializable_hash
3894
- { abc: 'def' }
3895
- end
3896
- end
3897
-
3898
- before do
3899
- subject.format :serializable_hash
3900
- end
3901
-
3902
- it 'instance' do
3903
- subject.get '/example' do
3904
- SerializableHashExample.new
3905
- end
3906
- get '/example'
3907
- expect(last_response.body).to eq('{"abc":"def"}')
3908
- end
3909
-
3910
- it 'root' do
3911
- subject.get '/example' do
3912
- { 'root' => SerializableHashExample.new }
3913
- end
3914
- get '/example'
3915
- expect(last_response.body).to eq('{"root":{"abc":"def"}}')
3916
- end
3917
-
3918
- it 'array' do
3919
- subject.get '/examples' do
3920
- [SerializableHashExample.new, SerializableHashExample.new]
3921
- end
3922
- get '/examples'
3923
- expect(last_response.body).to eq('[{"abc":"def"},{"abc":"def"}]')
3924
- end
3925
- end
3926
-
3927
- context ':xml' do
3928
- before do
3929
- subject.format :xml
3930
- end
3931
-
3932
- it 'string' do
3933
- subject.get '/example' do
3934
- 'example'
3935
- end
3936
- get '/example'
3937
- expect(last_response.status).to eq(500)
3938
- expect(last_response.body).to eq <<~XML
3939
- <?xml version="1.0" encoding="UTF-8"?>
3940
- <error>
3941
- <message>cannot convert String to xml</message>
3942
- </error>
3943
- XML
3944
- end
3945
-
3946
- it 'hash' do
3947
- subject.get '/example' do
3948
- {
3949
- example1: 'example1',
3950
- example2: 'example2'
3951
- }
3952
- end
3953
- get '/example'
3954
- expect(last_response.status).to eq(200)
3955
- expect(last_response.body).to eq <<~XML
3956
- <?xml version="1.0" encoding="UTF-8"?>
3957
- <hash>
3958
- <example1>example1</example1>
3959
- <example2>example2</example2>
3960
- </hash>
3961
- XML
3962
- end
3963
-
3964
- it 'array' do
3965
- subject.get '/example' do
3966
- %w[example1 example2]
3967
- end
3968
- get '/example'
3969
- expect(last_response.status).to eq(200)
3970
- expect(last_response.body).to eq <<~XML
3971
- <?xml version="1.0" encoding="UTF-8"?>
3972
- <strings type="array">
3973
- <string>example1</string>
3974
- <string>example2</string>
3975
- </strings>
3976
- XML
3977
- end
3978
-
3979
- it 'raised :error from middleware' do
3980
- middleware = Class.new(Grape::Middleware::Base) do
3981
- def before
3982
- throw :error, message: 'Unauthorized', status: 42
3983
- end
3984
- end
3985
- subject.use middleware
3986
- subject.get do
3987
- end
3988
- get '/'
3989
- expect(last_response.status).to eq(42)
3990
- expect(last_response.body).to eq <<~XML
3991
- <?xml version="1.0" encoding="UTF-8"?>
3992
- <error>
3993
- <message>Unauthorized</message>
3994
- </error>
3995
- XML
3996
- end
3997
- end
3998
- end
3999
-
4000
- describe '.configure' do
4001
- context 'when given a block' do
4002
- it 'returns self' do
4003
- expect(subject.configure {}).to be subject
4004
- end
4005
-
4006
- it 'calls the block passing the config' do
4007
- call = [false, nil]
4008
- subject.configure do |config|
4009
- call = [true, config]
4010
- end
4011
-
4012
- expect(call[0]).to be true
4013
- expect(call[1]).not_to be_nil
4014
- end
4015
- end
4016
-
4017
- context 'when not given a block' do
4018
- it 'returns a configuration object' do
4019
- expect(subject.configure).to respond_to(:[], :[]=)
4020
- end
4021
- end
4022
-
4023
- it 'allows configuring the api' do
4024
- subject.configure do |config|
4025
- config[:hello] = 'hello'
4026
- config[:bread] = 'bread'
4027
- end
4028
-
4029
- subject.get '/hello-bread' do
4030
- "#{configuration[:hello]} #{configuration[:bread]}"
4031
- end
4032
-
4033
- get '/hello-bread'
4034
- expect(last_response.body).to eq 'hello bread'
4035
- end
4036
- end
4037
-
4038
- context 'catch-all' do
4039
- before do
4040
- api1 = Class.new(described_class)
4041
- api1.version 'v1', using: :path
4042
- api1.get 'hello' do
4043
- 'v1'
4044
- end
4045
- api2 = Class.new(described_class)
4046
- api2.version 'v2', using: :path
4047
- api2.get 'hello' do
4048
- 'v2'
4049
- end
4050
- subject.mount api1
4051
- subject.mount api2
4052
- end
4053
-
4054
- [true, false].each do |anchor|
4055
- it "anchor=#{anchor}" do
4056
- subject.route :any, '*path', anchor: anchor do
4057
- error!("Unrecognized request path: #{params[:path]} - #{env['PATH_INFO']}#{env['SCRIPT_NAME']}", 404)
4058
- end
4059
- get '/v1/hello'
4060
- expect(last_response.status).to eq(200)
4061
- expect(last_response.body).to eq('v1')
4062
- get '/v2/hello'
4063
- expect(last_response.status).to eq(200)
4064
- expect(last_response.body).to eq('v2')
4065
- options '/v2/hello'
4066
- expect(last_response.status).to eq(204)
4067
- expect(last_response.body).to be_blank
4068
- head '/v2/hello'
4069
- expect(last_response.status).to eq(200)
4070
- expect(last_response.body).to be_blank
4071
- get '/foobar'
4072
- expect(last_response.status).to eq(404)
4073
- expect(last_response.body).to eq('Unrecognized request path: foobar - /foobar')
4074
- end
4075
- end
4076
- end
4077
-
4078
- context 'cascading' do
4079
- context 'via version' do
4080
- it 'cascades' do
4081
- subject.version 'v1', using: :path, cascade: true
4082
- get '/v1/hello'
4083
- expect(last_response.status).to eq(404)
4084
- expect(last_response.headers['X-Cascade']).to eq('pass')
4085
- end
4086
-
4087
- it 'does not cascade' do
4088
- subject.version 'v2', using: :path, cascade: false
4089
- get '/v2/hello'
4090
- expect(last_response.status).to eq(404)
4091
- expect(last_response.headers.keys).not_to include 'X-Cascade'
4092
- end
4093
- end
4094
-
4095
- context 'via endpoint' do
4096
- it 'cascades' do
4097
- subject.cascade true
4098
- get '/hello'
4099
- expect(last_response.status).to eq(404)
4100
- expect(last_response.headers['X-Cascade']).to eq('pass')
4101
- end
4102
-
4103
- it 'does not cascade' do
4104
- subject.cascade false
4105
- get '/hello'
4106
- expect(last_response.status).to eq(404)
4107
- expect(last_response.headers.keys).not_to include 'X-Cascade'
4108
- end
4109
- end
4110
- end
4111
-
4112
- context 'with json default_error_formatter' do
4113
- it 'returns json error' do
4114
- subject.content_type :json, 'application/json'
4115
- subject.default_error_formatter :json
4116
- subject.get '/something' do
4117
- 'foo'
4118
- end
4119
- get '/something'
4120
- expect(last_response.status).to eq(406)
4121
- if ActiveSupport::VERSION::MAJOR == 3
4122
- expect(last_response.body).to eq('{&quot;error&quot;:&quot;The requested format &#x27;txt&#x27; is not supported.&quot;}')
4123
- else
4124
- expect(last_response.body).to eq('{&quot;error&quot;:&quot;The requested format &#39;txt&#39; is not supported.&quot;}')
4125
- end
4126
- end
4127
- end
4128
-
4129
- context 'with unsafe HTML format specified' do
4130
- it 'escapes the HTML' do
4131
- subject.content_type :json, 'application/json'
4132
- subject.get '/something' do
4133
- 'foo'
4134
- end
4135
- get '/something?format=<script>blah</script>'
4136
- expect(last_response.status).to eq(406)
4137
- if ActiveSupport::VERSION::MAJOR == 3
4138
- expect(last_response.body).to eq('The requested format &#x27;&lt;script&gt;blah&lt;/script&gt;&#x27; is not supported.')
4139
- else
4140
- expect(last_response.body).to eq('The requested format &#39;&lt;script&gt;blah&lt;/script&gt;&#39; is not supported.')
4141
- end
4142
- end
4143
- end
4144
-
4145
- context 'with non-UTF-8 characters in specified format' do
4146
- it 'converts the characters' do
4147
- subject.format :json
4148
- subject.content_type :json, 'application/json'
4149
- subject.get '/something' do
4150
- 'foo'
4151
- end
4152
- get '/something?format=%0A%0B%BF'
4153
- expect(last_response.status).to eq(406)
4154
- message = "The requested format '\n\u000b\357\277\275' is not supported."
4155
- expect(last_response.body).to eq({ error: message }.to_json)
4156
- end
4157
- end
4158
-
4159
- context 'body' do
4160
- context 'false' do
4161
- before do
4162
- subject.get '/blank' do
4163
- body false
4164
- end
4165
- end
4166
-
4167
- it 'returns blank body' do
4168
- get '/blank'
4169
- expect(last_response.status).to eq(204)
4170
- expect(last_response.body).to be_blank
4171
- end
4172
- end
4173
-
4174
- context 'plain text' do
4175
- before do
4176
- subject.get '/text' do
4177
- content_type 'text/plain'
4178
- body 'Hello World'
4179
- 'ignored'
4180
- end
4181
- end
4182
-
4183
- it 'returns blank body' do
4184
- get '/text'
4185
- expect(last_response.status).to eq(200)
4186
- expect(last_response.body).to eq 'Hello World'
4187
- end
4188
- end
4189
- end
4190
-
4191
- describe 'normal class methods' do
4192
- subject(:grape_api) { Class.new(described_class) }
4193
-
4194
- before do
4195
- stub_const('MyAPI', grape_api)
4196
- end
4197
-
4198
- it 'can find the appropiate name' do
4199
- expect(grape_api.name).to eq 'MyAPI'
4200
- end
4201
-
4202
- it 'is equal to itself' do
4203
- expect(grape_api.itself).to eq grape_api
4204
- expect(grape_api).to eq MyAPI
4205
- expect(grape_api.eql?(MyAPI))
4206
- end
4207
- end
4208
-
4209
- describe '.inherited' do
4210
- context 'overriding within class' do
4211
- let(:root_api) do
4212
- Class.new(described_class) do
4213
- @bar = 'Hello, world'
4214
-
4215
- def self.inherited(child_api)
4216
- super
4217
- child_api.instance_variable_set(:@foo, @bar.dup)
4218
- end
4219
- end
4220
- end
4221
-
4222
- let(:child_api) { Class.new(root_api) }
4223
-
4224
- it 'allows overriding the hook' do
4225
- expect(child_api.instance_variable_get(:@foo)).to eq('Hello, world')
4226
- end
4227
- end
4228
-
4229
- context 'overriding via composition' do
4230
- module Inherited
4231
- def inherited(api)
4232
- super
4233
- api.instance_variable_set(:@foo, @bar.dup)
4234
- end
4235
- end
4236
-
4237
- let(:root_api) do
4238
- Class.new(described_class) do
4239
- @bar = 'Hello, world'
4240
- extend Inherited
4241
- end
4242
- end
4243
-
4244
- let(:child_api) { Class.new(root_api) }
4245
-
4246
- it 'allows overriding the hook' do
4247
- expect(child_api.instance_variable_get(:@foo)).to eq('Hello, world')
4248
- end
4249
- end
4250
- end
4251
-
4252
- describe 'const_missing' do
4253
- subject(:grape_api) { Class.new(described_class) }
4254
-
4255
- let(:mounted) do
4256
- Class.new(described_class) do
4257
- get '/missing' do
4258
- SomeRandomConstant
4259
- end
4260
- end
4261
- end
4262
-
4263
- before { subject.mount mounted => '/const' }
4264
-
4265
- it 'raises an error' do
4266
- expect { get '/const/missing' }.to raise_error(NameError).with_message(/SomeRandomConstant/)
4267
- end
4268
- end
4269
-
4270
- describe 'custom route helpers on nested APIs' do
4271
- subject(:grape_api) do
4272
- Class.new(described_class) do
4273
- version 'v1', using: :path
4274
- end
4275
- end
4276
-
4277
- let(:shared_api_module) do
4278
- Module.new do
4279
- # rubocop:disable Style/ExplicitBlockArgument because this causes
4280
- # the underlying issue in this form
4281
- def uniqe_id_route
4282
- params do
4283
- use :unique_id
4284
- end
4285
- route_param(:id) do
4286
- yield
4287
- end
4288
- end
4289
- # rubocop:enable Style/ExplicitBlockArgument
4290
- end
4291
- end
4292
- let(:shared_api_definitions) do
4293
- Module.new do
4294
- extend ActiveSupport::Concern
4295
-
4296
- included do
4297
- helpers do
4298
- params :unique_id do
4299
- requires :id, type: String,
4300
- allow_blank: false,
4301
- regexp: /\d+-\d+/
4302
- end
4303
- end
4304
- end
4305
- end
4306
- end
4307
- let(:orders_root) do
4308
- shared = shared_api_definitions
4309
- find = orders_find_endpoint
4310
- Class.new(described_class) do
4311
- include shared
4312
-
4313
- namespace(:orders) do
4314
- mount find
4315
- end
4316
- end
4317
- end
4318
- let(:orders_find_endpoint) do
4319
- shared = shared_api_definitions
4320
- Class.new(described_class) do
4321
- include shared
4322
-
4323
- uniqe_id_route do
4324
- desc 'Fetch a single order' do
4325
- detail 'While specifying the order id on the route'
4326
- end
4327
- get { params[:id] }
4328
- end
4329
- end
4330
- end
4331
-
4332
- before do
4333
- Grape::API::Instance.extend(shared_api_module)
4334
- subject.mount orders_root
4335
- end
4336
-
4337
- it 'returns an error when the id is bad' do
4338
- get '/v1/orders/abc'
4339
- expect(last_response.body).to be_eql('id is invalid')
4340
- end
4341
-
4342
- it 'returns the given id when it is valid' do
4343
- get '/v1/orders/1-2'
4344
- expect(last_response.body).to be_eql('1-2')
4345
- end
4346
- end
4347
- end