grape-security 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +45 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +70 -0
  5. data/.travis.yml +18 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG.md +314 -0
  8. data/CONTRIBUTING.md +118 -0
  9. data/Gemfile +21 -0
  10. data/Guardfile +14 -0
  11. data/LICENSE +20 -0
  12. data/README.md +1777 -0
  13. data/RELEASING.md +105 -0
  14. data/Rakefile +69 -0
  15. data/UPGRADING.md +124 -0
  16. data/grape-security.gemspec +39 -0
  17. data/grape.png +0 -0
  18. data/lib/grape.rb +99 -0
  19. data/lib/grape/api.rb +646 -0
  20. data/lib/grape/cookies.rb +39 -0
  21. data/lib/grape/endpoint.rb +533 -0
  22. data/lib/grape/error_formatter/base.rb +31 -0
  23. data/lib/grape/error_formatter/json.rb +15 -0
  24. data/lib/grape/error_formatter/txt.rb +16 -0
  25. data/lib/grape/error_formatter/xml.rb +15 -0
  26. data/lib/grape/exceptions/base.rb +66 -0
  27. data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
  28. data/lib/grape/exceptions/invalid_formatter.rb +10 -0
  29. data/lib/grape/exceptions/invalid_versioner_option.rb +10 -0
  30. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +10 -0
  31. data/lib/grape/exceptions/missing_mime_type.rb +10 -0
  32. data/lib/grape/exceptions/missing_option.rb +10 -0
  33. data/lib/grape/exceptions/missing_vendor_option.rb +10 -0
  34. data/lib/grape/exceptions/unknown_options.rb +10 -0
  35. data/lib/grape/exceptions/unknown_validator.rb +10 -0
  36. data/lib/grape/exceptions/validation.rb +26 -0
  37. data/lib/grape/exceptions/validation_errors.rb +43 -0
  38. data/lib/grape/formatter/base.rb +31 -0
  39. data/lib/grape/formatter/json.rb +12 -0
  40. data/lib/grape/formatter/serializable_hash.rb +35 -0
  41. data/lib/grape/formatter/txt.rb +11 -0
  42. data/lib/grape/formatter/xml.rb +12 -0
  43. data/lib/grape/http/request.rb +26 -0
  44. data/lib/grape/locale/en.yml +32 -0
  45. data/lib/grape/middleware/auth/base.rb +30 -0
  46. data/lib/grape/middleware/auth/basic.rb +13 -0
  47. data/lib/grape/middleware/auth/digest.rb +13 -0
  48. data/lib/grape/middleware/auth/oauth2.rb +83 -0
  49. data/lib/grape/middleware/base.rb +62 -0
  50. data/lib/grape/middleware/error.rb +89 -0
  51. data/lib/grape/middleware/filter.rb +17 -0
  52. data/lib/grape/middleware/formatter.rb +150 -0
  53. data/lib/grape/middleware/globals.rb +13 -0
  54. data/lib/grape/middleware/versioner.rb +32 -0
  55. data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
  56. data/lib/grape/middleware/versioner/header.rb +132 -0
  57. data/lib/grape/middleware/versioner/param.rb +42 -0
  58. data/lib/grape/middleware/versioner/path.rb +52 -0
  59. data/lib/grape/namespace.rb +23 -0
  60. data/lib/grape/parser/base.rb +29 -0
  61. data/lib/grape/parser/json.rb +11 -0
  62. data/lib/grape/parser/xml.rb +11 -0
  63. data/lib/grape/path.rb +70 -0
  64. data/lib/grape/route.rb +27 -0
  65. data/lib/grape/util/content_types.rb +18 -0
  66. data/lib/grape/util/deep_merge.rb +23 -0
  67. data/lib/grape/util/hash_stack.rb +120 -0
  68. data/lib/grape/validations.rb +322 -0
  69. data/lib/grape/validations/coerce.rb +63 -0
  70. data/lib/grape/validations/default.rb +25 -0
  71. data/lib/grape/validations/exactly_one_of.rb +26 -0
  72. data/lib/grape/validations/mutual_exclusion.rb +25 -0
  73. data/lib/grape/validations/presence.rb +16 -0
  74. data/lib/grape/validations/regexp.rb +12 -0
  75. data/lib/grape/validations/values.rb +23 -0
  76. data/lib/grape/version.rb +3 -0
  77. data/spec/grape/api_spec.rb +2571 -0
  78. data/spec/grape/endpoint_spec.rb +784 -0
  79. data/spec/grape/entity_spec.rb +324 -0
  80. data/spec/grape/exceptions/invalid_formatter_spec.rb +18 -0
  81. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +18 -0
  82. data/spec/grape/exceptions/missing_mime_type_spec.rb +18 -0
  83. data/spec/grape/exceptions/missing_option_spec.rb +18 -0
  84. data/spec/grape/exceptions/unknown_options_spec.rb +18 -0
  85. data/spec/grape/exceptions/unknown_validator_spec.rb +18 -0
  86. data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
  87. data/spec/grape/middleware/auth/basic_spec.rb +31 -0
  88. data/spec/grape/middleware/auth/digest_spec.rb +47 -0
  89. data/spec/grape/middleware/auth/oauth2_spec.rb +135 -0
  90. data/spec/grape/middleware/base_spec.rb +58 -0
  91. data/spec/grape/middleware/error_spec.rb +45 -0
  92. data/spec/grape/middleware/exception_spec.rb +184 -0
  93. data/spec/grape/middleware/formatter_spec.rb +258 -0
  94. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
  95. data/spec/grape/middleware/versioner/header_spec.rb +302 -0
  96. data/spec/grape/middleware/versioner/param_spec.rb +58 -0
  97. data/spec/grape/middleware/versioner/path_spec.rb +44 -0
  98. data/spec/grape/middleware/versioner_spec.rb +22 -0
  99. data/spec/grape/path_spec.rb +229 -0
  100. data/spec/grape/util/hash_stack_spec.rb +132 -0
  101. data/spec/grape/validations/coerce_spec.rb +208 -0
  102. data/spec/grape/validations/default_spec.rb +123 -0
  103. data/spec/grape/validations/exactly_one_of_spec.rb +71 -0
  104. data/spec/grape/validations/mutual_exclusion_spec.rb +61 -0
  105. data/spec/grape/validations/presence_spec.rb +142 -0
  106. data/spec/grape/validations/regexp_spec.rb +40 -0
  107. data/spec/grape/validations/values_spec.rb +152 -0
  108. data/spec/grape/validations/zh-CN.yml +10 -0
  109. data/spec/grape/validations_spec.rb +994 -0
  110. data/spec/shared/versioning_examples.rb +121 -0
  111. data/spec/spec_helper.rb +26 -0
  112. data/spec/support/basic_auth_encode_helpers.rb +3 -0
  113. data/spec/support/content_type_helpers.rb +11 -0
  114. data/spec/support/versioned_helpers.rb +50 -0
  115. metadata +421 -0
@@ -0,0 +1,63 @@
1
+ module Grape
2
+ class API
3
+ Boolean = Virtus::Attribute::Boolean # rubocop:disable ConstantName
4
+ end
5
+
6
+ module Validations
7
+ class CoerceValidator < SingleOptionValidator
8
+ def validate_param!(attr_name, params)
9
+ raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message_key: :coerce unless params.is_a? Hash
10
+ new_value = coerce_value(@option, params[attr_name])
11
+ if valid_type?(new_value)
12
+ params[attr_name] = new_value
13
+ else
14
+ raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message_key: :coerce
15
+ end
16
+ end
17
+
18
+ class InvalidValue; end
19
+
20
+ private
21
+
22
+ def _valid_array_type?(type, values)
23
+ values.all? do |val|
24
+ _valid_single_type?(type, val)
25
+ end
26
+ end
27
+
28
+ def _valid_single_type?(klass, val)
29
+ # allow nil, to ignore when a parameter is absent
30
+ return true if val.nil?
31
+ if klass == Virtus::Attribute::Boolean
32
+ val.is_a?(TrueClass) || val.is_a?(FalseClass)
33
+ elsif klass == Rack::Multipart::UploadedFile
34
+ val.is_a?(Hashie::Mash) && val.key?(:tempfile)
35
+ else
36
+ val.is_a?(klass)
37
+ end
38
+ end
39
+
40
+ def valid_type?(val)
41
+ if @option.is_a?(Array)
42
+ _valid_array_type?(@option[0], val)
43
+ else
44
+ _valid_single_type?(@option, val)
45
+ end
46
+ end
47
+
48
+ def coerce_value(type, val)
49
+ # Don't coerce things other than nil to Arrays or Hashes
50
+ return val || [] if type == Array
51
+ return val || {} if type == Hash
52
+
53
+ converter = Virtus::Attribute.build(type)
54
+ converter.coerce(val)
55
+
56
+ # not the prettiest but some invalid coercion can currently trigger
57
+ # errors in Virtus (see coerce_spec.rb:75)
58
+ rescue
59
+ InvalidValue.new
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,25 @@
1
+ module Grape
2
+ module Validations
3
+ class DefaultValidator < Validator
4
+ def initialize(attrs, options, required, scope)
5
+ @default = options
6
+ super
7
+ end
8
+
9
+ def validate_param!(attr_name, params)
10
+ params[attr_name] = @default.is_a?(Proc) ? @default.call : @default unless params.key?(attr_name)
11
+ end
12
+
13
+ def validate!(params)
14
+ attrs = AttributesIterator.new(self, @scope, params)
15
+ parent_element = @scope.element
16
+ attrs.each do |resource_params, attr_name|
17
+ if resource_params[attr_name].nil?
18
+ validate_param!(attr_name, resource_params)
19
+ params[parent_element] = resource_params if parent_element && params[parent_element].nil?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ module Grape
2
+ module Validations
3
+ require 'grape/validations/mutual_exclusion'
4
+ class ExactlyOneOfValidator < MutualExclusionValidator
5
+ attr_reader :params
6
+
7
+ def validate!(params)
8
+ super
9
+ if none_of_restricted_params_is_present
10
+ raise Grape::Exceptions::Validation, param: "#{all_keys}", message_key: :exactly_one
11
+ end
12
+ params
13
+ end
14
+
15
+ private
16
+
17
+ def none_of_restricted_params_is_present
18
+ keys_in_common.length < 1
19
+ end
20
+
21
+ def all_keys
22
+ attrs.map(&:to_sym)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module Grape
2
+ module Validations
3
+ class MutualExclusionValidator < Validator
4
+ attr_reader :params
5
+
6
+ def validate!(params)
7
+ @params = params
8
+ if two_or_more_exclusive_params_are_present
9
+ raise Grape::Exceptions::Validation, param: "#{keys_in_common.map(&:to_sym)}", message_key: :mutual_exclusion
10
+ end
11
+ params
12
+ end
13
+
14
+ private
15
+
16
+ def two_or_more_exclusive_params_are_present
17
+ keys_in_common.length > 1
18
+ end
19
+
20
+ def keys_in_common
21
+ attrs.map(&:to_s) & params.stringify_keys.keys
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ module Grape
2
+ module Validations
3
+ class PresenceValidator < Validator
4
+ def validate!(params)
5
+ return unless @scope.should_validate?(params)
6
+ super
7
+ end
8
+
9
+ def validate_param!(attr_name, params)
10
+ unless params.respond_to?(:key?) && params.key?(attr_name)
11
+ raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message_key: :presence
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ module Grape
2
+ module Validations
3
+ class RegexpValidator < SingleOptionValidator
4
+ def validate_param!(attr_name, params)
5
+ if params.key?(attr_name) &&
6
+ (params[attr_name].nil? || !(params[attr_name].to_s =~ @option))
7
+ raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message_key: :regexp
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ module Grape
2
+ module Validations
3
+ class ValuesValidator < Validator
4
+ def initialize(attrs, options, required, scope)
5
+ @values = options
6
+ @required = required
7
+ super
8
+ end
9
+
10
+ def validate_param!(attr_name, params)
11
+ if (params[attr_name] || required_for_root_scope?) && !(@values.is_a?(Proc) ? @values.call : @values).include?(params[attr_name])
12
+ raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message_key: :values
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def required_for_root_scope?
19
+ @required && @scope.root?
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Grape
2
+ VERSION = '0.8.0'
3
+ end
@@ -0,0 +1,2571 @@
1
+ require 'spec_helper'
2
+ require 'shared/versioning_examples'
3
+
4
+ describe Grape::API do
5
+ subject { Class.new(Grape::API) }
6
+
7
+ def app
8
+ subject
9
+ end
10
+
11
+ describe '.prefix' do
12
+ it 'routes root through with the prefix' do
13
+ subject.prefix 'awesome/sauce'
14
+ subject.get do
15
+ "Hello there."
16
+ end
17
+
18
+ get 'awesome/sauce/'
19
+ expect(last_response.body).to eql "Hello there."
20
+ end
21
+
22
+ it 'routes through with the prefix' do
23
+ subject.prefix 'awesome/sauce'
24
+ subject.get :hello do
25
+ "Hello there."
26
+ end
27
+
28
+ get 'awesome/sauce/hello'
29
+ expect(last_response.body).to eql "Hello there."
30
+
31
+ get '/hello'
32
+ expect(last_response.status).to eql 404
33
+ end
34
+ end
35
+
36
+ describe '.version' do
37
+ context 'when defined' do
38
+ it 'returns version value' do
39
+ subject.version 'v1'
40
+ expect(subject.version).to eq('v1')
41
+ end
42
+ end
43
+
44
+ context 'when not defined' do
45
+ it 'returns nil' do
46
+ expect(subject.version).to be_nil
47
+ end
48
+ end
49
+ end
50
+
51
+ describe '.version using path' do
52
+ it_should_behave_like 'versioning' do
53
+ let(:macro_options) do
54
+ {
55
+ using: :path
56
+ }
57
+ end
58
+ end
59
+ end
60
+
61
+ describe '.version using param' do
62
+ it_should_behave_like 'versioning' do
63
+ let(:macro_options) do
64
+ {
65
+ using: :param,
66
+ parameter: "apiver"
67
+ }
68
+ end
69
+ end
70
+ end
71
+
72
+ describe '.version using header' do
73
+ it_should_behave_like 'versioning' do
74
+ let(:macro_options) do
75
+ {
76
+ using: :header,
77
+ vendor: 'mycompany',
78
+ format: 'json'
79
+ }
80
+ end
81
+ end
82
+
83
+ # Behavior as defined by rfc2616 when no header is defined
84
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
85
+ describe 'no specified accept header' do
86
+ # subject.version 'v1', using: :header
87
+ # subject.get '/hello' do
88
+ # 'hello'
89
+ # end
90
+
91
+ # it 'routes' do
92
+ # get '/hello'
93
+ # last_response.status.should eql 200
94
+ # end
95
+ end
96
+
97
+ # pending 'routes if any media type is allowed'
98
+ end
99
+
100
+ describe '.version using accept_version_header' do
101
+ it_should_behave_like 'versioning' do
102
+ let(:macro_options) do
103
+ {
104
+ using: :accept_version_header
105
+ }
106
+ end
107
+ end
108
+ end
109
+
110
+ describe '.represent' do
111
+ it 'requires a :with option' do
112
+ expect { subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent)
113
+ end
114
+
115
+ it 'adds the association to the :representations setting' do
116
+ klass = Class.new
117
+ subject.represent Object, with: klass
118
+ expect(subject.settings[:representations][Object]).to eq(klass)
119
+ end
120
+ end
121
+
122
+ describe '.namespace' do
123
+ it 'is retrievable and converted to a path' do
124
+ subject.namespace :awesome do
125
+ namespace.should == '/awesome'
126
+ end
127
+ end
128
+
129
+ it 'comes after the prefix and version' do
130
+ subject.prefix :rad
131
+ subject.version 'v1', using: :path
132
+
133
+ subject.namespace :awesome do
134
+ get('/hello') { "worked" }
135
+ end
136
+
137
+ get "/rad/v1/awesome/hello"
138
+ expect(last_response.body).to eq("worked")
139
+ end
140
+
141
+ it 'cancels itself after the block is over' do
142
+ subject.namespace :awesome do
143
+ namespace.should == '/awesome'
144
+ end
145
+
146
+ expect(subject.namespace).to eq('/')
147
+ end
148
+
149
+ it 'is stackable' do
150
+ subject.namespace :awesome do
151
+ namespace :rad do
152
+ namespace.should == '/awesome/rad'
153
+ end
154
+ namespace.should == '/awesome'
155
+ end
156
+ expect(subject.namespace).to eq('/')
157
+ end
158
+
159
+ it 'accepts path segments correctly' do
160
+ subject.namespace :members do
161
+ namespace '/:member_id' do
162
+ namespace.should == '/members/:member_id'
163
+ get '/' do
164
+ params[:member_id]
165
+ end
166
+ end
167
+ end
168
+ get '/members/23'
169
+ expect(last_response.body).to eq("23")
170
+ end
171
+
172
+ it 'is callable with nil just to push onto the stack' do
173
+ subject.namespace do
174
+ version 'v2', using: :path
175
+ get('/hello') { "inner" }
176
+ end
177
+ subject.get('/hello') { "outer" }
178
+
179
+ get '/v2/hello'
180
+ expect(last_response.body).to eq("inner")
181
+ get '/hello'
182
+ expect(last_response.body).to eq("outer")
183
+ end
184
+
185
+ %w(group resource resources segment).each do |als|
186
+ it '`.#{als}` is an alias' do
187
+ subject.send(als, :awesome) do
188
+ namespace.should == "/awesome"
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ describe '.route_param' do
195
+ it 'adds a parameterized route segment namespace' do
196
+ subject.namespace :users do
197
+ route_param :id do
198
+ get do
199
+ params[:id]
200
+ end
201
+ end
202
+ end
203
+
204
+ get '/users/23'
205
+ expect(last_response.body).to eq('23')
206
+ end
207
+
208
+ it 'should be able to define requirements with a single hash' do
209
+ subject.namespace :users do
210
+ route_param :id, requirements: /[0-9]+/ do
211
+ get do
212
+ params[:id]
213
+ end
214
+ end
215
+ end
216
+
217
+ get '/users/michael'
218
+ expect(last_response.status).to eq(404)
219
+ get '/users/23'
220
+ expect(last_response.status).to eq(200)
221
+ end
222
+ end
223
+
224
+ describe '.route' do
225
+ it 'allows for no path' do
226
+ subject.namespace :votes do
227
+ get do
228
+ "Votes"
229
+ end
230
+ post do
231
+ "Created a Vote"
232
+ end
233
+ end
234
+
235
+ get '/votes'
236
+ expect(last_response.body).to eql 'Votes'
237
+ post '/votes'
238
+ expect(last_response.body).to eql 'Created a Vote'
239
+ end
240
+
241
+ it 'handles empty calls' do
242
+ subject.get "/"
243
+ get "/"
244
+ expect(last_response.body).to eql ""
245
+ end
246
+
247
+ describe 'root routes should work with' do
248
+ before do
249
+ subject.format :txt
250
+ def subject.enable_root_route!
251
+ get("/") { "root" }
252
+ end
253
+ end
254
+
255
+ after do
256
+ expect(last_response.body).to eql "root"
257
+ end
258
+
259
+ describe 'path versioned APIs' do
260
+ before do
261
+ subject.version 'v1', using: :path
262
+ subject.enable_root_route!
263
+ end
264
+
265
+ it 'without a format' do
266
+ versioned_get "/", "v1", using: :path
267
+ end
268
+
269
+ it 'with a format' do
270
+ get "/v1/.json"
271
+ end
272
+ end
273
+
274
+ it 'header versioned APIs' do
275
+ subject.version 'v1', using: :header, vendor: 'test'
276
+ subject.enable_root_route!
277
+
278
+ versioned_get "/", "v1", using: :header, vendor: 'test'
279
+ end
280
+
281
+ it 'header versioned APIs with multiple headers' do
282
+ subject.version ['v1', 'v2'], using: :header, vendor: 'test'
283
+ subject.enable_root_route!
284
+
285
+ versioned_get "/", "v1", using: :header, vendor: 'test'
286
+ versioned_get "/", "v2", using: :header, vendor: 'test'
287
+ end
288
+
289
+ it 'param versioned APIs' do
290
+ subject.version 'v1', using: :param
291
+ subject.enable_root_route!
292
+
293
+ versioned_get "/", "v1", using: :param
294
+ end
295
+
296
+ it 'Accept-Version header versioned APIs' do
297
+ subject.version 'v1', using: :accept_version_header
298
+ subject.enable_root_route!
299
+
300
+ versioned_get "/", "v1", using: :accept_version_header
301
+ end
302
+
303
+ it 'unversioned APIs' do
304
+ subject.enable_root_route!
305
+
306
+ get "/"
307
+ end
308
+ end
309
+
310
+ it 'allows for multiple paths' do
311
+ subject.get(["/abc", "/def"]) do
312
+ "foo"
313
+ end
314
+
315
+ get '/abc'
316
+ expect(last_response.body).to eql 'foo'
317
+ get '/def'
318
+ expect(last_response.body).to eql 'foo'
319
+ end
320
+
321
+ context 'format' do
322
+ before(:each) do
323
+ subject.get("/abc") do
324
+ RSpec::Mocks::Mock.new(to_json: 'abc', to_txt: 'def')
325
+ end
326
+ end
327
+
328
+ it 'allows .json' do
329
+ get '/abc.json'
330
+ expect(last_response.status).to eq(200)
331
+ expect(last_response.body).to eql 'abc' # json-encoded symbol
332
+ end
333
+
334
+ it 'allows .txt' do
335
+ get '/abc.txt'
336
+ expect(last_response.status).to eq(200)
337
+ expect(last_response.body).to eql 'def' # raw text
338
+ end
339
+ end
340
+
341
+ it 'allows for format without corrupting a param' do
342
+ subject.get('/:id') do
343
+ { "id" => params[:id] }
344
+ end
345
+
346
+ get '/awesome.json'
347
+ expect(last_response.body).to eql '{"id":"awesome"}'
348
+ end
349
+
350
+ it 'allows for format in namespace with no path' do
351
+ subject.namespace :abc do
352
+ get do
353
+ ["json"]
354
+ end
355
+ end
356
+
357
+ get '/abc.json'
358
+ expect(last_response.body).to eql '["json"]'
359
+ end
360
+
361
+ it 'allows for multiple verbs' do
362
+ subject.route([:get, :post], '/abc') do
363
+ "hiya"
364
+ end
365
+
366
+ subject.endpoints.first.routes.each do |route|
367
+ expect(route.route_path).to eql '/abc(.:format)'
368
+ end
369
+
370
+ get '/abc'
371
+ expect(last_response.body).to eql 'hiya'
372
+ post '/abc'
373
+ expect(last_response.body).to eql 'hiya'
374
+ end
375
+
376
+ [:put, :post].each do |verb|
377
+ context verb do
378
+ ['string', :symbol, 1, -1.1, {}, [], true, false, nil].each do |object|
379
+ it "allows a(n) #{object.class} json object in params" do
380
+ subject.format :json
381
+ subject.send(verb) do
382
+ env['api.request.body']
383
+ end
384
+ send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json'
385
+ expect(last_response.status).to eq(verb == :post ? 201 : 200)
386
+ expect(last_response.body).to eql MultiJson.dump(object)
387
+ expect(last_request.params).to eql Hash.new
388
+ end
389
+ it "stores input in api.request.input" do
390
+ subject.format :json
391
+ subject.send(verb) do
392
+ env['api.request.input']
393
+ end
394
+ send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json'
395
+ expect(last_response.status).to eq(verb == :post ? 201 : 200)
396
+ expect(last_response.body).to eql MultiJson.dump(object).to_json
397
+ end
398
+ context "chunked transfer encoding" do
399
+ it "stores input in api.request.input" do
400
+ subject.format :json
401
+ subject.send(verb) do
402
+ env['api.request.input']
403
+ end
404
+ send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked', 'CONTENT_LENGTH' => nil
405
+ expect(last_response.status).to eq(verb == :post ? 201 : 200)
406
+ expect(last_response.body).to eql MultiJson.dump(object).to_json
407
+ end
408
+ end
409
+ end
410
+ end
411
+ end
412
+
413
+ it 'allows for multipart paths' do
414
+
415
+ subject.route([:get, :post], '/:id/first') do
416
+ "first"
417
+ end
418
+
419
+ subject.route([:get, :post], '/:id') do
420
+ "ola"
421
+ end
422
+ subject.route([:get, :post], '/:id/first/second') do
423
+ "second"
424
+ end
425
+
426
+ get '/1'
427
+ expect(last_response.body).to eql 'ola'
428
+ post '/1'
429
+ expect(last_response.body).to eql 'ola'
430
+ get '/1/first'
431
+ expect(last_response.body).to eql 'first'
432
+ post '/1/first'
433
+ expect(last_response.body).to eql 'first'
434
+ get '/1/first/second'
435
+ expect(last_response.body).to eql 'second'
436
+
437
+ end
438
+
439
+ it 'allows for :any as a verb' do
440
+ subject.route(:any, '/abc') do
441
+ "lol"
442
+ end
443
+
444
+ %w(get post put delete options patch).each do |m|
445
+ send(m, '/abc')
446
+ expect(last_response.body).to eql 'lol'
447
+ end
448
+ end
449
+
450
+ verbs = %w(post get head delete put options patch)
451
+ verbs.each do |verb|
452
+ it 'allows and properly constrain a #{verb.upcase} method' do
453
+ subject.send(verb, '/example') do
454
+ verb
455
+ end
456
+ send(verb, '/example')
457
+ expect(last_response.body).to eql verb == 'head' ? '' : verb
458
+ # Call it with a method other than the properly constrained one.
459
+ send(used_verb = verbs[(verbs.index(verb) + 2) % verbs.size], '/example')
460
+ expect(last_response.status).to eql used_verb == 'options' ? 204 : 405
461
+ end
462
+ end
463
+
464
+ it 'returns a 201 response code for POST by default' do
465
+ subject.post('example') do
466
+ "Created"
467
+ end
468
+
469
+ post '/example'
470
+ expect(last_response.status).to eql 201
471
+ expect(last_response.body).to eql 'Created'
472
+ end
473
+
474
+ it 'returns a 405 for an unsupported method with an X-Custom-Header' do
475
+ subject.before { header 'X-Custom-Header', 'foo' }
476
+ subject.get 'example' do
477
+ "example"
478
+ end
479
+ put '/example'
480
+ expect(last_response.status).to eql 405
481
+ expect(last_response.body).to eql ''
482
+ expect(last_response.headers['X-Custom-Header']).to eql 'foo'
483
+ end
484
+
485
+ specify '405 responses includes an Allow header specifying supported methods' do
486
+ subject.get 'example' do
487
+ "example"
488
+ end
489
+ subject.post 'example' do
490
+ "example"
491
+ end
492
+ put '/example'
493
+ expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, POST, HEAD'
494
+ end
495
+
496
+ specify '405 responses includes an Content-Type header' do
497
+ subject.get 'example' do
498
+ "example"
499
+ end
500
+ subject.post 'example' do
501
+ "example"
502
+ end
503
+ put '/example'
504
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
505
+ end
506
+
507
+ it 'adds an OPTIONS route that returns a 204, an Allow header and a X-Custom-Header' do
508
+ subject.before { header 'X-Custom-Header', 'foo' }
509
+ subject.get 'example' do
510
+ "example"
511
+ end
512
+ options '/example'
513
+ expect(last_response.status).to eql 204
514
+ expect(last_response.body).to eql ''
515
+ expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD'
516
+ expect(last_response.headers['X-Custom-Header']).to eql 'foo'
517
+ end
518
+
519
+ it 'allows HEAD on a GET request' do
520
+ subject.get 'example' do
521
+ "example"
522
+ end
523
+ head '/example'
524
+ expect(last_response.status).to eql 200
525
+ expect(last_response.body).to eql ''
526
+ end
527
+
528
+ it 'overwrites the default HEAD request' do
529
+ subject.head 'example' do
530
+ error! 'nothing to see here', 400
531
+ end
532
+ subject.get 'example' do
533
+ "example"
534
+ end
535
+ head '/example'
536
+ expect(last_response.status).to eql 400
537
+ end
538
+ end
539
+
540
+ context "do_not_route_head!" do
541
+ before :each do
542
+ subject.do_not_route_head!
543
+ subject.get 'example' do
544
+ "example"
545
+ end
546
+ end
547
+ it 'options does not contain HEAD' do
548
+ options '/example'
549
+ expect(last_response.status).to eql 204
550
+ expect(last_response.body).to eql ''
551
+ expect(last_response.headers['Allow']).to eql 'OPTIONS, GET'
552
+ end
553
+ it 'does not allow HEAD on a GET request' do
554
+ head '/example'
555
+ expect(last_response.status).to eql 405
556
+ end
557
+ end
558
+
559
+ context "do_not_route_options!" do
560
+ before :each do
561
+ subject.do_not_route_options!
562
+ subject.get 'example' do
563
+ "example"
564
+ end
565
+ end
566
+ it 'options does not exist' do
567
+ options '/example'
568
+ expect(last_response.status).to eql 405
569
+ end
570
+ end
571
+
572
+ describe 'filters' do
573
+ it 'adds a before filter' do
574
+ subject.before { @foo = 'first' }
575
+ subject.before { @bar = 'second' }
576
+ subject.get '/' do
577
+ "#{@foo} #{@bar}"
578
+ end
579
+
580
+ get '/'
581
+ expect(last_response.body).to eql 'first second'
582
+ end
583
+
584
+ it 'adds a before filter to current and child namespaces only' do
585
+ subject.get '/' do
586
+ "root - #{@foo}"
587
+ end
588
+ subject.namespace :blah do
589
+ before { @foo = 'foo' }
590
+ get '/' do
591
+ "blah - #{@foo}"
592
+ end
593
+
594
+ namespace :bar do
595
+ get '/' do
596
+ "blah - bar - #{@foo}"
597
+ end
598
+ end
599
+ end
600
+
601
+ get '/'
602
+ expect(last_response.body).to eql 'root - '
603
+ get '/blah'
604
+ expect(last_response.body).to eql 'blah - foo'
605
+ get '/blah/bar'
606
+ expect(last_response.body).to eql 'blah - bar - foo'
607
+ end
608
+
609
+ it 'adds a after_validation filter' do
610
+ subject.after_validation { @foo = "first #{params[:id] }:#{params[:id].class}" }
611
+ subject.after_validation { @bar = 'second' }
612
+ subject.params do
613
+ requires :id, type: Integer
614
+ end
615
+ subject.get '/' do
616
+ "#{@foo} #{@bar}"
617
+ end
618
+
619
+ get '/', id: "32"
620
+ expect(last_response.body).to eql 'first 32:Integer second'
621
+ end
622
+
623
+ it 'adds a after filter' do
624
+ m = double('after mock')
625
+ subject.after { m.do_something! }
626
+ subject.after { m.do_something! }
627
+ subject.get '/' do
628
+ @var ||= 'default'
629
+ end
630
+
631
+ expect(m).to receive(:do_something!).exactly(2).times
632
+ get '/'
633
+ expect(last_response.body).to eql 'default'
634
+ end
635
+
636
+ it 'calls all filters when validation passes' do
637
+ a = double('before mock')
638
+ b = double('before_validation mock')
639
+ c = double('after_validation mock')
640
+ d = double('after mock')
641
+
642
+ subject.params do
643
+ requires :id, type: Integer
644
+ end
645
+ subject.resource ':id' do
646
+ before { a.do_something! }
647
+ before_validation { b.do_something! }
648
+ after_validation { c.do_something! }
649
+ after { d.do_something! }
650
+ get do
651
+ 'got it'
652
+ end
653
+ end
654
+
655
+ expect(a).to receive(:do_something!).exactly(1).times
656
+ expect(b).to receive(:do_something!).exactly(1).times
657
+ expect(c).to receive(:do_something!).exactly(1).times
658
+ expect(d).to receive(:do_something!).exactly(1).times
659
+
660
+ get '/123'
661
+ expect(last_response.status).to eql 200
662
+ expect(last_response.body).to eql 'got it'
663
+ end
664
+
665
+ it 'calls only before filters when validation fails' do
666
+ a = double('before mock')
667
+ b = double('before_validation mock')
668
+ c = double('after_validation mock')
669
+ d = double('after mock')
670
+
671
+ subject.params do
672
+ requires :id, type: Integer
673
+ end
674
+ subject.resource ':id' do
675
+ before { a.do_something! }
676
+ before_validation { b.do_something! }
677
+ after_validation { c.do_something! }
678
+ after { d.do_something! }
679
+ get do
680
+ 'got it'
681
+ end
682
+ end
683
+
684
+ expect(a).to receive(:do_something!).exactly(1).times
685
+ expect(b).to receive(:do_something!).exactly(1).times
686
+ expect(c).to receive(:do_something!).exactly(0).times
687
+ expect(d).to receive(:do_something!).exactly(0).times
688
+
689
+ get '/abc'
690
+ expect(last_response.status).to eql 400
691
+ expect(last_response.body).to eql 'id is invalid'
692
+ end
693
+
694
+ it 'calls filters in the correct order' do
695
+ i = 0
696
+ a = double('before mock')
697
+ b = double('before_validation mock')
698
+ c = double('after_validation mock')
699
+ d = double('after mock')
700
+
701
+ subject.params do
702
+ requires :id, type: Integer
703
+ end
704
+ subject.resource ':id' do
705
+ before { a.here(i += 1) }
706
+ before_validation { b.here(i += 1) }
707
+ after_validation { c.here(i += 1) }
708
+ after { d.here(i += 1) }
709
+ get do
710
+ 'got it'
711
+ end
712
+ end
713
+
714
+ expect(a).to receive(:here).with(1).exactly(1).times
715
+ expect(b).to receive(:here).with(2).exactly(1).times
716
+ expect(c).to receive(:here).with(3).exactly(1).times
717
+ expect(d).to receive(:here).with(4).exactly(1).times
718
+
719
+ get '/123'
720
+ expect(last_response.status).to eql 200
721
+ expect(last_response.body).to eql 'got it'
722
+ end
723
+ end
724
+
725
+ context 'format' do
726
+ before do
727
+ subject.get("/foo") { "bar" }
728
+ end
729
+
730
+ it 'sets content type for txt format' do
731
+ get '/foo'
732
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
733
+ end
734
+
735
+ it 'sets content type for json' do
736
+ get '/foo.json'
737
+ expect(last_response.headers['Content-Type']).to eql 'application/json'
738
+ end
739
+
740
+ it 'sets content type for error' do
741
+ subject.get('/error') { error!('error in plain text', 500) }
742
+ get '/error'
743
+ expect(last_response.headers['Content-Type']).to eql 'text/plain'
744
+ end
745
+
746
+ it 'sets content type for error' do
747
+ subject.format :json
748
+ subject.get('/error') { error!('error in json', 500) }
749
+ get '/error.json'
750
+ expect(last_response.headers['Content-Type']).to eql 'application/json'
751
+ end
752
+
753
+ it 'sets content type for xml' do
754
+ subject.format :xml
755
+ subject.get('/error') { error!('error in xml', 500) }
756
+ get '/error.xml'
757
+ expect(last_response.headers['Content-Type']).to eql 'application/xml'
758
+ end
759
+
760
+ context 'with a custom content_type' do
761
+ before do
762
+ subject.content_type :custom, 'application/custom'
763
+ subject.formatter :custom, lambda { |object, env| "custom" }
764
+
765
+ subject.get('/custom') { 'bar' }
766
+ subject.get('/error') { error!('error in custom', 500) }
767
+ end
768
+
769
+ it 'sets content type' do
770
+ get '/custom.custom'
771
+ expect(last_response.headers['Content-Type']).to eql 'application/custom'
772
+ end
773
+
774
+ it 'sets content type for error' do
775
+ get '/error.custom'
776
+ expect(last_response.headers['Content-Type']).to eql 'application/custom'
777
+ end
778
+ end
779
+
780
+ context 'env["api.format"]' do
781
+ before do
782
+ subject.post "attachment" do
783
+ filename = params[:file][:filename]
784
+ content_type MIME::Types.type_for(filename)[0].to_s
785
+ env['api.format'] = :binary # there's no formatter for :binary, data will be returned "as is"
786
+ header "Content-Disposition", "attachment; filename*=UTF-8''#{URI.escape(filename)}"
787
+ params[:file][:tempfile].read
788
+ end
789
+ end
790
+
791
+ ['/attachment.png', 'attachment'].each do |url|
792
+ it "uploads and downloads a PNG file via #{url}" do
793
+ image_filename = "grape.png"
794
+ post url, file: Rack::Test::UploadedFile.new(image_filename, 'image/png', true)
795
+ last_response.status.should == 201
796
+ last_response.headers['Content-Type'].should == "image/png"
797
+ last_response.headers['Content-Disposition'].should == "attachment; filename*=UTF-8''grape.png"
798
+ File.open(image_filename, 'rb') do |io|
799
+ last_response.body.should eq io.read
800
+ end
801
+ end
802
+ end
803
+
804
+ it "uploads and downloads a Ruby file" do
805
+ filename = __FILE__
806
+ post "/attachment.rb", file: Rack::Test::UploadedFile.new(filename, 'application/x-ruby', true)
807
+ last_response.status.should == 201
808
+ last_response.headers['Content-Type'].should == "application/x-ruby"
809
+ last_response.headers['Content-Disposition'].should == "attachment; filename*=UTF-8''api_spec.rb"
810
+ File.open(filename, 'rb') do |io|
811
+ last_response.body.should eq io.read
812
+ end
813
+ end
814
+ end
815
+ end
816
+
817
+ context 'custom middleware' do
818
+ module ApiSpec
819
+ class PhonyMiddleware
820
+ def initialize(app, *args)
821
+ @args = args
822
+ @app = app
823
+ @block = true if block_given?
824
+ end
825
+
826
+ def call(env)
827
+ env['phony.args'] ||= []
828
+ env['phony.args'] << @args
829
+ env['phony.block'] = true if @block
830
+ @app.call(env)
831
+ end
832
+ end
833
+ end
834
+
835
+ describe '.middleware' do
836
+ it 'includes middleware arguments from settings' do
837
+ settings = Grape::Util::HashStack.new
838
+ allow(settings).to receive(:stack).and_return([{ middleware: [[ApiSpec::PhonyMiddleware, 'abc', 123]] }])
839
+ allow(subject).to receive(:settings).and_return(settings)
840
+ expect(subject.middleware).to eql [[ApiSpec::PhonyMiddleware, 'abc', 123]]
841
+ end
842
+
843
+ it 'includes all middleware from stacked settings' do
844
+ settings = Grape::Util::HashStack.new
845
+ allow(settings).to receive(:stack).and_return [
846
+ { middleware: [[ApiSpec::PhonyMiddleware, 123], [ApiSpec::PhonyMiddleware, 'abc']] },
847
+ { middleware: [[ApiSpec::PhonyMiddleware, 'foo']] }
848
+ ]
849
+
850
+ allow(subject).to receive(:settings).and_return(settings)
851
+
852
+ expect(subject.middleware).to eql [
853
+ [ApiSpec::PhonyMiddleware, 123],
854
+ [ApiSpec::PhonyMiddleware, 'abc'],
855
+ [ApiSpec::PhonyMiddleware, 'foo']
856
+ ]
857
+ end
858
+ end
859
+
860
+ describe '.use' do
861
+ it 'adds middleware' do
862
+ subject.use ApiSpec::PhonyMiddleware, 123
863
+ expect(subject.middleware).to eql [[ApiSpec::PhonyMiddleware, 123]]
864
+ end
865
+
866
+ it 'does not show up outside the namespace' do
867
+ subject.use ApiSpec::PhonyMiddleware, 123
868
+ subject.namespace :awesome do
869
+ use ApiSpec::PhonyMiddleware, 'abc'
870
+ middleware.should == [[ApiSpec::PhonyMiddleware, 123], [ApiSpec::PhonyMiddleware, 'abc']]
871
+ end
872
+
873
+ expect(subject.middleware).to eql [[ApiSpec::PhonyMiddleware, 123]]
874
+ end
875
+
876
+ it 'calls the middleware' do
877
+ subject.use ApiSpec::PhonyMiddleware, 'hello'
878
+ subject.get '/' do
879
+ env['phony.args'].first.first
880
+ end
881
+
882
+ get '/'
883
+ expect(last_response.body).to eql 'hello'
884
+ end
885
+
886
+ it 'adds a block if one is given' do
887
+ block = lambda {}
888
+ subject.use ApiSpec::PhonyMiddleware, &block
889
+ expect(subject.middleware).to eql [[ApiSpec::PhonyMiddleware, block]]
890
+ end
891
+
892
+ it 'uses a block if one is given' do
893
+ block = lambda {}
894
+ subject.use ApiSpec::PhonyMiddleware, &block
895
+ subject.get '/' do
896
+ env['phony.block'].inspect
897
+ end
898
+
899
+ get '/'
900
+ expect(last_response.body).to eq('true')
901
+ end
902
+
903
+ it 'does not destroy the middleware settings on multiple runs' do
904
+ block = lambda {}
905
+ subject.use ApiSpec::PhonyMiddleware, &block
906
+ subject.get '/' do
907
+ env['phony.block'].inspect
908
+ end
909
+
910
+ 2.times do
911
+ get '/'
912
+ expect(last_response.body).to eq('true')
913
+ end
914
+ end
915
+
916
+ it 'mounts behind error middleware' do
917
+ m = Class.new(Grape::Middleware::Base) do
918
+ def before
919
+ throw :error, message: "Caught in the Net", status: 400
920
+ end
921
+ end
922
+ subject.use m
923
+ subject.get "/" do
924
+ end
925
+ get "/"
926
+ expect(last_response.status).to eq(400)
927
+ expect(last_response.body).to eq("Caught in the Net")
928
+ end
929
+ end
930
+ end
931
+ describe '.http_basic' do
932
+ it 'protects any resources on the same scope' do
933
+ subject.http_basic do |u, p|
934
+ u == 'allow'
935
+ end
936
+ subject.get(:hello) { "Hello, world." }
937
+ get '/hello'
938
+ expect(last_response.status).to eql 401
939
+ get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
940
+ expect(last_response.status).to eql 200
941
+ end
942
+
943
+ it 'is scopable' do
944
+ subject.get(:hello) { "Hello, world." }
945
+ subject.namespace :admin do
946
+ http_basic do |u, p|
947
+ u == 'allow'
948
+ end
949
+
950
+ get(:hello) { "Hello, world." }
951
+ end
952
+
953
+ get '/hello'
954
+ expect(last_response.status).to eql 200
955
+ get '/admin/hello'
956
+ expect(last_response.status).to eql 401
957
+ end
958
+
959
+ it 'is callable via .auth as well' do
960
+ subject.auth :http_basic do |u, p|
961
+ u == 'allow'
962
+ end
963
+
964
+ subject.get(:hello) { "Hello, world." }
965
+ get '/hello'
966
+ expect(last_response.status).to eql 401
967
+ get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
968
+ expect(last_response.status).to eql 200
969
+ end
970
+
971
+ it 'has access to the current endpoint' do
972
+ basic_auth_context = nil
973
+
974
+ subject.http_basic do |u, p|
975
+ basic_auth_context = self
976
+
977
+ u == 'allow'
978
+ end
979
+
980
+ subject.get(:hello) { "Hello, world." }
981
+ get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
982
+ expect(basic_auth_context).to be_an_instance_of(Grape::Endpoint)
983
+ end
984
+
985
+ it 'has access to helper methods' do
986
+ subject.helpers do
987
+ def authorize(u, p)
988
+ u == 'allow' && p == 'whatever'
989
+ end
990
+ end
991
+
992
+ subject.http_basic do |u, p|
993
+ authorize(u, p)
994
+ end
995
+
996
+ subject.get(:hello) { "Hello, world." }
997
+ get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
998
+ expect(last_response.status).to eql 200
999
+ get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('disallow', 'whatever')
1000
+ expect(last_response.status).to eql 401
1001
+ end
1002
+
1003
+ it 'can set instance variables accessible to routes' do
1004
+ subject.http_basic do |u, p|
1005
+ @hello = "Hello, world."
1006
+
1007
+ u == 'allow'
1008
+ end
1009
+
1010
+ subject.get(:hello) { @hello }
1011
+ get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever')
1012
+ expect(last_response.status).to eql 200
1013
+ expect(last_response.body).to eql "Hello, world."
1014
+ end
1015
+ end
1016
+
1017
+ describe '.logger' do
1018
+ it 'returns an instance of Logger class by default' do
1019
+ expect(subject.logger.class).to eql Logger
1020
+ end
1021
+
1022
+ it 'allows setting a custom logger' do
1023
+ mylogger = Class.new
1024
+ subject.logger mylogger
1025
+ expect(mylogger).to receive(:info).exactly(1).times
1026
+ subject.logger.info "this will be logged"
1027
+ end
1028
+
1029
+ it "defaults to a standard logger log format" do
1030
+ t = Time.at(100)
1031
+ allow(Time).to receive(:now).and_return(t)
1032
+ expect(STDOUT).to receive(:write).with("I, [#{Logger::Formatter.new.send(:format_datetime, t)}\##{Process.pid}] INFO -- : this will be logged\n")
1033
+ subject.logger.info "this will be logged"
1034
+ end
1035
+ end
1036
+
1037
+ describe '.helpers' do
1038
+ it 'is accessible from the endpoint' do
1039
+ subject.helpers do
1040
+ def hello
1041
+ "Hello, world."
1042
+ end
1043
+ end
1044
+
1045
+ subject.get '/howdy' do
1046
+ hello
1047
+ end
1048
+
1049
+ get '/howdy'
1050
+ expect(last_response.body).to eql 'Hello, world.'
1051
+ end
1052
+
1053
+ it 'is scopable' do
1054
+ subject.helpers do
1055
+ def generic
1056
+ 'always there'
1057
+ end
1058
+ end
1059
+
1060
+ subject.namespace :admin do
1061
+ helpers do
1062
+ def secret
1063
+ 'only in admin'
1064
+ end
1065
+ end
1066
+
1067
+ get '/secret' do
1068
+ [generic, secret].join ':'
1069
+ end
1070
+ end
1071
+
1072
+ subject.get '/generic' do
1073
+ [generic, respond_to?(:secret)].join ':'
1074
+ end
1075
+
1076
+ get '/generic'
1077
+ expect(last_response.body).to eql 'always there:false'
1078
+ get '/admin/secret'
1079
+ expect(last_response.body).to eql 'always there:only in admin'
1080
+ end
1081
+
1082
+ it 'is reopenable' do
1083
+ subject.helpers do
1084
+ def one
1085
+ 1
1086
+ end
1087
+ end
1088
+
1089
+ subject.helpers do
1090
+ def two
1091
+ 2
1092
+ end
1093
+ end
1094
+
1095
+ subject.get 'howdy' do
1096
+ [one, two]
1097
+ end
1098
+
1099
+ expect { get '/howdy' }.not_to raise_error
1100
+ end
1101
+
1102
+ it 'allows for modules' do
1103
+ mod = Module.new do
1104
+ def hello
1105
+ "Hello, world."
1106
+ end
1107
+ end
1108
+ subject.helpers mod
1109
+
1110
+ subject.get '/howdy' do
1111
+ hello
1112
+ end
1113
+
1114
+ get '/howdy'
1115
+ expect(last_response.body).to eql 'Hello, world.'
1116
+ end
1117
+
1118
+ it 'allows multiple calls with modules and blocks' do
1119
+ subject.helpers Module.new do
1120
+ def one
1121
+ 1
1122
+ end
1123
+ end
1124
+ subject.helpers Module.new do
1125
+ def two
1126
+ 2
1127
+ end
1128
+ end
1129
+ subject.helpers do
1130
+ def three
1131
+ 3
1132
+ end
1133
+ end
1134
+ subject.get 'howdy' do
1135
+ [one, two, three]
1136
+ end
1137
+ expect { get '/howdy' }.not_to raise_error
1138
+ end
1139
+ end
1140
+
1141
+ describe '.scope' do
1142
+ # TODO: refactor this to not be tied to versioning. How about a generic
1143
+ # .setting macro?
1144
+ it 'scopes the various settings' do
1145
+ subject.prefix 'new'
1146
+
1147
+ subject.scope :legacy do
1148
+ prefix 'legacy'
1149
+ get '/abc' do
1150
+ 'abc'
1151
+ end
1152
+ end
1153
+
1154
+ subject.get '/def' do
1155
+ 'def'
1156
+ end
1157
+
1158
+ get '/new/abc'
1159
+ expect(last_response.status).to eql 404
1160
+ get '/legacy/abc'
1161
+ expect(last_response.status).to eql 200
1162
+ get '/legacy/def'
1163
+ expect(last_response.status).to eql 404
1164
+ get '/new/def'
1165
+ expect(last_response.status).to eql 200
1166
+ end
1167
+ end
1168
+
1169
+ describe '.rescue_from' do
1170
+ it 'does not rescue errors when rescue_from is not set' do
1171
+ subject.get '/exception' do
1172
+ raise "rain!"
1173
+ end
1174
+ expect { get '/exception' }.to raise_error
1175
+ end
1176
+
1177
+ it 'rescues all errors if rescue_from :all is called' do
1178
+ subject.rescue_from :all
1179
+ subject.get '/exception' do
1180
+ raise "rain!"
1181
+ end
1182
+ get '/exception'
1183
+ expect(last_response.status).to eql 500
1184
+ end
1185
+
1186
+ it 'rescues only certain errors if rescue_from is called with specific errors' do
1187
+ subject.rescue_from ArgumentError
1188
+ subject.get('/rescued') { raise ArgumentError }
1189
+ subject.get('/unrescued') { raise "beefcake" }
1190
+
1191
+ get '/rescued'
1192
+ expect(last_response.status).to eql 500
1193
+
1194
+ expect { get '/unrescued' }.to raise_error
1195
+ end
1196
+
1197
+ context 'CustomError subclass of Grape::Exceptions::Base' do
1198
+ before do
1199
+ class CustomError < Grape::Exceptions::Base; end
1200
+ end
1201
+
1202
+ it 'does not re-raise exceptions of type Grape::Exceptions::Base' do
1203
+ subject.get('/custom_exception') { raise CustomError }
1204
+
1205
+ expect { get '/custom_exception' }.not_to raise_error
1206
+ end
1207
+
1208
+ it 'rescues custom grape exceptions' do
1209
+ subject.rescue_from CustomError do |e|
1210
+ rack_response('New Error', e.status)
1211
+ end
1212
+ subject.get '/custom_error' do
1213
+ raise CustomError, status: 400, message: 'Custom Error'
1214
+ end
1215
+
1216
+ get '/custom_error'
1217
+ expect(last_response.status).to eq(400)
1218
+ expect(last_response.body).to eq('New Error')
1219
+ end
1220
+ end
1221
+
1222
+ it 'can rescue exceptions raised in the formatter' do
1223
+ formatter = double(:formatter)
1224
+ allow(formatter).to receive(:call) { raise StandardError }
1225
+ allow(Grape::Formatter::Base).to receive(:formatter_for) { formatter }
1226
+
1227
+ subject.rescue_from :all do |e|
1228
+ rack_response('Formatter Error', 500)
1229
+ end
1230
+ subject.get('/formatter_exception') { 'Hello world' }
1231
+
1232
+ get '/formatter_exception'
1233
+ expect(last_response.status).to eql 500
1234
+ expect(last_response.body).to eq('Formatter Error')
1235
+ end
1236
+ end
1237
+
1238
+ describe '.rescue_from klass, block' do
1239
+ it 'rescues Exception' do
1240
+ subject.rescue_from RuntimeError do |e|
1241
+ rack_response("rescued from #{e.message}", 202)
1242
+ end
1243
+ subject.get '/exception' do
1244
+ raise "rain!"
1245
+ end
1246
+ get '/exception'
1247
+ expect(last_response.status).to eql 202
1248
+ expect(last_response.body).to eq('rescued from rain!')
1249
+ end
1250
+
1251
+ context 'custom errors' do
1252
+ before do
1253
+ class ConnectionError < RuntimeError; end
1254
+ class DatabaseError < RuntimeError; end
1255
+ class CommunicationError < StandardError; end
1256
+ end
1257
+
1258
+ it 'rescues an error via rescue_from :all' do
1259
+ subject.rescue_from :all do |e|
1260
+ rack_response("rescued from #{e.class.name}", 500)
1261
+ end
1262
+ subject.get '/exception' do
1263
+ raise ConnectionError
1264
+ end
1265
+ get '/exception'
1266
+ expect(last_response.status).to eql 500
1267
+ expect(last_response.body).to eq('rescued from ConnectionError')
1268
+ end
1269
+ it 'rescues a specific error' do
1270
+ subject.rescue_from ConnectionError do |e|
1271
+ rack_response("rescued from #{e.class.name}", 500)
1272
+ end
1273
+ subject.get '/exception' do
1274
+ raise ConnectionError
1275
+ end
1276
+ get '/exception'
1277
+ expect(last_response.status).to eql 500
1278
+ expect(last_response.body).to eq('rescued from ConnectionError')
1279
+ end
1280
+ it 'rescues a subclass of an error by default' do
1281
+ subject.rescue_from RuntimeError do |e|
1282
+ rack_response("rescued from #{e.class.name}", 500)
1283
+ end
1284
+ subject.get '/exception' do
1285
+ raise ConnectionError
1286
+ end
1287
+ get '/exception'
1288
+ expect(last_response.status).to eql 500
1289
+ expect(last_response.body).to eq('rescued from ConnectionError')
1290
+ end
1291
+ it 'rescues multiple specific errors' do
1292
+ subject.rescue_from ConnectionError do |e|
1293
+ rack_response("rescued from #{e.class.name}", 500)
1294
+ end
1295
+ subject.rescue_from DatabaseError do |e|
1296
+ rack_response("rescued from #{e.class.name}", 500)
1297
+ end
1298
+ subject.get '/connection' do
1299
+ raise ConnectionError
1300
+ end
1301
+ subject.get '/database' do
1302
+ raise DatabaseError
1303
+ end
1304
+ get '/connection'
1305
+ expect(last_response.status).to eql 500
1306
+ expect(last_response.body).to eq('rescued from ConnectionError')
1307
+ get '/database'
1308
+ expect(last_response.status).to eql 500
1309
+ expect(last_response.body).to eq('rescued from DatabaseError')
1310
+ end
1311
+ it 'does not rescue a different error' do
1312
+ subject.rescue_from RuntimeError do |e|
1313
+ rack_response("rescued from #{e.class.name}", 500)
1314
+ end
1315
+ subject.get '/uncaught' do
1316
+ raise CommunicationError
1317
+ end
1318
+ expect { get '/uncaught' }.to raise_error(CommunicationError)
1319
+ end
1320
+ end
1321
+ end
1322
+
1323
+ describe '.rescue_from klass, lambda' do
1324
+ it 'rescues an error with the lambda' do
1325
+ subject.rescue_from ArgumentError, lambda {
1326
+ rack_response("rescued with a lambda", 400)
1327
+ }
1328
+ subject.get('/rescue_lambda') { raise ArgumentError }
1329
+
1330
+ get '/rescue_lambda'
1331
+ expect(last_response.status).to eq(400)
1332
+ expect(last_response.body).to eq("rescued with a lambda")
1333
+ end
1334
+
1335
+ it 'can execute the lambda with an argument' do
1336
+ subject.rescue_from ArgumentError, lambda { |e|
1337
+ rack_response(e.message, 400)
1338
+ }
1339
+ subject.get('/rescue_lambda') { raise ArgumentError, 'lambda takes an argument' }
1340
+
1341
+ get '/rescue_lambda'
1342
+ expect(last_response.status).to eq(400)
1343
+ expect(last_response.body).to eq('lambda takes an argument')
1344
+ end
1345
+ end
1346
+
1347
+ describe '.rescue_from klass, with: method' do
1348
+ it 'rescues an error with the specified message' do
1349
+ def rescue_arg_error
1350
+ Rack::Response.new('rescued with a method', 400)
1351
+ end
1352
+
1353
+ subject.rescue_from ArgumentError, with: rescue_arg_error
1354
+ subject.get('/rescue_method') { raise ArgumentError }
1355
+
1356
+ get '/rescue_method'
1357
+ expect(last_response.status).to eq(400)
1358
+ expect(last_response.body).to eq('rescued with a method')
1359
+ end
1360
+ end
1361
+
1362
+ describe '.rescue_from klass, rescue_subclasses: boolean' do
1363
+ before do
1364
+ module APIErrors
1365
+ class ParentError < StandardError; end
1366
+ class ChildError < ParentError; end
1367
+ end
1368
+ end
1369
+
1370
+ it 'rescues error as well as subclass errors with rescue_subclasses option set' do
1371
+ subject.rescue_from APIErrors::ParentError, rescue_subclasses: true do |e|
1372
+ rack_response("rescued from #{e.class.name}", 500)
1373
+ end
1374
+ subject.get '/caught_child' do
1375
+ raise APIErrors::ChildError
1376
+ end
1377
+ subject.get '/caught_parent' do
1378
+ raise APIErrors::ParentError
1379
+ end
1380
+ subject.get '/uncaught_parent' do
1381
+ raise StandardError
1382
+ end
1383
+
1384
+ get '/caught_child'
1385
+ expect(last_response.status).to eql 500
1386
+ get '/caught_parent'
1387
+ expect(last_response.status).to eql 500
1388
+ expect { get '/uncaught_parent' }.to raise_error(StandardError)
1389
+ end
1390
+
1391
+ it 'sets rescue_subclasses to true by default' do
1392
+ subject.rescue_from APIErrors::ParentError do |e|
1393
+ rack_response("rescued from #{e.class.name}", 500)
1394
+ end
1395
+ subject.get '/caught_child' do
1396
+ raise APIErrors::ChildError
1397
+ end
1398
+
1399
+ get '/caught_child'
1400
+ expect(last_response.status).to eql 500
1401
+ end
1402
+
1403
+ it 'does not rescue child errors if rescue_subclasses is false' do
1404
+ subject.rescue_from APIErrors::ParentError, rescue_subclasses: false do |e|
1405
+ rack_response("rescued from #{e.class.name}", 500)
1406
+ end
1407
+ subject.get '/uncaught' do
1408
+ raise APIErrors::ChildError
1409
+ end
1410
+ expect { get '/uncaught' }.to raise_error(APIErrors::ChildError)
1411
+ end
1412
+ end
1413
+
1414
+ describe '.error_format' do
1415
+ it 'rescues all errors and return :txt' do
1416
+ subject.rescue_from :all
1417
+ subject.format :txt
1418
+ subject.get '/exception' do
1419
+ raise "rain!"
1420
+ end
1421
+ get '/exception'
1422
+ expect(last_response.body).to eql "rain!"
1423
+ end
1424
+
1425
+ it 'rescues all errors and return :txt with backtrace' do
1426
+ subject.rescue_from :all, backtrace: true
1427
+ subject.format :txt
1428
+ subject.get '/exception' do
1429
+ raise "rain!"
1430
+ end
1431
+ get '/exception'
1432
+ expect(last_response.body.start_with?("rain!\r\n")).to be true
1433
+ end
1434
+
1435
+ it 'rescues all errors with a default formatter' do
1436
+ subject.default_format :foo
1437
+ subject.content_type :foo, "text/foo"
1438
+ subject.rescue_from :all
1439
+ subject.get '/exception' do
1440
+ raise "rain!"
1441
+ end
1442
+ get '/exception.foo'
1443
+ expect(last_response.body).to start_with "rain!"
1444
+ end
1445
+
1446
+ it 'defaults the error formatter to format' do
1447
+ subject.format :json
1448
+ subject.rescue_from :all
1449
+ subject.content_type :json, "application/json"
1450
+ subject.content_type :foo, "text/foo"
1451
+ subject.get '/exception' do
1452
+ raise "rain!"
1453
+ end
1454
+ get '/exception.json'
1455
+ expect(last_response.body).to eq('{"error":"rain!"}')
1456
+ get '/exception.foo'
1457
+ expect(last_response.body).to eq('{"error":"rain!"}')
1458
+ end
1459
+
1460
+ context 'class' do
1461
+ before :each do
1462
+ class CustomErrorFormatter
1463
+ def self.call(message, backtrace, options, env)
1464
+ "message: #{message} @backtrace"
1465
+ end
1466
+ end
1467
+ end
1468
+ it 'returns a custom error format' do
1469
+ subject.rescue_from :all, backtrace: true
1470
+ subject.error_formatter :txt, CustomErrorFormatter
1471
+ subject.get '/exception' do
1472
+ raise "rain!"
1473
+ end
1474
+ get '/exception'
1475
+ expect(last_response.body).to eq("message: rain! @backtrace")
1476
+ end
1477
+ end
1478
+
1479
+ describe 'with' do
1480
+ context 'class' do
1481
+ before :each do
1482
+ class CustomErrorFormatter
1483
+ def self.call(message, backtrace, option, env)
1484
+ "message: #{message} @backtrace"
1485
+ end
1486
+ end
1487
+ end
1488
+
1489
+ it 'returns a custom error format' do
1490
+ subject.rescue_from :all, backtrace: true
1491
+ subject.error_formatter :txt, with: CustomErrorFormatter
1492
+ subject.get('/exception') { raise "rain!" }
1493
+
1494
+ get '/exception'
1495
+ expect(last_response.body).to eq('message: rain! @backtrace')
1496
+ end
1497
+ end
1498
+ end
1499
+
1500
+ it 'rescues all errors and return :json' do
1501
+ subject.rescue_from :all
1502
+ subject.format :json
1503
+ subject.get '/exception' do
1504
+ raise "rain!"
1505
+ end
1506
+ get '/exception'
1507
+ expect(last_response.body).to eql '{"error":"rain!"}'
1508
+ end
1509
+ it 'rescues all errors and return :json with backtrace' do
1510
+ subject.rescue_from :all, backtrace: true
1511
+ subject.format :json
1512
+ subject.get '/exception' do
1513
+ raise "rain!"
1514
+ end
1515
+ get '/exception'
1516
+ json = MultiJson.load(last_response.body)
1517
+ expect(json["error"]).to eql 'rain!'
1518
+ expect(json["backtrace"].length).to be > 0
1519
+ end
1520
+ it 'rescues error! and return txt' do
1521
+ subject.format :txt
1522
+ subject.get '/error' do
1523
+ error!("Access Denied", 401)
1524
+ end
1525
+ get '/error'
1526
+ expect(last_response.body).to eql "Access Denied"
1527
+ end
1528
+ it 'rescues error! and return json' do
1529
+ subject.format :json
1530
+ subject.get '/error' do
1531
+ error!("Access Denied", 401)
1532
+ end
1533
+ get '/error'
1534
+ expect(last_response.body).to eql '{"error":"Access Denied"}'
1535
+ end
1536
+ end
1537
+
1538
+ describe '.content_type' do
1539
+ it 'sets additional content-type' do
1540
+ subject.content_type :xls, "application/vnd.ms-excel"
1541
+ subject.get :excel do
1542
+ "some binary content"
1543
+ end
1544
+ get '/excel.xls'
1545
+ expect(last_response.content_type).to eq("application/vnd.ms-excel")
1546
+ end
1547
+ it 'allows to override content-type' do
1548
+ subject.get :content do
1549
+ content_type "text/javascript"
1550
+ "var x = 1;"
1551
+ end
1552
+ get '/content'
1553
+ expect(last_response.content_type).to eq("text/javascript")
1554
+ end
1555
+ it 'removes existing content types' do
1556
+ subject.content_type :xls, "application/vnd.ms-excel"
1557
+ subject.get :excel do
1558
+ "some binary content"
1559
+ end
1560
+ get '/excel.json'
1561
+ expect(last_response.status).to eq(406)
1562
+ expect(last_response.body).to eq("The requested format 'txt' is not supported.")
1563
+ end
1564
+ end
1565
+
1566
+ describe '.formatter' do
1567
+ context 'multiple formatters' do
1568
+ before :each do
1569
+ subject.formatter :json, lambda { |object, env| "{\"custom_formatter\":\"#{object[:some] }\"}" }
1570
+ subject.formatter :txt, lambda { |object, env| "custom_formatter: #{object[:some] }" }
1571
+ subject.get :simple do
1572
+ { some: 'hash' }
1573
+ end
1574
+ end
1575
+ it 'sets one formatter' do
1576
+ get '/simple.json'
1577
+ expect(last_response.body).to eql '{"custom_formatter":"hash"}'
1578
+ end
1579
+ it 'sets another formatter' do
1580
+ get '/simple.txt'
1581
+ expect(last_response.body).to eql 'custom_formatter: hash'
1582
+ end
1583
+ end
1584
+ context 'custom formatter' do
1585
+ before :each do
1586
+ subject.content_type :json, 'application/json'
1587
+ subject.content_type :custom, 'application/custom'
1588
+ subject.formatter :custom, lambda { |object, env| "{\"custom_formatter\":\"#{object[:some] }\"}" }
1589
+ subject.get :simple do
1590
+ { some: 'hash' }
1591
+ end
1592
+ end
1593
+ it 'uses json' do
1594
+ get '/simple.json'
1595
+ expect(last_response.body).to eql '{"some":"hash"}'
1596
+ end
1597
+ it 'uses custom formatter' do
1598
+ get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom'
1599
+ expect(last_response.body).to eql '{"custom_formatter":"hash"}'
1600
+ end
1601
+ end
1602
+ context 'custom formatter class' do
1603
+ module CustomFormatter
1604
+ def self.call(object, env)
1605
+ "{\"custom_formatter\":\"#{object[:some] }\"}"
1606
+ end
1607
+ end
1608
+ before :each do
1609
+ subject.content_type :json, 'application/json'
1610
+ subject.content_type :custom, 'application/custom'
1611
+ subject.formatter :custom, CustomFormatter
1612
+ subject.get :simple do
1613
+ { some: 'hash' }
1614
+ end
1615
+ end
1616
+ it 'uses json' do
1617
+ get '/simple.json'
1618
+ expect(last_response.body).to eql '{"some":"hash"}'
1619
+ end
1620
+ it 'uses custom formatter' do
1621
+ get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom'
1622
+ expect(last_response.body).to eql '{"custom_formatter":"hash"}'
1623
+ end
1624
+ end
1625
+ end
1626
+
1627
+ describe '.parser' do
1628
+ it 'parses data in format requested by content-type' do
1629
+ subject.format :json
1630
+ subject.post '/data' do
1631
+ { x: params[:x] }
1632
+ end
1633
+ post "/data", '{"x":42}', 'CONTENT_TYPE' => 'application/json'
1634
+ expect(last_response.status).to eq(201)
1635
+ expect(last_response.body).to eq('{"x":42}')
1636
+ end
1637
+ context 'lambda parser' do
1638
+ before :each do
1639
+ subject.content_type :txt, "text/plain"
1640
+ subject.content_type :custom, "text/custom"
1641
+ subject.parser :custom, lambda { |object, env| { object.to_sym => object.to_s.reverse } }
1642
+ subject.put :simple do
1643
+ params[:simple]
1644
+ end
1645
+ end
1646
+ ["text/custom", "text/custom; charset=UTF-8"].each do |content_type|
1647
+ it "uses parser for #{content_type}" do
1648
+ put '/simple', "simple", "CONTENT_TYPE" => content_type
1649
+ expect(last_response.status).to eq(200)
1650
+ expect(last_response.body).to eql "elpmis"
1651
+ end
1652
+ end
1653
+ end
1654
+ context 'custom parser class' do
1655
+ module CustomParser
1656
+ def self.call(object, env)
1657
+ { object.to_sym => object.to_s.reverse }
1658
+ end
1659
+ end
1660
+ before :each do
1661
+ subject.content_type :txt, "text/plain"
1662
+ subject.content_type :custom, "text/custom"
1663
+ subject.parser :custom, CustomParser
1664
+ subject.put :simple do
1665
+ params[:simple]
1666
+ end
1667
+ end
1668
+ it 'uses custom parser' do
1669
+ put '/simple', "simple", "CONTENT_TYPE" => "text/custom"
1670
+ expect(last_response.status).to eq(200)
1671
+ expect(last_response.body).to eql "elpmis"
1672
+ end
1673
+ end
1674
+ context "multi_xml" do
1675
+ it "doesn't parse yaml" do
1676
+ subject.put :yaml do
1677
+ params[:tag]
1678
+ end
1679
+ put '/yaml', '<tag type="symbol">a123</tag>', "CONTENT_TYPE" => "application/xml"
1680
+ expect(last_response.status).to eq(400)
1681
+ expect(last_response.body).to eql 'Disallowed type attribute: "symbol"'
1682
+ end
1683
+ end
1684
+ context "none parser class" do
1685
+ before :each do
1686
+ subject.parser :json, nil
1687
+ subject.put "data" do
1688
+ "body: #{env['api.request.body'] }"
1689
+ end
1690
+ end
1691
+ it "does not parse data" do
1692
+ put '/data', 'not valid json', "CONTENT_TYPE" => "application/json"
1693
+ expect(last_response.status).to eq(200)
1694
+ expect(last_response.body).to eq("body: not valid json")
1695
+ end
1696
+ end
1697
+ end
1698
+
1699
+ describe '.default_format' do
1700
+ before :each do
1701
+ subject.format :json
1702
+ subject.default_format :json
1703
+ end
1704
+ it 'returns data in default format' do
1705
+ subject.get '/data' do
1706
+ { x: 42 }
1707
+ end
1708
+ get "/data"
1709
+ expect(last_response.status).to eq(200)
1710
+ expect(last_response.body).to eq('{"x":42}')
1711
+ end
1712
+ it 'parses data in default format' do
1713
+ subject.post '/data' do
1714
+ { x: params[:x] }
1715
+ end
1716
+ post "/data", '{"x":42}', "CONTENT_TYPE" => ""
1717
+ expect(last_response.status).to eq(201)
1718
+ expect(last_response.body).to eq('{"x":42}')
1719
+ end
1720
+ end
1721
+
1722
+ describe '.default_error_status' do
1723
+ it 'allows setting default_error_status' do
1724
+ subject.rescue_from :all
1725
+ subject.default_error_status 200
1726
+ subject.get '/exception' do
1727
+ raise "rain!"
1728
+ end
1729
+ get '/exception'
1730
+ expect(last_response.status).to eql 200
1731
+ end
1732
+ it 'has a default error status' do
1733
+ subject.rescue_from :all
1734
+ subject.get '/exception' do
1735
+ raise "rain!"
1736
+ end
1737
+ get '/exception'
1738
+ expect(last_response.status).to eql 500
1739
+ end
1740
+ it 'uses the default error status in error!' do
1741
+ subject.rescue_from :all
1742
+ subject.default_error_status 400
1743
+ subject.get '/exception' do
1744
+ error! "rain!"
1745
+ end
1746
+ get '/exception'
1747
+ expect(last_response.status).to eql 400
1748
+ end
1749
+ end
1750
+
1751
+ context 'routes' do
1752
+ describe 'empty api structure' do
1753
+ it 'returns an empty array of routes' do
1754
+ expect(subject.routes).to eq([])
1755
+ end
1756
+ end
1757
+ describe 'single method api structure' do
1758
+ before(:each) do
1759
+ subject.get :ping do
1760
+ 'pong'
1761
+ end
1762
+ end
1763
+ it 'returns one route' do
1764
+ expect(subject.routes.size).to eq(1)
1765
+ route = subject.routes[0]
1766
+ expect(route.route_version).to be_nil
1767
+ expect(route.route_path).to eq("/ping(.:format)")
1768
+ expect(route.route_method).to eq("GET")
1769
+ end
1770
+ end
1771
+ describe 'api structure with two versions and a namespace' do
1772
+ before :each do
1773
+ subject.version 'v1', using: :path
1774
+ subject.get 'version' do
1775
+ api.version
1776
+ end
1777
+ # version v2
1778
+ subject.version 'v2', using: :path
1779
+ subject.prefix 'p'
1780
+ subject.namespace 'n1' do
1781
+ namespace 'n2' do
1782
+ get 'version' do
1783
+ api.version
1784
+ end
1785
+ end
1786
+ end
1787
+ end
1788
+ it 'returns the latest version set' do
1789
+ expect(subject.version).to eq('v2')
1790
+ end
1791
+ it 'returns versions' do
1792
+ expect(subject.versions).to eq(['v1', 'v2'])
1793
+ end
1794
+ it 'sets route paths' do
1795
+ expect(subject.routes.size).to be >= 2
1796
+ expect(subject.routes[0].route_path).to eq("/:version/version(.:format)")
1797
+ expect(subject.routes[1].route_path).to eq("/p/:version/n1/n2/version(.:format)")
1798
+ end
1799
+ it 'sets route versions' do
1800
+ expect(subject.routes[0].route_version).to eq('v1')
1801
+ expect(subject.routes[1].route_version).to eq('v2')
1802
+ end
1803
+ it 'sets a nested namespace' do
1804
+ expect(subject.routes[1].route_namespace).to eq("/n1/n2")
1805
+ end
1806
+ it 'sets prefix' do
1807
+ expect(subject.routes[1].route_prefix).to eq('p')
1808
+ end
1809
+ end
1810
+ describe 'api structure with additional parameters' do
1811
+ before(:each) do
1812
+ subject.get 'split/:string', params: { "token" => "a token" }, optional_params: { "limit" => "the limit" } do
1813
+ params[:string].split(params[:token], (params[:limit] || 0).to_i)
1814
+ end
1815
+ end
1816
+ it 'splits a string' do
1817
+ get "/split/a,b,c.json", token: ','
1818
+ expect(last_response.body).to eq('["a","b","c"]')
1819
+ end
1820
+ it 'splits a string with limit' do
1821
+ get "/split/a,b,c.json", token: ',', limit: '2'
1822
+ expect(last_response.body).to eq('["a","b,c"]')
1823
+ end
1824
+ it 'sets route_params' do
1825
+ expect(subject.routes.map { |route|
1826
+ { params: route.route_params, optional_params: route.route_optional_params }
1827
+ }).to eq [
1828
+ { params: { "string" => "", "token" => "a token" }, optional_params: { "limit" => "the limit" } }
1829
+ ]
1830
+ end
1831
+ end
1832
+ end
1833
+
1834
+ context 'desc' do
1835
+ it 'empty array of routes' do
1836
+ expect(subject.routes).to eq([])
1837
+ end
1838
+ it 'empty array of routes' do
1839
+ subject.desc "grape api"
1840
+ expect(subject.routes).to eq([])
1841
+ end
1842
+ it 'describes a method' do
1843
+ subject.desc "first method"
1844
+ subject.get :first do ; end
1845
+ expect(subject.routes.length).to eq(1)
1846
+ route = subject.routes.first
1847
+ expect(route.route_description).to eq("first method")
1848
+ expect(route.route_foo).to be_nil
1849
+ expect(route.route_params).to eq({})
1850
+ end
1851
+ it 'describes methods separately' do
1852
+ subject.desc "first method"
1853
+ subject.get :first do ; end
1854
+ subject.desc "second method"
1855
+ subject.get :second do ; end
1856
+ expect(subject.routes.count).to eq(2)
1857
+ expect(subject.routes.map { |route|
1858
+ { description: route.route_description, params: route.route_params }
1859
+ }).to eq [
1860
+ { description: "first method", params: {} },
1861
+ { description: "second method", params: {} }
1862
+ ]
1863
+ end
1864
+ it 'resets desc' do
1865
+ subject.desc "first method"
1866
+ subject.get :first do ; end
1867
+ subject.get :second do ; end
1868
+ expect(subject.routes.map { |route|
1869
+ { description: route.route_description, params: route.route_params }
1870
+ }).to eq [
1871
+ { description: "first method", params: {} },
1872
+ { description: nil, params: {} }
1873
+ ]
1874
+ end
1875
+ it 'namespaces and describe arbitrary parameters' do
1876
+ subject.namespace 'ns' do
1877
+ desc "ns second", foo: "bar"
1878
+ get 'second' do ; end
1879
+ end
1880
+ expect(subject.routes.map { |route|
1881
+ { description: route.route_description, foo: route.route_foo, params: route.route_params }
1882
+ }).to eq [
1883
+ { description: "ns second", foo: "bar", params: {} }
1884
+ ]
1885
+ end
1886
+ it 'includes details' do
1887
+ subject.desc "method", details: "method details"
1888
+ subject.get 'method' do ; end
1889
+ expect(subject.routes.map { |route|
1890
+ { description: route.route_description, details: route.route_details, params: route.route_params }
1891
+ }).to eq [
1892
+ { description: "method", details: "method details", params: {} }
1893
+ ]
1894
+ end
1895
+ it 'describes a method with parameters' do
1896
+ subject.desc "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } }
1897
+ subject.get 'reverse' do
1898
+ params[:s].reverse
1899
+ end
1900
+ expect(subject.routes.map { |route|
1901
+ { description: route.route_description, params: route.route_params }
1902
+ }).to eq [
1903
+ { description: "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } } }
1904
+ ]
1905
+ end
1906
+ it 'merges the parameters of the namespace with the parameters of the method' do
1907
+ subject.desc "namespace"
1908
+ subject.params do
1909
+ requires :ns_param, desc: "namespace parameter"
1910
+ end
1911
+ subject.namespace 'ns' do
1912
+ desc "method"
1913
+ params do
1914
+ optional :method_param, desc: "method parameter"
1915
+ end
1916
+ get 'method' do ; end
1917
+ end
1918
+ expect(subject.routes.map { |route|
1919
+ { description: route.route_description, params: route.route_params }
1920
+ }).to eq [
1921
+ { description: "method",
1922
+ params: {
1923
+ "ns_param" => { required: true, desc: "namespace parameter" },
1924
+ "method_param" => { required: false, desc: "method parameter" }
1925
+ }
1926
+ }
1927
+ ]
1928
+ end
1929
+ it 'merges the parameters of nested namespaces' do
1930
+ subject.desc "ns1"
1931
+ subject.params do
1932
+ optional :ns_param, desc: "ns param 1"
1933
+ requires :ns1_param, desc: "ns1 param"
1934
+ end
1935
+ subject.namespace 'ns1' do
1936
+ desc "ns2"
1937
+ params do
1938
+ requires :ns_param, desc: "ns param 2"
1939
+ requires :ns2_param, desc: "ns2 param"
1940
+ end
1941
+ namespace 'ns2' do
1942
+ desc "method"
1943
+ params do
1944
+ optional :method_param, desc: "method param"
1945
+ end
1946
+ get 'method' do ; end
1947
+ end
1948
+ end
1949
+ expect(subject.routes.map { |route|
1950
+ { description: route.route_description, params: route.route_params }
1951
+ }).to eq [
1952
+ { description: "method",
1953
+ params: {
1954
+ "ns_param" => { required: true, desc: "ns param 2" },
1955
+ "ns1_param" => { required: true, desc: "ns1 param" },
1956
+ "ns2_param" => { required: true, desc: "ns2 param" },
1957
+ "method_param" => { required: false, desc: "method param" }
1958
+ }
1959
+ }
1960
+ ]
1961
+ end
1962
+ it "groups nested params and prevents overwriting of params with same name in different groups" do
1963
+ subject.desc "method"
1964
+ subject.params do
1965
+ group :group1 do
1966
+ optional :param1, desc: "group1 param1 desc"
1967
+ requires :param2, desc: "group1 param2 desc"
1968
+ end
1969
+ group :group2 do
1970
+ optional :param1, desc: "group2 param1 desc"
1971
+ requires :param2, desc: "group2 param2 desc"
1972
+ end
1973
+ end
1974
+ subject.get "method" do ; end
1975
+
1976
+ expect(subject.routes.map { |route|
1977
+ route.route_params
1978
+ }).to eq [{
1979
+ "group1" => { required: true, type: "Array" },
1980
+ "group1[param1]" => { required: false, desc: "group1 param1 desc" },
1981
+ "group1[param2]" => { required: true, desc: "group1 param2 desc" },
1982
+ "group2" => { required: true, type: "Array" },
1983
+ "group2[param1]" => { required: false, desc: "group2 param1 desc" },
1984
+ "group2[param2]" => { required: true, desc: "group2 param2 desc" }
1985
+ }]
1986
+ end
1987
+ it 'uses full name of parameters in nested groups' do
1988
+ subject.desc "nesting"
1989
+ subject.params do
1990
+ requires :root_param, desc: "root param"
1991
+ group :nested do
1992
+ requires :nested_param, desc: "nested param"
1993
+ end
1994
+ end
1995
+ subject.get 'method' do ; end
1996
+ expect(subject.routes.map { |route|
1997
+ { description: route.route_description, params: route.route_params }
1998
+ }).to eq [
1999
+ { description: "nesting",
2000
+ params: {
2001
+ "root_param" => { required: true, desc: "root param" },
2002
+ "nested" => { required: true, type: "Array" },
2003
+ "nested[nested_param]" => { required: true, desc: "nested param" }
2004
+ }
2005
+ }
2006
+ ]
2007
+ end
2008
+ it 'allows to set the type attribute on :group element' do
2009
+ subject.params do
2010
+ group :foo, type: Array do
2011
+ optional :bar
2012
+ end
2013
+ end
2014
+ end
2015
+ it 'parses parameters when no description is given' do
2016
+ subject.params do
2017
+ requires :one_param, desc: "one param"
2018
+ end
2019
+ subject.get 'method' do ; end
2020
+ expect(subject.routes.map { |route|
2021
+ { description: route.route_description, params: route.route_params }
2022
+ }).to eq [
2023
+ { description: nil, params: { "one_param" => { required: true, desc: "one param" } } }
2024
+ ]
2025
+ end
2026
+ it 'does not symbolize params' do
2027
+ subject.desc "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } }
2028
+ subject.get 'reverse/:s' do
2029
+ params[:s].reverse
2030
+ end
2031
+ expect(subject.routes.map { |route|
2032
+ { description: route.route_description, params: route.route_params }
2033
+ }).to eq [
2034
+ { description: "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } } }
2035
+ ]
2036
+ end
2037
+ end
2038
+
2039
+ describe '.mount' do
2040
+ let(:mounted_app) { lambda { |env| [200, {}, ["MOUNTED"]] } }
2041
+
2042
+ context 'with a bare rack app' do
2043
+ before do
2044
+ subject.mount mounted_app => '/mounty'
2045
+ end
2046
+
2047
+ it 'makes a bare Rack app available at the endpoint' do
2048
+ get '/mounty'
2049
+ expect(last_response.body).to eq('MOUNTED')
2050
+ end
2051
+
2052
+ it 'anchors the routes, passing all subroutes to it' do
2053
+ get '/mounty/awesome'
2054
+ expect(last_response.body).to eq('MOUNTED')
2055
+ end
2056
+
2057
+ it 'is able to cascade' do
2058
+ subject.mount lambda { |env|
2059
+ headers = {}
2060
+ headers['X-Cascade'] == 'pass' unless env['PATH_INFO'].include?('boo')
2061
+ [200, headers, ["Farfegnugen"]]
2062
+ } => '/'
2063
+
2064
+ get '/boo'
2065
+ expect(last_response.body).to eq('Farfegnugen')
2066
+ get '/mounty'
2067
+ expect(last_response.body).to eq('MOUNTED')
2068
+ end
2069
+ end
2070
+
2071
+ context 'without a hash' do
2072
+ it 'calls through setting the route to "/"' do
2073
+ subject.mount mounted_app
2074
+ get '/'
2075
+ expect(last_response.body).to eq('MOUNTED')
2076
+ end
2077
+ end
2078
+
2079
+ context 'mounting an API' do
2080
+ it 'applies the settings of the mounting api' do
2081
+ subject.version 'v1', using: :path
2082
+
2083
+ subject.namespace :cool do
2084
+ app = Class.new(Grape::API)
2085
+ app.get('/awesome') do
2086
+ "yo"
2087
+ end
2088
+
2089
+ mount app
2090
+ end
2091
+
2092
+ get '/v1/cool/awesome'
2093
+ expect(last_response.body).to eq('yo')
2094
+ end
2095
+
2096
+ it 'applies the settings to nested mounted apis' do
2097
+ subject.version 'v1', using: :path
2098
+
2099
+ subject.namespace :cool do
2100
+ inner_app = Class.new(Grape::API)
2101
+ inner_app.get('/awesome') do
2102
+ "yo"
2103
+ end
2104
+
2105
+ app = Class.new(Grape::API)
2106
+ app.mount inner_app
2107
+ mount app
2108
+ end
2109
+
2110
+ get '/v1/cool/awesome'
2111
+ expect(last_response.body).to eq('yo')
2112
+ end
2113
+
2114
+ it 'inherits rescues even when some defined by mounted' do
2115
+ subject.rescue_from :all do |e|
2116
+ rack_response("rescued from #{e.message}", 202)
2117
+ end
2118
+ subject.namespace :mounted do
2119
+ app = Class.new(Grape::API)
2120
+ app.rescue_from ArgumentError
2121
+ app.get('/fail') { raise "doh!" }
2122
+ mount app
2123
+ end
2124
+ get '/mounted/fail'
2125
+ expect(last_response.status).to eql 202
2126
+ expect(last_response.body).to eq('rescued from doh!')
2127
+ end
2128
+
2129
+ it 'collects the routes of the mounted api' do
2130
+ subject.namespace :cool do
2131
+ app = Class.new(Grape::API)
2132
+ app.get('/awesome') {}
2133
+ app.post('/sauce') {}
2134
+ mount app
2135
+ end
2136
+ expect(subject.routes.size).to eq(2)
2137
+ expect(subject.routes.first.route_path).to match(%r{\/cool\/awesome})
2138
+ expect(subject.routes.last.route_path).to match(%r{\/cool\/sauce})
2139
+ end
2140
+
2141
+ it 'mounts on a path' do
2142
+ subject.namespace :cool do
2143
+ app = Class.new(Grape::API)
2144
+ app.get '/awesome' do
2145
+ "sauce"
2146
+ end
2147
+ mount app => '/mounted'
2148
+ end
2149
+ get "/mounted/cool/awesome"
2150
+ expect(last_response.status).to eq(200)
2151
+ expect(last_response.body).to eq("sauce")
2152
+ end
2153
+
2154
+ it 'mounts on a nested path' do
2155
+ app1 = Class.new(Grape::API)
2156
+ app2 = Class.new(Grape::API)
2157
+ app2.get '/nice' do
2158
+ "play"
2159
+ end
2160
+ # note that the reverse won't work, mount from outside-in
2161
+ subject.mount app1 => '/app1'
2162
+ app1.mount app2 => '/app2'
2163
+ get "/app1/app2/nice"
2164
+ expect(last_response.status).to eq(200)
2165
+ expect(last_response.body).to eq("play")
2166
+ options "/app1/app2/nice"
2167
+ expect(last_response.status).to eq(204)
2168
+ end
2169
+
2170
+ it 'responds to options' do
2171
+ app = Class.new(Grape::API)
2172
+ app.get '/colour' do
2173
+ 'red'
2174
+ end
2175
+ app.namespace :pears do
2176
+ get '/colour' do
2177
+ 'green'
2178
+ end
2179
+ end
2180
+ subject.namespace :apples do
2181
+ mount app
2182
+ end
2183
+ get '/apples/colour'
2184
+ expect(last_response.status).to eql 200
2185
+ expect(last_response.body).to eq('red')
2186
+ options '/apples/colour'
2187
+ expect(last_response.status).to eql 204
2188
+ get '/apples/pears/colour'
2189
+ expect(last_response.status).to eql 200
2190
+ expect(last_response.body).to eq('green')
2191
+ options '/apples/pears/colour'
2192
+ expect(last_response.status).to eql 204
2193
+ end
2194
+
2195
+ it 'responds to options with path versioning' do
2196
+ subject.version 'v1', using: :path
2197
+ subject.namespace :apples do
2198
+ app = Class.new(Grape::API)
2199
+ app.get('/colour') do
2200
+ "red"
2201
+ end
2202
+ mount app
2203
+ end
2204
+
2205
+ get '/v1/apples/colour'
2206
+ expect(last_response.status).to eql 200
2207
+ expect(last_response.body).to eq('red')
2208
+ options '/v1/apples/colour'
2209
+ expect(last_response.status).to eql 204
2210
+ end
2211
+
2212
+ end
2213
+ end
2214
+
2215
+ describe '.endpoints' do
2216
+ it 'adds one for each route created' do
2217
+ subject.get '/'
2218
+ subject.post '/'
2219
+ expect(subject.endpoints.size).to eq(2)
2220
+ end
2221
+ end
2222
+
2223
+ describe '.compile' do
2224
+ it 'sets the instance' do
2225
+ expect(subject.instance).to be_nil
2226
+ subject.compile
2227
+ expect(subject.instance).to be_kind_of(subject)
2228
+ end
2229
+ end
2230
+
2231
+ describe '.change!' do
2232
+ it 'invalidates any compiled instance' do
2233
+ subject.compile
2234
+ subject.change!
2235
+ expect(subject.instance).to be_nil
2236
+ end
2237
+ end
2238
+
2239
+ describe ".endpoint" do
2240
+ before(:each) do
2241
+ subject.format :json
2242
+ subject.get '/endpoint/options' do
2243
+ {
2244
+ path: options[:path],
2245
+ source_location: source.source_location
2246
+ }
2247
+ end
2248
+ end
2249
+ it 'path' do
2250
+ get '/endpoint/options'
2251
+ options = MultiJson.load(last_response.body)
2252
+ expect(options["path"]).to eq(["/endpoint/options"])
2253
+ expect(options["source_location"][0]).to include "api_spec.rb"
2254
+ expect(options["source_location"][1].to_i).to be > 0
2255
+ end
2256
+ end
2257
+
2258
+ describe '.route' do
2259
+ context 'plain' do
2260
+ before(:each) do
2261
+ subject.get '/' do
2262
+ route.route_path
2263
+ end
2264
+ subject.get '/path' do
2265
+ route.route_path
2266
+ end
2267
+ end
2268
+ it 'provides access to route info' do
2269
+ get '/'
2270
+ expect(last_response.body).to eq("/(.:format)")
2271
+ get '/path'
2272
+ expect(last_response.body).to eq("/path(.:format)")
2273
+ end
2274
+ end
2275
+ context 'with desc' do
2276
+ before(:each) do
2277
+ subject.desc 'returns description'
2278
+ subject.get '/description' do
2279
+ route.route_description
2280
+ end
2281
+ subject.desc 'returns parameters', params: { "x" => "y" }
2282
+ subject.get '/params/:id' do
2283
+ route.route_params[params[:id]]
2284
+ end
2285
+ end
2286
+ it 'returns route description' do
2287
+ get '/description'
2288
+ expect(last_response.body).to eq("returns description")
2289
+ end
2290
+ it 'returns route parameters' do
2291
+ get '/params/x'
2292
+ expect(last_response.body).to eq("y")
2293
+ end
2294
+ end
2295
+ end
2296
+ describe '.format' do
2297
+ context ':txt' do
2298
+ before(:each) do
2299
+ subject.format :txt
2300
+ subject.content_type :json, "application/json"
2301
+ subject.get '/meaning_of_life' do
2302
+ { meaning_of_life: 42 }
2303
+ end
2304
+ end
2305
+ it 'forces txt without an extension' do
2306
+ get '/meaning_of_life'
2307
+ expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
2308
+ end
2309
+ it 'does not force txt with an extension' do
2310
+ get '/meaning_of_life.json'
2311
+ expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json)
2312
+ end
2313
+ it 'forces txt from a non-accepting header' do
2314
+ get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json'
2315
+ expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
2316
+ end
2317
+ end
2318
+ context ':txt only' do
2319
+ before(:each) do
2320
+ subject.format :txt
2321
+ subject.get '/meaning_of_life' do
2322
+ { meaning_of_life: 42 }
2323
+ end
2324
+ end
2325
+ it 'forces txt without an extension' do
2326
+ get '/meaning_of_life'
2327
+ expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
2328
+ end
2329
+ it 'forces txt with the wrong extension' do
2330
+ get '/meaning_of_life.json'
2331
+ expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
2332
+ end
2333
+ it 'forces txt from a non-accepting header' do
2334
+ get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json'
2335
+ expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
2336
+ end
2337
+ end
2338
+ context ':json' do
2339
+ before(:each) do
2340
+ subject.format :json
2341
+ subject.content_type :txt, "text/plain"
2342
+ subject.get '/meaning_of_life' do
2343
+ { meaning_of_life: 42 }
2344
+ end
2345
+ end
2346
+ it 'forces json without an extension' do
2347
+ get '/meaning_of_life'
2348
+ expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json)
2349
+ end
2350
+ it 'does not force json with an extension' do
2351
+ get '/meaning_of_life.txt'
2352
+ expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
2353
+ end
2354
+ it 'forces json from a non-accepting header' do
2355
+ get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html'
2356
+ expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json)
2357
+ end
2358
+ it 'can be overwritten with an explicit content type' do
2359
+ subject.get '/meaning_of_life_with_content_type' do
2360
+ content_type "text/plain"
2361
+ { meaning_of_life: 42 }.to_s
2362
+ end
2363
+ get '/meaning_of_life_with_content_type'
2364
+ expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s)
2365
+ end
2366
+ it 'raised :error from middleware' do
2367
+ middleware = Class.new(Grape::Middleware::Base) do
2368
+ def before
2369
+ throw :error, message: "Unauthorized", status: 42
2370
+ end
2371
+ end
2372
+ subject.use middleware
2373
+ subject.get do
2374
+
2375
+ end
2376
+ get "/"
2377
+ expect(last_response.status).to eq(42)
2378
+ expect(last_response.body).to eq({ error: "Unauthorized" }.to_json)
2379
+ end
2380
+
2381
+ end
2382
+ context ':serializable_hash' do
2383
+ before(:each) do
2384
+ class SerializableHashExample
2385
+ def serializable_hash
2386
+ { abc: 'def' }
2387
+ end
2388
+ end
2389
+ subject.format :serializable_hash
2390
+ end
2391
+ it 'instance' do
2392
+ subject.get '/example' do
2393
+ SerializableHashExample.new
2394
+ end
2395
+ get '/example'
2396
+ expect(last_response.body).to eq('{"abc":"def"}')
2397
+ end
2398
+ it 'root' do
2399
+ subject.get '/example' do
2400
+ { "root" => SerializableHashExample.new }
2401
+ end
2402
+ get '/example'
2403
+ expect(last_response.body).to eq('{"root":{"abc":"def"}}')
2404
+ end
2405
+ it 'array' do
2406
+ subject.get '/examples' do
2407
+ [SerializableHashExample.new, SerializableHashExample.new]
2408
+ end
2409
+ get '/examples'
2410
+ expect(last_response.body).to eq('[{"abc":"def"},{"abc":"def"}]')
2411
+ end
2412
+ end
2413
+ context ":xml" do
2414
+ before(:each) do
2415
+ subject.format :xml
2416
+ end
2417
+ it 'string' do
2418
+ subject.get "/example" do
2419
+ "example"
2420
+ end
2421
+ get '/example'
2422
+ expect(last_response.status).to eq(500)
2423
+ expect(last_response.body).to eq <<-XML
2424
+ <?xml version="1.0" encoding="UTF-8"?>
2425
+ <error>
2426
+ <message>cannot convert String to xml</message>
2427
+ </error>
2428
+ XML
2429
+ end
2430
+ it 'hash' do
2431
+ subject.get "/example" do
2432
+ ActiveSupport::OrderedHash[
2433
+ :example1, "example1",
2434
+ :example2, "example2"
2435
+ ]
2436
+ end
2437
+ get '/example'
2438
+ expect(last_response.status).to eq(200)
2439
+ expect(last_response.body).to eq <<-XML
2440
+ <?xml version="1.0" encoding="UTF-8"?>
2441
+ <hash>
2442
+ <example1>example1</example1>
2443
+ <example2>example2</example2>
2444
+ </hash>
2445
+ XML
2446
+ end
2447
+ it 'array' do
2448
+ subject.get "/example" do
2449
+ ["example1", "example2"]
2450
+ end
2451
+ get '/example'
2452
+ expect(last_response.status).to eq(200)
2453
+ expect(last_response.body).to eq <<-XML
2454
+ <?xml version="1.0" encoding="UTF-8"?>
2455
+ <strings type="array">
2456
+ <string>example1</string>
2457
+ <string>example2</string>
2458
+ </strings>
2459
+ XML
2460
+ end
2461
+ it 'raised :error from middleware' do
2462
+ middleware = Class.new(Grape::Middleware::Base) do
2463
+ def before
2464
+ throw :error, message: "Unauthorized", status: 42
2465
+ end
2466
+ end
2467
+ subject.use middleware
2468
+ subject.get do
2469
+
2470
+ end
2471
+ get "/"
2472
+ expect(last_response.status).to eq(42)
2473
+ expect(last_response.body).to eq <<-XML
2474
+ <?xml version="1.0" encoding="UTF-8"?>
2475
+ <error>
2476
+ <message>Unauthorized</message>
2477
+ </error>
2478
+ XML
2479
+ end
2480
+ end
2481
+ end
2482
+
2483
+ context "catch-all" do
2484
+ before do
2485
+ api1 = Class.new(Grape::API)
2486
+ api1.version 'v1', using: :path
2487
+ api1.get "hello" do
2488
+ "v1"
2489
+ end
2490
+ api2 = Class.new(Grape::API)
2491
+ api2.version 'v2', using: :path
2492
+ api2.get "hello" do
2493
+ "v2"
2494
+ end
2495
+ subject.mount api1
2496
+ subject.mount api2
2497
+ end
2498
+ [true, false].each do |anchor|
2499
+ it "anchor=#{anchor}" do
2500
+ subject.route :any, '*path', anchor: anchor do
2501
+ error!("Unrecognized request path: #{params[:path] } - #{env['PATH_INFO'] }#{env['SCRIPT_NAME'] }", 404)
2502
+ end
2503
+ get "/v1/hello"
2504
+ expect(last_response.status).to eq(200)
2505
+ expect(last_response.body).to eq("v1")
2506
+ get "/v2/hello"
2507
+ expect(last_response.status).to eq(200)
2508
+ expect(last_response.body).to eq("v2")
2509
+ get "/foobar"
2510
+ expect(last_response.status).to eq(404)
2511
+ expect(last_response.body).to eq("Unrecognized request path: foobar - /foobar")
2512
+ end
2513
+ end
2514
+ end
2515
+
2516
+ context "cascading" do
2517
+ context "via version" do
2518
+ it "cascades" do
2519
+ subject.version 'v1', using: :path, cascade: true
2520
+ get "/v1/hello"
2521
+ expect(last_response.status).to eq(404)
2522
+ expect(last_response.headers["X-Cascade"]).to eq("pass")
2523
+ end
2524
+ it "does not cascade" do
2525
+ subject.version 'v2', using: :path, cascade: false
2526
+ get "/v2/hello"
2527
+ expect(last_response.status).to eq(404)
2528
+ expect(last_response.headers.keys).not_to include "X-Cascade"
2529
+ end
2530
+ end
2531
+ context "via endpoint" do
2532
+ it "cascades" do
2533
+ subject.cascade true
2534
+ get "/hello"
2535
+ expect(last_response.status).to eq(404)
2536
+ expect(last_response.headers["X-Cascade"]).to eq("pass")
2537
+ end
2538
+ it "does not cascade" do
2539
+ subject.cascade false
2540
+ get "/hello"
2541
+ expect(last_response.status).to eq(404)
2542
+ expect(last_response.headers.keys).not_to include "X-Cascade"
2543
+ end
2544
+ end
2545
+ end
2546
+
2547
+ context 'with json default_error_formatter' do
2548
+ it 'returns json error' do
2549
+ subject.content_type :json, "application/json"
2550
+ subject.default_error_formatter :json
2551
+ subject.get '/something' do
2552
+ 'foo'
2553
+ end
2554
+ get '/something'
2555
+ expect(last_response.status).to eq(406)
2556
+ expect(last_response.body).to eq("{\"error\":\"The requested format &#39;txt&#39; is not supported.\"}")
2557
+ end
2558
+ end
2559
+
2560
+ context 'with unsafe HTML format specified' do
2561
+ it 'escapes the HTML' do
2562
+ subject.content_type :json, 'application/json'
2563
+ subject.get '/something' do
2564
+ 'foo'
2565
+ end
2566
+ get '/something?format=<script>blah</script>'
2567
+ expect(last_response.status).to eq(406)
2568
+ expect(last_response.body).to eq('The requested format &#39;&lt;script&gt;blah&lt;/script&gt;&#39; is not supported.')
2569
+ end
2570
+ end
2571
+ end