grape 0.3.0 → 0.7.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.
Potentially problematic release.
This version of grape might be problematic. Click here for more details.
- checksums.yaml +15 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +70 -0
- data/.travis.yml +7 -6
- data/CHANGELOG.md +134 -4
- data/CONTRIBUTING.md +118 -0
- data/Gemfile +5 -2
- data/README.md +551 -116
- data/RELEASING.md +105 -0
- data/Rakefile +29 -8
- data/UPGRADING.md +124 -0
- data/grape.gemspec +3 -3
- data/lib/grape/api.rb +207 -88
- data/lib/grape/cookies.rb +4 -8
- data/lib/grape/endpoint.rb +198 -144
- data/lib/grape/error_formatter/base.rb +5 -7
- data/lib/grape/error_formatter/json.rb +3 -5
- data/lib/grape/error_formatter/txt.rb +1 -3
- data/lib/grape/error_formatter/xml.rb +4 -6
- data/lib/grape/exceptions/base.rb +9 -9
- data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
- data/lib/grape/exceptions/invalid_formatter.rb +1 -4
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -5
- data/lib/grape/exceptions/invalid_with_option_for_represent.rb +1 -6
- data/lib/grape/exceptions/missing_mime_type.rb +1 -5
- data/lib/grape/exceptions/missing_option.rb +1 -4
- data/lib/grape/exceptions/missing_vendor_option.rb +1 -4
- data/lib/grape/exceptions/unknown_options.rb +1 -5
- data/lib/grape/exceptions/unknown_validator.rb +1 -3
- data/lib/grape/exceptions/validation.rb +13 -3
- data/lib/grape/exceptions/validation_errors.rb +43 -0
- data/lib/grape/formatter/base.rb +5 -7
- data/lib/grape/formatter/json.rb +0 -3
- data/lib/grape/formatter/serializable_hash.rb +15 -15
- data/lib/grape/formatter/txt.rb +0 -2
- data/lib/grape/formatter/xml.rb +0 -2
- data/lib/grape/http/request.rb +26 -0
- data/lib/grape/locale/en.yml +8 -5
- data/lib/grape/middleware/auth/base.rb +30 -0
- data/lib/grape/middleware/auth/basic.rb +3 -20
- data/lib/grape/middleware/auth/digest.rb +2 -19
- data/lib/grape/middleware/auth/oauth2.rb +31 -24
- data/lib/grape/middleware/base.rb +7 -7
- data/lib/grape/middleware/error.rb +36 -22
- data/lib/grape/middleware/filter.rb +3 -3
- data/lib/grape/middleware/formatter.rb +99 -61
- data/lib/grape/middleware/globals.rb +13 -0
- data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
- data/lib/grape/middleware/versioner/header.rb +22 -16
- data/lib/grape/middleware/versioner/param.rb +9 -11
- data/lib/grape/middleware/versioner/path.rb +10 -13
- data/lib/grape/middleware/versioner.rb +3 -1
- data/lib/grape/namespace.rb +23 -0
- data/lib/grape/parser/base.rb +3 -5
- data/lib/grape/parser/json.rb +0 -2
- data/lib/grape/parser/xml.rb +0 -2
- data/lib/grape/path.rb +70 -0
- data/lib/grape/route.rb +10 -6
- data/lib/grape/util/content_types.rb +2 -1
- data/lib/grape/util/deep_merge.rb +5 -5
- data/lib/grape/util/hash_stack.rb +13 -2
- data/lib/grape/validations/coerce.rb +11 -10
- data/lib/grape/validations/default.rb +25 -0
- data/lib/grape/validations/presence.rb +7 -3
- data/lib/grape/validations/regexp.rb +2 -5
- data/lib/grape/validations/values.rb +17 -0
- data/lib/grape/validations.rb +161 -54
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +19 -4
- data/spec/grape/api_spec.rb +897 -268
- data/spec/grape/endpoint_spec.rb +283 -66
- data/spec/grape/entity_spec.rb +132 -29
- data/spec/grape/exceptions/missing_mime_type_spec.rb +3 -9
- data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
- data/spec/grape/middleware/auth/basic_spec.rb +8 -8
- data/spec/grape/middleware/auth/digest_spec.rb +5 -5
- data/spec/grape/middleware/auth/oauth2_spec.rb +81 -36
- data/spec/grape/middleware/base_spec.rb +8 -13
- data/spec/grape/middleware/error_spec.rb +13 -17
- data/spec/grape/middleware/exception_spec.rb +47 -27
- data/spec/grape/middleware/formatter_spec.rb +103 -41
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
- data/spec/grape/middleware/versioner/header_spec.rb +76 -51
- data/spec/grape/middleware/versioner/param_spec.rb +18 -18
- data/spec/grape/middleware/versioner/path_spec.rb +6 -6
- data/spec/grape/middleware/versioner_spec.rb +5 -2
- data/spec/grape/path_spec.rb +229 -0
- data/spec/grape/util/hash_stack_spec.rb +31 -32
- data/spec/grape/validations/coerce_spec.rb +116 -51
- data/spec/grape/validations/default_spec.rb +123 -0
- data/spec/grape/validations/presence_spec.rb +42 -44
- data/spec/grape/validations/regexp_spec.rb +9 -9
- data/spec/grape/validations/values_spec.rb +138 -0
- data/spec/grape/validations/zh-CN.yml +4 -3
- data/spec/grape/validations_spec.rb +681 -48
- data/spec/shared/versioning_examples.rb +22 -6
- data/spec/spec_helper.rb +3 -2
- data/spec/support/basic_auth_encode_helpers.rb +0 -1
- data/spec/support/content_type_helpers.rb +11 -0
- data/spec/support/versioned_helpers.rb +13 -5
- metadata +34 -84
@@ -2,48 +2,44 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Grape::Middleware::Error do
|
4
4
|
class ErrApp
|
5
|
-
class << self
|
5
|
+
class << self
|
6
6
|
attr_accessor :error
|
7
7
|
attr_accessor :format
|
8
|
-
|
8
|
+
|
9
9
|
def call(env)
|
10
|
-
throw :error,
|
10
|
+
throw :error, error
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def app
|
16
16
|
Rack::Builder.app do
|
17
|
-
use Grape::Middleware::Error, :
|
17
|
+
use Grape::Middleware::Error, default_message: 'Aww, hamburgers.'
|
18
18
|
run ErrApp
|
19
19
|
end
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
it 'sets the status code appropriately' do
|
23
|
-
ErrApp.error = {:
|
23
|
+
ErrApp.error = { status: 410 }
|
24
24
|
get '/'
|
25
25
|
last_response.status.should == 410
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
it 'sets the error message appropriately' do
|
29
|
-
ErrApp.error = {:
|
29
|
+
ErrApp.error = { message: 'Awesome stuff.' }
|
30
30
|
get '/'
|
31
31
|
last_response.body.should == 'Awesome stuff.'
|
32
32
|
end
|
33
|
-
|
34
|
-
it 'defaults to a
|
33
|
+
|
34
|
+
it 'defaults to a 500 status' do
|
35
35
|
ErrApp.error = {}
|
36
36
|
get '/'
|
37
|
-
last_response.status.should ==
|
37
|
+
last_response.status.should == 500
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
it 'has a default message' do
|
41
41
|
ErrApp.error = {}
|
42
42
|
get '/'
|
43
43
|
last_response.body.should == 'Aww, hamburgers.'
|
44
44
|
end
|
45
|
-
|
46
|
-
context 'with formatting' do
|
47
|
-
|
48
|
-
end
|
49
45
|
end
|
@@ -15,9 +15,10 @@ describe Grape::Middleware::Error do
|
|
15
15
|
# raises a hash error
|
16
16
|
class ErrorHashApp
|
17
17
|
class << self
|
18
|
-
def error!(message, status
|
19
|
-
throw :error, :
|
18
|
+
def error!(message, status)
|
19
|
+
throw :error, message: { error: message, detail: "missing widget" }, status: status
|
20
20
|
end
|
21
|
+
|
21
22
|
def call(env)
|
22
23
|
error!("rain!", 401)
|
23
24
|
end
|
@@ -27,9 +28,10 @@ describe Grape::Middleware::Error do
|
|
27
28
|
# raises an error!
|
28
29
|
class AccessDeniedApp
|
29
30
|
class << self
|
30
|
-
def error!(message, status
|
31
|
-
throw :error, :
|
31
|
+
def error!(message, status)
|
32
|
+
throw :error, message: message, status: status
|
32
33
|
end
|
34
|
+
|
33
35
|
def call(env)
|
34
36
|
error!("Access Denied", 401)
|
35
37
|
end
|
@@ -37,18 +39,18 @@ describe Grape::Middleware::Error do
|
|
37
39
|
end
|
38
40
|
|
39
41
|
# raises a custom error
|
40
|
-
class CustomError < Grape::Exceptions::Base
|
42
|
+
class CustomError < Grape::Exceptions::Base
|
43
|
+
end
|
44
|
+
|
41
45
|
class CustomErrorApp
|
42
46
|
class << self
|
43
47
|
def call(env)
|
44
|
-
raise CustomError, :
|
48
|
+
raise CustomError, status: 400, message: 'failed validation'
|
45
49
|
end
|
46
50
|
end
|
47
51
|
end
|
48
52
|
|
49
|
-
|
50
|
-
@app
|
51
|
-
end
|
53
|
+
attr_reader :app
|
52
54
|
|
53
55
|
it 'does not trap errors by default' do
|
54
56
|
@app ||= Rack::Builder.app do
|
@@ -61,25 +63,25 @@ describe Grape::Middleware::Error do
|
|
61
63
|
context 'with rescue_all set to true' do
|
62
64
|
it 'sets the message appropriately' do
|
63
65
|
@app ||= Rack::Builder.app do
|
64
|
-
use Grape::Middleware::Error, :
|
66
|
+
use Grape::Middleware::Error, rescue_all: true
|
65
67
|
run ExceptionApp
|
66
68
|
end
|
67
69
|
get '/'
|
68
70
|
last_response.body.should == "rain!"
|
69
71
|
end
|
70
72
|
|
71
|
-
it 'defaults to a
|
73
|
+
it 'defaults to a 500 status' do
|
72
74
|
@app ||= Rack::Builder.app do
|
73
|
-
use Grape::Middleware::Error, :
|
75
|
+
use Grape::Middleware::Error, rescue_all: true
|
74
76
|
run ExceptionApp
|
75
77
|
end
|
76
78
|
get '/'
|
77
|
-
last_response.status.should ==
|
79
|
+
last_response.status.should == 500
|
78
80
|
end
|
79
81
|
|
80
82
|
it 'is possible to specify a different default status code' do
|
81
83
|
@app ||= Rack::Builder.app do
|
82
|
-
use Grape::Middleware::Error, :
|
84
|
+
use Grape::Middleware::Error, rescue_all: true, default_status: 500
|
83
85
|
run ExceptionApp
|
84
86
|
end
|
85
87
|
get '/'
|
@@ -88,7 +90,7 @@ describe Grape::Middleware::Error do
|
|
88
90
|
|
89
91
|
it 'is possible to return errors in json format' do
|
90
92
|
@app ||= Rack::Builder.app do
|
91
|
-
use Grape::Middleware::Error, :
|
93
|
+
use Grape::Middleware::Error, rescue_all: true, format: :json
|
92
94
|
run ExceptionApp
|
93
95
|
end
|
94
96
|
get '/'
|
@@ -97,7 +99,26 @@ describe Grape::Middleware::Error do
|
|
97
99
|
|
98
100
|
it 'is possible to return hash errors in json format' do
|
99
101
|
@app ||= Rack::Builder.app do
|
100
|
-
use Grape::Middleware::Error, :
|
102
|
+
use Grape::Middleware::Error, rescue_all: true, format: :json
|
103
|
+
run ErrorHashApp
|
104
|
+
end
|
105
|
+
get '/'
|
106
|
+
['{"error":"rain!","detail":"missing widget"}',
|
107
|
+
'{"detail":"missing widget","error":"rain!"}'].should include(last_response.body)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'is possible to return errors in jsonapi format' do
|
111
|
+
@app ||= Rack::Builder.app do
|
112
|
+
use Grape::Middleware::Error, rescue_all: true, format: :jsonapi
|
113
|
+
run ExceptionApp
|
114
|
+
end
|
115
|
+
get '/'
|
116
|
+
last_response.body.should == '{"error":"rain!"}'
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'is possible to return hash errors in jsonapi format' do
|
120
|
+
@app ||= Rack::Builder.app do
|
121
|
+
use Grape::Middleware::Error, rescue_all: true, format: :jsonapi
|
101
122
|
run ErrorHashApp
|
102
123
|
end
|
103
124
|
get '/'
|
@@ -107,7 +128,7 @@ describe Grape::Middleware::Error do
|
|
107
128
|
|
108
129
|
it 'is possible to return errors in xml format' do
|
109
130
|
@app ||= Rack::Builder.app do
|
110
|
-
use Grape::Middleware::Error, :
|
131
|
+
use Grape::Middleware::Error, rescue_all: true, format: :xml
|
111
132
|
run ExceptionApp
|
112
133
|
end
|
113
134
|
get '/'
|
@@ -116,7 +137,7 @@ describe Grape::Middleware::Error do
|
|
116
137
|
|
117
138
|
it 'is possible to return hash errors in xml format' do
|
118
139
|
@app ||= Rack::Builder.app do
|
119
|
-
use Grape::Middleware::Error, :
|
140
|
+
use Grape::Middleware::Error, rescue_all: true, format: :xml
|
120
141
|
run ErrorHashApp
|
121
142
|
end
|
122
143
|
get '/'
|
@@ -126,14 +147,13 @@ describe Grape::Middleware::Error do
|
|
126
147
|
|
127
148
|
it 'is possible to specify a custom formatter' do
|
128
149
|
@app ||= Rack::Builder.app do
|
129
|
-
use Grape::Middleware::Error,
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
}
|
150
|
+
use Grape::Middleware::Error, rescue_all: true,
|
151
|
+
format: :custom,
|
152
|
+
error_formatters: {
|
153
|
+
custom: lambda { |message, backtrace, options, env|
|
154
|
+
{ custom_formatter: message }.inspect
|
155
|
+
}
|
156
|
+
}
|
137
157
|
run ExceptionApp
|
138
158
|
end
|
139
159
|
get '/'
|
@@ -151,7 +171,7 @@ describe Grape::Middleware::Error do
|
|
151
171
|
|
152
172
|
it 'responds to custom Grape exceptions appropriately' do
|
153
173
|
@app ||= Rack::Builder.app do
|
154
|
-
use Grape::Middleware::Error, :
|
174
|
+
use Grape::Middleware::Error, rescue_all: false
|
155
175
|
run CustomErrorApp
|
156
176
|
end
|
157
177
|
|
@@ -1,16 +1,16 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Grape::Middleware::Formatter do
|
4
|
-
subject{ Grape::Middleware::Formatter.new(app) }
|
5
|
-
before{ subject.stub
|
4
|
+
subject { Grape::Middleware::Formatter.new(app) }
|
5
|
+
before { subject.stub(:dup).and_return(subject) }
|
6
6
|
|
7
|
-
let(:app){ lambda{|env| [200, {}, [@body || { "foo" => "bar" }]]} }
|
7
|
+
let(:app) { lambda { |env| [200, {}, [@body || { "foo" => "bar" }]] } }
|
8
8
|
|
9
9
|
context 'serialization' do
|
10
10
|
it 'looks at the bodies for possibly serializable data' do
|
11
|
-
@body = {"abc" => "def"}
|
12
|
-
|
13
|
-
bodies.each{|b| b.should == MultiJson.dump(@body) }
|
11
|
+
@body = { "abc" => "def" }
|
12
|
+
_, _, bodies = *subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json')
|
13
|
+
bodies.each { |b| b.should == MultiJson.dump(@body) }
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'calls #to_json since default format is json' do
|
@@ -21,7 +21,18 @@ describe Grape::Middleware::Formatter do
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
subject.call(
|
24
|
+
subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json').last.each { |b| b.should == '"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| b.should == '{"foos":[{"bar":"baz"}] }' }
|
25
36
|
end
|
26
37
|
|
27
38
|
it 'calls #to_xml if the content type is xml' do
|
@@ -32,98 +43,124 @@ describe Grape::Middleware::Formatter do
|
|
32
43
|
end
|
33
44
|
end
|
34
45
|
|
35
|
-
subject.call(
|
46
|
+
subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json').last.each { |b| b.should == '<bar/>' }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'error handling' do
|
51
|
+
let(:formatter) { double(:formatter) }
|
52
|
+
before do
|
53
|
+
Grape::Formatter::Base.stub(:formatter_for) { formatter }
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'rescues formatter-specific exceptions' do
|
57
|
+
formatter.stub(: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
|
+
formatter.stub(:call) { raise StandardError }
|
66
|
+
|
67
|
+
expect {
|
68
|
+
catch(:error) { subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') }
|
69
|
+
}.to raise_error
|
36
70
|
end
|
37
71
|
end
|
38
72
|
|
39
73
|
context 'detection' do
|
40
74
|
|
41
|
-
it 'uses the extension if one is provided' do
|
42
|
-
subject.call(
|
75
|
+
it 'uses the xml extension if one is provided' do
|
76
|
+
subject.call('PATH_INFO' => '/info.xml')
|
43
77
|
subject.env['api.format'].should == :xml
|
44
|
-
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'uses the json extension if one is provided' do
|
81
|
+
subject.call('PATH_INFO' => '/info.json')
|
45
82
|
subject.env['api.format'].should == :json
|
46
83
|
end
|
47
84
|
|
48
85
|
it 'uses the format parameter if one is provided' do
|
49
|
-
subject.call(
|
86
|
+
subject.call('PATH_INFO' => '/info', 'QUERY_STRING' => 'format=json')
|
50
87
|
subject.env['api.format'].should == :json
|
51
|
-
subject.call(
|
88
|
+
subject.call('PATH_INFO' => '/info', 'QUERY_STRING' => 'format=xml')
|
52
89
|
subject.env['api.format'].should == :xml
|
53
90
|
end
|
54
91
|
|
55
92
|
it 'uses the default format if none is provided' do
|
56
|
-
subject.call(
|
93
|
+
subject.call('PATH_INFO' => '/info')
|
57
94
|
subject.env['api.format'].should == :txt
|
58
95
|
end
|
59
96
|
|
60
97
|
it 'uses the requested format if provided in headers' do
|
61
|
-
subject.call(
|
98
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json')
|
62
99
|
subject.env['api.format'].should == :json
|
63
100
|
end
|
64
101
|
|
65
102
|
it 'uses the file extension format if provided before headers' do
|
66
|
-
subject.call(
|
103
|
+
subject.call('PATH_INFO' => '/info.txt', 'HTTP_ACCEPT' => 'application/json')
|
67
104
|
subject.env['api.format'].should == :txt
|
68
105
|
end
|
69
106
|
end
|
70
107
|
|
71
108
|
context 'accept header detection' do
|
72
109
|
it 'detects from the Accept header' do
|
73
|
-
subject.call(
|
110
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/xml')
|
74
111
|
subject.env['api.format'].should == :xml
|
75
112
|
end
|
76
113
|
|
77
114
|
it 'looks for case-indifferent headers' do
|
78
|
-
subject.call(
|
115
|
+
subject.call('PATH_INFO' => '/info', 'http_accept' => 'application/xml')
|
79
116
|
subject.env['api.format'].should == :xml
|
80
117
|
end
|
81
118
|
|
82
119
|
it 'uses quality rankings to determine formats' do
|
83
|
-
subject.call(
|
120
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=0.3,application/xml; q=1.0')
|
84
121
|
subject.env['api.format'].should == :xml
|
85
|
-
subject.call(
|
122
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; q=1.0,application/xml; q=0.3')
|
86
123
|
subject.env['api.format'].should == :json
|
87
124
|
end
|
88
125
|
|
89
126
|
it 'handles quality rankings mixed with nothing' do
|
90
|
-
subject.call(
|
127
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml; q=1.0')
|
91
128
|
subject.env['api.format'].should == :xml
|
92
129
|
end
|
93
130
|
|
94
131
|
it 'parses headers with other attributes' do
|
95
|
-
subject.call(
|
132
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/json; abc=2.3; q=1.0,application/xml; q=0.7')
|
96
133
|
subject.env['api.format'].should == :json
|
97
134
|
end
|
98
135
|
|
99
136
|
it 'parses headers with vendor and api version' do
|
100
|
-
subject.call(
|
137
|
+
subject.call('PATH_INFO' => '/info', 'HTTP_ACCEPT' => 'application/vnd.test-v1+xml')
|
101
138
|
subject.env['api.format'].should == :xml
|
102
139
|
end
|
103
140
|
|
104
141
|
it 'parses headers with symbols as hash keys' do
|
105
|
-
subject.call(
|
142
|
+
subject.call('PATH_INFO' => '/info', 'http_accept' => 'application/xml', system_time: '091293')
|
106
143
|
subject.env[:system_time].should == '091293'
|
107
144
|
end
|
108
145
|
end
|
109
146
|
|
110
147
|
context 'content-type' do
|
111
148
|
it 'is set for json' do
|
112
|
-
_, headers, _ = subject.call(
|
149
|
+
_, headers, _ = subject.call('PATH_INFO' => '/info.json')
|
113
150
|
headers['Content-type'].should == 'application/json'
|
114
151
|
end
|
115
152
|
it 'is set for xml' do
|
116
|
-
_, headers, _ = subject.call(
|
153
|
+
_, headers, _ = subject.call('PATH_INFO' => '/info.xml')
|
117
154
|
headers['Content-type'].should == 'application/xml'
|
118
155
|
end
|
119
156
|
it 'is set for txt' do
|
120
|
-
_, headers, _ = subject.call(
|
157
|
+
_, headers, _ = subject.call('PATH_INFO' => '/info.txt')
|
121
158
|
headers['Content-type'].should == 'text/plain'
|
122
159
|
end
|
123
160
|
it 'is set for custom' do
|
124
161
|
subject.options[:content_types] = {}
|
125
162
|
subject.options[:content_types][:custom] = 'application/x-custom'
|
126
|
-
_, headers, _ = subject.call(
|
163
|
+
_, headers, _ = subject.call('PATH_INFO' => '/info.custom')
|
127
164
|
headers['Content-type'].should == 'application/x-custom'
|
128
165
|
end
|
129
166
|
end
|
@@ -133,60 +170,85 @@ describe Grape::Middleware::Formatter do
|
|
133
170
|
subject.options[:content_types] = {}
|
134
171
|
subject.options[:content_types][:custom] = "don't care"
|
135
172
|
subject.options[:formatters][:custom] = lambda { |obj, env| 'CUSTOM FORMAT' }
|
136
|
-
_, _, body = subject.call(
|
173
|
+
_, _, body = subject.call('PATH_INFO' => '/info.custom')
|
137
174
|
body.body.should == ['CUSTOM FORMAT']
|
138
175
|
end
|
139
176
|
it 'uses default json formatter' do
|
140
177
|
@body = ['blah']
|
141
|
-
_, _, body = subject.call(
|
178
|
+
_, _, body = subject.call('PATH_INFO' => '/info.json')
|
142
179
|
body.body.should == ['["blah"]']
|
143
180
|
end
|
144
181
|
it 'uses custom json formatter' do
|
145
182
|
subject.options[:formatters][:json] = lambda { |obj, env| 'CUSTOM JSON FORMAT' }
|
146
|
-
_, _, body = subject.call(
|
183
|
+
_, _, body = subject.call('PATH_INFO' => '/info.json')
|
147
184
|
body.body.should == ['CUSTOM JSON FORMAT']
|
148
185
|
end
|
149
186
|
end
|
150
187
|
|
151
188
|
context 'input' do
|
152
|
-
[
|
153
|
-
[
|
189
|
+
["POST", "PATCH", "PUT", "DELETE"].each do |method|
|
190
|
+
["application/json", "application/json; charset=utf-8"].each do |content_type|
|
154
191
|
context content_type do
|
155
192
|
it 'parses the body from #{method} and copies values into rack.request.form_hash' do
|
156
193
|
io = StringIO.new('{"is_boolean":true,"string":"thing"}')
|
157
|
-
subject.call(
|
194
|
+
subject.call(
|
158
195
|
'PATH_INFO' => '/info',
|
159
196
|
'REQUEST_METHOD' => method,
|
160
197
|
'CONTENT_TYPE' => content_type,
|
161
198
|
'rack.input' => io,
|
162
199
|
'CONTENT_LENGTH' => io.length
|
163
|
-
|
164
|
-
subject.env['rack.request.form_hash']['is_boolean'].should
|
200
|
+
)
|
201
|
+
subject.env['rack.request.form_hash']['is_boolean'].should be true
|
165
202
|
subject.env['rack.request.form_hash']['string'].should == 'thing'
|
166
203
|
end
|
167
204
|
end
|
168
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
|
+
subject.env['rack.request.form_hash']['is_boolean'].should be true
|
216
|
+
subject.env['rack.request.form_hash']['string'].should == '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
|
+
subject.env['rack.request.form_hash']['is_boolean'].should be true
|
229
|
+
subject.env['rack.request.form_hash']['string'].should == 'thing'
|
230
|
+
end
|
169
231
|
it 'parses the body from an xml #{method} and copies values into rack.request.from_hash' do
|
170
232
|
io = StringIO.new('<thing><name>Test</name></thing>')
|
171
|
-
subject.call(
|
233
|
+
subject.call(
|
172
234
|
'PATH_INFO' => '/info.xml',
|
173
235
|
'REQUEST_METHOD' => method,
|
174
236
|
'CONTENT_TYPE' => 'application/xml',
|
175
237
|
'rack.input' => io,
|
176
238
|
'CONTENT_LENGTH' => io.length
|
177
|
-
|
239
|
+
)
|
178
240
|
subject.env['rack.request.form_hash']['thing']['name'].should == 'Test'
|
179
241
|
end
|
180
|
-
[
|
242
|
+
[Rack::Request::FORM_DATA_MEDIA_TYPES, Rack::Request::PARSEABLE_DATA_MEDIA_TYPES].flatten.each do |content_type|
|
181
243
|
it "ignores #{content_type}" do
|
182
244
|
io = StringIO.new('name=Other+Test+Thing')
|
183
|
-
subject.call(
|
245
|
+
subject.call(
|
184
246
|
'PATH_INFO' => '/info',
|
185
247
|
'REQUEST_METHOD' => method,
|
186
248
|
'CONTENT_TYPE' => content_type,
|
187
249
|
'rack.input' => io,
|
188
250
|
'CONTENT_LENGTH' => io.length
|
189
|
-
|
251
|
+
)
|
190
252
|
subject.env['rack.request.form_hash'].should be_nil
|
191
253
|
end
|
192
254
|
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
|
+
env['api.version'].should eql 'v1'
|
23
|
+
status.should == 200
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'is set if format provided' do
|
27
|
+
status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1')
|
28
|
+
env['api.version'].should eql 'v1'
|
29
|
+
status.should == 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
|
+
subject.call('HTTP_ACCEPT_VERSION' => '').first.should == 200
|
46
|
+
subject.call({}).first.should == 200
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'succeeds if :strict is set to false' do
|
50
|
+
@options[:version_options][:strict] = false
|
51
|
+
subject.call('HTTP_ACCEPT_VERSION' => '').first.should == 200
|
52
|
+
subject.call({}).first.should == 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
|
+
subject.call('HTTP_ACCEPT_VERSION' => 'v1').first.should == 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
|
+
subject.call('HTTP_ACCEPT_VERSION' => 'v1').first.should == 200
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|