grape 1.7.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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