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,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
|