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