grape-security 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,258 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Middleware::Formatter do
|
4
|
+
subject { Grape::Middleware::Formatter.new(app) }
|
5
|
+
before { allow(subject).to receive(:dup).and_return(subject) }
|
6
|
+
|
7
|
+
let(:app) { lambda { |env| [200, {}, [@body || { "foo" => "bar" }]] } }
|
8
|
+
|
9
|
+
context 'serialization' do
|
10
|
+
it 'looks at the bodies for possibly serializable data' do
|
11
|
+
@body = { "abc" => "def" }
|
12
|
+
_, _, bodies = *subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json')
|
13
|
+
bodies.each { |b| expect(b).to eq(MultiJson.dump(@body)) }
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'calls #to_json since default format is json' do
|
17
|
+
@body = ['foo']
|
18
|
+
@body.instance_eval do
|
19
|
+
def to_json
|
20
|
+
"\"bar\""
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json').last.each { |b| expect(b).to eq('"bar"') }
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'calls #to_json if the content type is jsonapi' do
|
28
|
+
@body = { 'foos' => [{ 'bar' => 'baz' }] }
|
29
|
+
@body.instance_eval do
|
30
|
+
def to_json
|
31
|
+
"{\"foos\":[{\"bar\":\"baz\"}] }"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/vnd.api+json').last.each { |b| expect(b).to eq('{"foos":[{"bar":"baz"}] }') }
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'calls #to_xml if the content type is xml' do
|
39
|
+
@body = "string"
|
40
|
+
@body.instance_eval do
|
41
|
+
def to_xml
|
42
|
+
"<bar/>"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json').last.each { |b| expect(b).to eq('<bar/>') }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'error handling' do
|
51
|
+
let(:formatter) { double(:formatter) }
|
52
|
+
before do
|
53
|
+
allow(Grape::Formatter::Base).to receive(:formatter_for) { formatter }
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'rescues formatter-specific exceptions' do
|
57
|
+
allow(formatter).to receive(:call) { raise Grape::Exceptions::InvalidFormatter.new(String, 'xml') }
|
58
|
+
|
59
|
+
expect {
|
60
|
+
catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') }
|
61
|
+
}.to_not raise_error
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'does not rescue other exceptions' do
|
65
|
+
allow(formatter).to receive(:call) { raise StandardError }
|
66
|
+
|
67
|
+
expect {
|
68
|
+
catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') }
|
69
|
+
}.to raise_error
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'detection' do
|
74
|
+
|
75
|
+
it 'uses the xml extension if one is provided' do
|
76
|
+
subject.call('PATH_INFO' => '/info.xml')
|
77
|
+
expect(subject.env['api.format']).to eq(:xml)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'uses the json extension if one is provided' do
|
81
|
+
subject.call('PATH_INFO' => '/info.json')
|
82
|
+
expect(subject.env['api.format']).to eq(:json)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'uses the format parameter if one is provided' do
|
86
|
+
subject.call('PATH_INFO' => '/info', 'QUERY_STRING' => 'format=json')
|
87
|
+
expect(subject.env['api.format']).to eq(:json)
|
88
|
+
subject.call('PATH_INFO' => '/info', 'QUERY_STRING' => 'format=xml')
|
89
|
+
expect(subject.env['api.format']).to eq(:xml)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'uses the default format if none is provided' do
|
93
|
+
subject.call('PATH_INFO' => '/info')
|
94
|
+
expect(subject.env['api.format']).to eq(:txt)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'uses the requested format if provided in headers' do
|
98
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json')
|
99
|
+
expect(subject.env['api.format']).to eq(:json)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'uses the file extension format if provided before headers' do
|
103
|
+
subject.call('PATH_INFO' => '/info.txt', 'HTTP_ACCEPT' => 'application/json')
|
104
|
+
expect(subject.env['api.format']).to eq(:txt)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'accept header detection' do
|
109
|
+
it 'detects from the Accept header' do
|
110
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml')
|
111
|
+
expect(subject.env['api.format']).to eq(:xml)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'looks for case-indifferent headers' do
|
115
|
+
subject.call('PATH_INFO' => '/info', 'http_accept' => 'application/xml')
|
116
|
+
expect(subject.env['api.format']).to eq(:xml)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'uses quality rankings to determine formats' do
|
120
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=0.3,application/xml; q=1.0')
|
121
|
+
expect(subject.env['api.format']).to eq(:xml)
|
122
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=1.0,application/xml; q=0.3')
|
123
|
+
expect(subject.env['api.format']).to eq(:json)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'handles quality rankings mixed with nothing' do
|
127
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml; q=1.0')
|
128
|
+
expect(subject.env['api.format']).to eq(:xml)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'parses headers with other attributes' do
|
132
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; abc=2.3; q=1.0,application/xml; q=0.7')
|
133
|
+
expect(subject.env['api.format']).to eq(:json)
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'parses headers with vendor and api version' do
|
137
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test-v1+xml')
|
138
|
+
expect(subject.env['api.format']).to eq(:xml)
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'parses headers with symbols as hash keys' do
|
142
|
+
subject.call('PATH_INFO' => '/info', 'http_accept' => 'application/xml', system_time: '091293')
|
143
|
+
expect(subject.env[:system_time]).to eq('091293')
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context 'content-type' do
|
148
|
+
it 'is set for json' do
|
149
|
+
_, headers, _ = subject.call('PATH_INFO' => '/info.json')
|
150
|
+
expect(headers['Content-type']).to eq('application/json')
|
151
|
+
end
|
152
|
+
it 'is set for xml' do
|
153
|
+
_, headers, _ = subject.call('PATH_INFO' => '/info.xml')
|
154
|
+
expect(headers['Content-type']).to eq('application/xml')
|
155
|
+
end
|
156
|
+
it 'is set for txt' do
|
157
|
+
_, headers, _ = subject.call('PATH_INFO' => '/info.txt')
|
158
|
+
expect(headers['Content-type']).to eq('text/plain')
|
159
|
+
end
|
160
|
+
it 'is set for custom' do
|
161
|
+
subject.options[:content_types] = {}
|
162
|
+
subject.options[:content_types][:custom] = 'application/x-custom'
|
163
|
+
_, headers, _ = subject.call('PATH_INFO' => '/info.custom')
|
164
|
+
expect(headers['Content-type']).to eq('application/x-custom')
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'format' do
|
169
|
+
it 'uses custom formatter' do
|
170
|
+
subject.options[:content_types] = {}
|
171
|
+
subject.options[:content_types][:custom] = "don't care"
|
172
|
+
subject.options[:formatters][:custom] = lambda { |obj, env| 'CUSTOM FORMAT' }
|
173
|
+
_, _, body = subject.call('PATH_INFO' => '/info.custom')
|
174
|
+
expect(body.body).to eq(['CUSTOM FORMAT'])
|
175
|
+
end
|
176
|
+
it 'uses default json formatter' do
|
177
|
+
@body = ['blah']
|
178
|
+
_, _, body = subject.call('PATH_INFO' => '/info.json')
|
179
|
+
expect(body.body).to eq(['["blah"]'])
|
180
|
+
end
|
181
|
+
it 'uses custom json formatter' do
|
182
|
+
subject.options[:formatters][:json] = lambda { |obj, env| 'CUSTOM JSON FORMAT' }
|
183
|
+
_, _, body = subject.call('PATH_INFO' => '/info.json')
|
184
|
+
expect(body.body).to eq(['CUSTOM JSON FORMAT'])
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
context 'input' do
|
189
|
+
["POST", "PATCH", "PUT", "DELETE"].each do |method|
|
190
|
+
["application/json", "application/json; charset=utf-8"].each do |content_type|
|
191
|
+
context content_type do
|
192
|
+
it 'parses the body from #{method} and copies values into rack.request.form_hash' do
|
193
|
+
io = StringIO.new('{"is_boolean":true,"string":"thing"}')
|
194
|
+
subject.call(
|
195
|
+
'PATH_INFO' => '/info',
|
196
|
+
'REQUEST_METHOD' => method,
|
197
|
+
'CONTENT_TYPE' => content_type,
|
198
|
+
'rack.input' => io,
|
199
|
+
'CONTENT_LENGTH' => io.length
|
200
|
+
)
|
201
|
+
expect(subject.env['rack.request.form_hash']['is_boolean']).to be true
|
202
|
+
expect(subject.env['rack.request.form_hash']['string']).to eq('thing')
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
it "parses the chunked body from #{method} and copies values into rack.request.from_hash" do
|
207
|
+
io = StringIO.new('{"is_boolean":true,"string":"thing"}')
|
208
|
+
subject.call(
|
209
|
+
'PATH_INFO' => '/infol',
|
210
|
+
'REQUEST_METHOD' => method,
|
211
|
+
'CONTENT_TYPE' => 'application/json',
|
212
|
+
'rack.input' => io,
|
213
|
+
'HTTP_TRANSFER_ENCODING' => 'chunked'
|
214
|
+
)
|
215
|
+
expect(subject.env['rack.request.form_hash']['is_boolean']).to be true
|
216
|
+
expect(subject.env['rack.request.form_hash']['string']).to eq('thing')
|
217
|
+
end
|
218
|
+
it "rewinds IO" do
|
219
|
+
io = StringIO.new('{"is_boolean":true,"string":"thing"}')
|
220
|
+
io.read
|
221
|
+
subject.call(
|
222
|
+
'PATH_INFO' => '/infol',
|
223
|
+
'REQUEST_METHOD' => method,
|
224
|
+
'CONTENT_TYPE' => 'application/json',
|
225
|
+
'rack.input' => io,
|
226
|
+
'HTTP_TRANSFER_ENCODING' => 'chunked'
|
227
|
+
)
|
228
|
+
expect(subject.env['rack.request.form_hash']['is_boolean']).to be true
|
229
|
+
expect(subject.env['rack.request.form_hash']['string']).to eq('thing')
|
230
|
+
end
|
231
|
+
it 'parses the body from an xml #{method} and copies values into rack.request.from_hash' do
|
232
|
+
io = StringIO.new('<thing><name>Test</name></thing>')
|
233
|
+
subject.call(
|
234
|
+
'PATH_INFO' => '/info.xml',
|
235
|
+
'REQUEST_METHOD' => method,
|
236
|
+
'CONTENT_TYPE' => 'application/xml',
|
237
|
+
'rack.input' => io,
|
238
|
+
'CONTENT_LENGTH' => io.length
|
239
|
+
)
|
240
|
+
expect(subject.env['rack.request.form_hash']['thing']['name']).to eq('Test')
|
241
|
+
end
|
242
|
+
[Rack::Request::FORM_DATA_MEDIA_TYPES, Rack::Request::PARSEABLE_DATA_MEDIA_TYPES].flatten.each do |content_type|
|
243
|
+
it "ignores #{content_type}" do
|
244
|
+
io = StringIO.new('name=Other+Test+Thing')
|
245
|
+
subject.call(
|
246
|
+
'PATH_INFO' => '/info',
|
247
|
+
'REQUEST_METHOD' => method,
|
248
|
+
'CONTENT_TYPE' => content_type,
|
249
|
+
'rack.input' => io,
|
250
|
+
'CONTENT_LENGTH' => io.length
|
251
|
+
)
|
252
|
+
expect(subject.env['rack.request.form_hash']).to be_nil
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Middleware::Versioner::AcceptVersionHeader do
|
4
|
+
let(:app) { lambda { |env| [200, env, env] } }
|
5
|
+
subject { Grape::Middleware::Versioner::AcceptVersionHeader.new(app, @options || {}) }
|
6
|
+
|
7
|
+
before do
|
8
|
+
@options = {
|
9
|
+
version_options: {
|
10
|
+
using: :accept_version_header
|
11
|
+
}
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'api.version' do
|
16
|
+
before do
|
17
|
+
@options[:versions] = ['v1']
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'is set' do
|
21
|
+
status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1')
|
22
|
+
expect(env['api.version']).to eql 'v1'
|
23
|
+
expect(status).to eq(200)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'is set if format provided' do
|
27
|
+
status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1')
|
28
|
+
expect(env['api.version']).to eql 'v1'
|
29
|
+
expect(status).to eq(200)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'fails with 406 Not Acceptable if version is not supported' do
|
33
|
+
expect {
|
34
|
+
subject.call('HTTP_ACCEPT_VERSION' => 'v2').last
|
35
|
+
}.to throw_symbol(
|
36
|
+
:error,
|
37
|
+
status: 406,
|
38
|
+
headers: { 'X-Cascade' => 'pass' },
|
39
|
+
message: 'The requested version is not supported.'
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'succeeds if :strict is not set' do
|
45
|
+
expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200)
|
46
|
+
expect(subject.call({}).first).to eq(200)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'succeeds if :strict is set to false' do
|
50
|
+
@options[:version_options][:strict] = false
|
51
|
+
expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200)
|
52
|
+
expect(subject.call({}).first).to eq(200)
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when :strict is set' do
|
56
|
+
before do
|
57
|
+
@options[:versions] = ['v1']
|
58
|
+
@options[:version_options][:strict] = true
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'fails with 406 Not Acceptable if header is not set' do
|
62
|
+
expect {
|
63
|
+
subject.call({}).last
|
64
|
+
}.to throw_symbol(
|
65
|
+
:error,
|
66
|
+
status: 406,
|
67
|
+
headers: { 'X-Cascade' => 'pass' },
|
68
|
+
message: 'Accept-Version header must be set.'
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'fails with 406 Not Acceptable if header is empty' do
|
73
|
+
expect {
|
74
|
+
subject.call('HTTP_ACCEPT_VERSION' => '').last
|
75
|
+
}.to throw_symbol(
|
76
|
+
:error,
|
77
|
+
status: 406,
|
78
|
+
headers: { 'X-Cascade' => 'pass' },
|
79
|
+
message: 'Accept-Version header must be set.'
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'succeeds if proper header is set' do
|
84
|
+
expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'when :strict and :cascade=>false are set' do
|
89
|
+
before do
|
90
|
+
@options[:versions] = ['v1']
|
91
|
+
@options[:version_options][:strict] = true
|
92
|
+
@options[:version_options][:cascade] = false
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'fails with 406 Not Acceptable if header is not set' do
|
96
|
+
expect {
|
97
|
+
subject.call({}).last
|
98
|
+
}.to throw_symbol(
|
99
|
+
:error,
|
100
|
+
status: 406,
|
101
|
+
headers: {},
|
102
|
+
message: 'Accept-Version header must be set.'
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'fails with 406 Not Acceptable if header is empty' do
|
107
|
+
expect {
|
108
|
+
subject.call('HTTP_ACCEPT_VERSION' => '').last
|
109
|
+
}.to throw_symbol(
|
110
|
+
:error,
|
111
|
+
status: 406,
|
112
|
+
headers: {},
|
113
|
+
message: 'Accept-Version header must be set.'
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'succeeds if proper header is set' do
|
118
|
+
expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,302 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Middleware::Versioner::Header do
|
4
|
+
let(:app) { lambda { |env| [200, env, env] } }
|
5
|
+
subject { Grape::Middleware::Versioner::Header.new(app, @options || {}) }
|
6
|
+
|
7
|
+
before do
|
8
|
+
@options = {
|
9
|
+
version_options: {
|
10
|
+
using: :header,
|
11
|
+
vendor: 'vendor'
|
12
|
+
}
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'api.type and api.subtype' do
|
17
|
+
it 'sets type and subtype to first choice of content type if no preference given' do
|
18
|
+
status, _, env = subject.call('HTTP_ACCEPT' => '*/*')
|
19
|
+
expect(env['api.type']).to eql 'application'
|
20
|
+
expect(env['api.subtype']).to eql 'vnd.vendor+xml'
|
21
|
+
expect(status).to eq(200)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'sets preferred type' do
|
25
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/*')
|
26
|
+
expect(env['api.type']).to eql 'application'
|
27
|
+
expect(env['api.subtype']).to eql 'vnd.vendor+xml'
|
28
|
+
expect(status).to eq(200)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'sets preferred type and subtype' do
|
32
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'text/plain')
|
33
|
+
expect(env['api.type']).to eql 'text'
|
34
|
+
expect(env['api.subtype']).to eql 'plain'
|
35
|
+
expect(status).to eq(200)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'api.format' do
|
40
|
+
it 'is set' do
|
41
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json')
|
42
|
+
expect(env['api.format']).to eql 'json'
|
43
|
+
expect(status).to eq(200)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'is nil if not provided' do
|
47
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor')
|
48
|
+
expect(env['api.format']).to eql nil
|
49
|
+
expect(status).to eq(200)
|
50
|
+
end
|
51
|
+
|
52
|
+
['v1', :v1].each do |version|
|
53
|
+
context 'when version is set to #{version{ ' do
|
54
|
+
before do
|
55
|
+
@options[:versions] = [version]
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'is set' do
|
59
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
|
60
|
+
expect(env['api.format']).to eql 'json'
|
61
|
+
expect(status).to eq(200)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'is nil if not provided' do
|
65
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
|
66
|
+
expect(env['api.format']).to eql nil
|
67
|
+
expect(status).to eq(200)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'api.vendor' do
|
74
|
+
it 'is set' do
|
75
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor')
|
76
|
+
expect(env['api.vendor']).to eql 'vendor'
|
77
|
+
expect(status).to eq(200)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'is set if format provided' do
|
81
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json')
|
82
|
+
expect(env['api.vendor']).to eql 'vendor'
|
83
|
+
expect(status).to eq(200)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'fails with 406 Not Acceptable if vendor is invalid' do
|
87
|
+
expect {
|
88
|
+
subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor+json').last
|
89
|
+
}.to throw_symbol(
|
90
|
+
:error,
|
91
|
+
status: 406,
|
92
|
+
headers: { 'X-Cascade' => 'pass' },
|
93
|
+
message: 'API vendor or version not found.'
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'when version is set' do
|
98
|
+
before do
|
99
|
+
@options[:versions] = ['v1']
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'is set' do
|
103
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
|
104
|
+
expect(env['api.vendor']).to eql 'vendor'
|
105
|
+
expect(status).to eq(200)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'is set if format provided' do
|
109
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
|
110
|
+
expect(env['api.vendor']).to eql 'vendor'
|
111
|
+
expect(status).to eq(200)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'fails with 406 Not Acceptable if vendor is invalid' do
|
115
|
+
expect {
|
116
|
+
subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor-v1+json').last
|
117
|
+
}.to throw_symbol(
|
118
|
+
:error,
|
119
|
+
status: 406,
|
120
|
+
headers: { 'X-Cascade' => 'pass' },
|
121
|
+
message: 'API vendor or version not found.'
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'api.version' do
|
128
|
+
before do
|
129
|
+
@options[:versions] = ['v1']
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'is set' do
|
133
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
|
134
|
+
expect(env['api.version']).to eql 'v1'
|
135
|
+
expect(status).to eq(200)
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'is set if format provided' do
|
139
|
+
status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
|
140
|
+
expect(env['api.version']).to eql 'v1'
|
141
|
+
expect(status).to eq(200)
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'fails with 406 Not Acceptable if version is invalid' do
|
145
|
+
expect {
|
146
|
+
subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last
|
147
|
+
}.to throw_symbol(
|
148
|
+
:error,
|
149
|
+
status: 406,
|
150
|
+
headers: { 'X-Cascade' => 'pass' },
|
151
|
+
message: 'API vendor or version not found.'
|
152
|
+
)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'succeeds if :strict is not set' do
|
157
|
+
expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200)
|
158
|
+
expect(subject.call({}).first).to eq(200)
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'succeeds if :strict is set to false' do
|
162
|
+
@options[:version_options][:strict] = false
|
163
|
+
expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200)
|
164
|
+
expect(subject.call({}).first).to eq(200)
|
165
|
+
end
|
166
|
+
|
167
|
+
context 'when :strict is set' do
|
168
|
+
before do
|
169
|
+
@options[:versions] = ['v1']
|
170
|
+
@options[:version_options][:strict] = true
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'fails with 406 Not Acceptable if header is not set' do
|
174
|
+
expect {
|
175
|
+
subject.call({}).last
|
176
|
+
}.to throw_symbol(
|
177
|
+
:error,
|
178
|
+
status: 406,
|
179
|
+
headers: { 'X-Cascade' => 'pass' },
|
180
|
+
message: 'Accept header must be set.'
|
181
|
+
)
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'fails with 406 Not Acceptable if header is empty' do
|
185
|
+
expect {
|
186
|
+
subject.call('HTTP_ACCEPT' => '').last
|
187
|
+
}.to throw_symbol(
|
188
|
+
:error,
|
189
|
+
status: 406,
|
190
|
+
headers: { 'X-Cascade' => 'pass' },
|
191
|
+
message: 'Accept header must be set.'
|
192
|
+
)
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'fails with 406 Not Acceptable if type is a range' do
|
196
|
+
expect {
|
197
|
+
subject.call('HTTP_ACCEPT' => '*/*').last
|
198
|
+
}.to throw_symbol(
|
199
|
+
:error,
|
200
|
+
status: 406,
|
201
|
+
headers: { 'X-Cascade' => 'pass' },
|
202
|
+
message: 'Accept header must not contain ranges ("*").'
|
203
|
+
)
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'fails with 406 Not Acceptable if subtype is a range' do
|
207
|
+
expect {
|
208
|
+
subject.call('HTTP_ACCEPT' => 'application/*').last
|
209
|
+
}.to throw_symbol(
|
210
|
+
:error,
|
211
|
+
status: 406,
|
212
|
+
headers: { 'X-Cascade' => 'pass' },
|
213
|
+
message: 'Accept header must not contain ranges ("*").'
|
214
|
+
)
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'succeeds if proper header is set' do
|
218
|
+
expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
context 'when :strict and :cascade=>false are set' do
|
223
|
+
before do
|
224
|
+
@options[:versions] = ['v1']
|
225
|
+
@options[:version_options][:strict] = true
|
226
|
+
@options[:version_options][:cascade] = false
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'fails with 406 Not Acceptable if header is not set' do
|
230
|
+
expect {
|
231
|
+
subject.call({}).last
|
232
|
+
}.to throw_symbol(
|
233
|
+
:error,
|
234
|
+
status: 406,
|
235
|
+
headers: {},
|
236
|
+
message: 'Accept header must be set.'
|
237
|
+
)
|
238
|
+
end
|
239
|
+
|
240
|
+
it 'fails with 406 Not Acceptable if header is empty' do
|
241
|
+
expect {
|
242
|
+
subject.call('HTTP_ACCEPT' => '').last
|
243
|
+
}.to throw_symbol(
|
244
|
+
:error,
|
245
|
+
status: 406,
|
246
|
+
headers: {},
|
247
|
+
message: 'Accept header must be set.'
|
248
|
+
)
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'fails with 406 Not Acceptable if type is a range' do
|
252
|
+
expect {
|
253
|
+
subject.call('HTTP_ACCEPT' => '*/*').last
|
254
|
+
}.to throw_symbol(
|
255
|
+
:error,
|
256
|
+
status: 406,
|
257
|
+
headers: {},
|
258
|
+
message: 'Accept header must not contain ranges ("*").'
|
259
|
+
)
|
260
|
+
end
|
261
|
+
|
262
|
+
it 'fails with 406 Not Acceptable if subtype is a range' do
|
263
|
+
expect {
|
264
|
+
subject.call('HTTP_ACCEPT' => 'application/*').last
|
265
|
+
}.to throw_symbol(
|
266
|
+
:error,
|
267
|
+
status: 406,
|
268
|
+
headers: {},
|
269
|
+
message: 'Accept header must not contain ranges ("*").'
|
270
|
+
)
|
271
|
+
end
|
272
|
+
|
273
|
+
it 'succeeds if proper header is set' do
|
274
|
+
expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
context 'when multiple versions are specified' do
|
279
|
+
before do
|
280
|
+
@options[:versions] = ['v1', 'v2']
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'succeeds with v1' do
|
284
|
+
expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200)
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'succeeds with v2' do
|
288
|
+
expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').first).to eq(200)
|
289
|
+
end
|
290
|
+
|
291
|
+
it 'fails with another version' do
|
292
|
+
expect {
|
293
|
+
subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json')
|
294
|
+
}.to throw_symbol(
|
295
|
+
:error,
|
296
|
+
status: 406,
|
297
|
+
headers: { 'X-Cascade' => 'pass' },
|
298
|
+
message: 'API vendor or version not found.'
|
299
|
+
)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|