grape-security 0.8.0

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