grape-security 0.8.0

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