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