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