grape 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of grape might be problematic. Click here for more details.
- checksums.yaml +5 -13
- data/.rubocop.yml +6 -6
- data/.travis.yml +11 -2
- data/CHANGELOG.md +23 -1
- data/Gemfile +11 -10
- data/Guardfile +5 -6
- data/README.md +194 -13
- data/UPGRADING.md +3 -3
- data/grape.gemspec +1 -1
- data/grape.png +0 -0
- data/lib/grape/api.rb +21 -13
- data/lib/grape/endpoint.rb +31 -13
- data/lib/grape/exceptions/validation.rb +2 -2
- data/lib/grape/locale/en.yml +2 -0
- data/lib/grape/middleware/auth/oauth2.rb +69 -65
- data/lib/grape/middleware/error.rb +4 -2
- data/lib/grape/middleware/formatter.rb +2 -2
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/middleware/versioner/header.rb +3 -3
- data/lib/grape/util/hash_stack.rb +1 -1
- data/lib/grape/validations.rb +22 -8
- data/lib/grape/validations/default.rb +1 -1
- 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 +1 -1
- data/lib/grape/validations/regexp.rb +2 -1
- data/lib/grape/validations/values.rb +7 -1
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +390 -333
- data/spec/grape/endpoint_spec.rb +129 -99
- data/spec/grape/entity_spec.rb +47 -27
- data/spec/grape/exceptions/invalid_formatter_spec.rb +1 -1
- data/spec/grape/exceptions/invalid_versioner_option_spec.rb +1 -1
- data/spec/grape/exceptions/missing_mime_type_spec.rb +2 -2
- data/spec/grape/exceptions/missing_option_spec.rb +1 -1
- data/spec/grape/exceptions/unknown_options_spec.rb +1 -1
- data/spec/grape/exceptions/unknown_validator_spec.rb +1 -1
- data/spec/grape/middleware/auth/basic_spec.rb +3 -3
- data/spec/grape/middleware/auth/digest_spec.rb +4 -4
- data/spec/grape/middleware/auth/oauth2_spec.rb +11 -11
- data/spec/grape/middleware/base_spec.rb +9 -9
- data/spec/grape/middleware/error_spec.rb +4 -4
- data/spec/grape/middleware/exception_spec.rb +17 -17
- data/spec/grape/middleware/formatter_spec.rb +38 -38
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +11 -11
- data/spec/grape/middleware/versioner/header_spec.rb +39 -39
- data/spec/grape/middleware/versioner/param_spec.rb +10 -10
- data/spec/grape/middleware/versioner/path_spec.rb +7 -7
- data/spec/grape/middleware/versioner_spec.rb +4 -4
- data/spec/grape/path_spec.rb +6 -6
- data/spec/grape/util/hash_stack_spec.rb +22 -22
- data/spec/grape/validations/coerce_spec.rb +34 -34
- data/spec/grape/validations/default_spec.rb +16 -16
- 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 +34 -34
- data/spec/grape/validations/regexp_spec.rb +11 -4
- data/spec/grape/validations/values_spec.rb +34 -20
- data/spec/grape/validations_spec.rb +300 -147
- data/spec/shared/versioning_examples.rb +18 -18
- data/spec/spec_helper.rb +2 -1
- metadata +81 -38
data/spec/grape/entity_spec.rb
CHANGED
@@ -28,19 +28,19 @@ describe Grape::Entity do
|
|
28
28
|
|
29
29
|
it 'pulls a representation from the class options if it exists' do
|
30
30
|
entity = Class.new(Grape::Entity)
|
31
|
-
entity.
|
31
|
+
allow(entity).to receive(:represent).and_return("Hiya")
|
32
32
|
|
33
33
|
subject.represent Object, with: entity
|
34
34
|
subject.get '/example' do
|
35
35
|
present Object.new
|
36
36
|
end
|
37
37
|
get '/example'
|
38
|
-
last_response.body.
|
38
|
+
expect(last_response.body).to eq('Hiya')
|
39
39
|
end
|
40
40
|
|
41
41
|
it 'pulls a representation from the class options if the presented object is a collection of objects' do
|
42
42
|
entity = Class.new(Grape::Entity)
|
43
|
-
entity.
|
43
|
+
allow(entity).to receive(:represent).and_return("Hiya")
|
44
44
|
|
45
45
|
class TestObject
|
46
46
|
end
|
@@ -61,15 +61,15 @@ describe Grape::Entity do
|
|
61
61
|
end
|
62
62
|
|
63
63
|
get '/example'
|
64
|
-
last_response.body.
|
64
|
+
expect(last_response.body).to eq("Hiya")
|
65
65
|
|
66
66
|
get '/example2'
|
67
|
-
last_response.body.
|
67
|
+
expect(last_response.body).to eq("Hiya")
|
68
68
|
end
|
69
69
|
|
70
70
|
it 'pulls a representation from the class ancestor if it exists' do
|
71
71
|
entity = Class.new(Grape::Entity)
|
72
|
-
entity.
|
72
|
+
allow(entity).to receive(:represent).and_return("Hiya")
|
73
73
|
|
74
74
|
subclass = Class.new(Object)
|
75
75
|
|
@@ -78,13 +78,13 @@ describe Grape::Entity do
|
|
78
78
|
present subclass.new
|
79
79
|
end
|
80
80
|
get '/example'
|
81
|
-
last_response.body.
|
81
|
+
expect(last_response.body).to eq('Hiya')
|
82
82
|
end
|
83
83
|
|
84
84
|
it 'automatically uses Klass::Entity if that exists' do
|
85
85
|
some_model = Class.new
|
86
86
|
entity = Class.new(Grape::Entity)
|
87
|
-
entity.
|
87
|
+
allow(entity).to receive(:represent).and_return("Auto-detect!")
|
88
88
|
|
89
89
|
some_model.const_set :Entity, entity
|
90
90
|
|
@@ -92,13 +92,13 @@ describe Grape::Entity do
|
|
92
92
|
present some_model.new
|
93
93
|
end
|
94
94
|
get '/example'
|
95
|
-
last_response.body.
|
95
|
+
expect(last_response.body).to eq('Auto-detect!')
|
96
96
|
end
|
97
97
|
|
98
98
|
it 'automatically uses Klass::Entity based on the first object in the collection being presented' do
|
99
99
|
some_model = Class.new
|
100
100
|
entity = Class.new(Grape::Entity)
|
101
|
-
entity.
|
101
|
+
allow(entity).to receive(:represent).and_return("Auto-detect!")
|
102
102
|
|
103
103
|
some_model.const_set :Entity, entity
|
104
104
|
|
@@ -106,7 +106,7 @@ describe Grape::Entity do
|
|
106
106
|
present [some_model.new]
|
107
107
|
end
|
108
108
|
get '/example'
|
109
|
-
last_response.body.
|
109
|
+
expect(last_response.body).to eq('Auto-detect!')
|
110
110
|
end
|
111
111
|
|
112
112
|
it 'does not run autodetection for Entity when explicitely provided' do
|
@@ -117,10 +117,30 @@ describe Grape::Entity do
|
|
117
117
|
present some_array, with: entity
|
118
118
|
end
|
119
119
|
|
120
|
-
some_array.
|
120
|
+
expect(some_array).not_to receive(:first)
|
121
121
|
get '/example'
|
122
122
|
end
|
123
123
|
|
124
|
+
it 'does not use #first method on ActiveRecord::Relation to prevent needless sql query' do
|
125
|
+
entity = Class.new(Grape::Entity)
|
126
|
+
some_relation = Class.new
|
127
|
+
some_model = Class.new
|
128
|
+
|
129
|
+
allow(entity).to receive(:represent).and_return("Auto-detect!")
|
130
|
+
allow(some_relation).to receive(:first)
|
131
|
+
allow(some_relation).to receive(:klass).and_return(some_model)
|
132
|
+
|
133
|
+
some_model.const_set :Entity, entity
|
134
|
+
|
135
|
+
subject.get '/example' do
|
136
|
+
present some_relation
|
137
|
+
end
|
138
|
+
|
139
|
+
expect(some_relation).not_to receive(:first)
|
140
|
+
get '/example'
|
141
|
+
expect(last_response.body).to eq('Auto-detect!')
|
142
|
+
end
|
143
|
+
|
124
144
|
it 'autodetection does not use Entity if it is not a presenter' do
|
125
145
|
some_model = Class.new
|
126
146
|
entity = Class.new
|
@@ -131,7 +151,7 @@ describe Grape::Entity do
|
|
131
151
|
present some_model
|
132
152
|
end
|
133
153
|
get '/example'
|
134
|
-
entity.
|
154
|
+
expect(entity).not_to receive(:represent)
|
135
155
|
end
|
136
156
|
|
137
157
|
it 'adds a root key to the output if one is given' do
|
@@ -161,8 +181,8 @@ describe Grape::Entity do
|
|
161
181
|
end
|
162
182
|
|
163
183
|
get '/example'
|
164
|
-
last_response.status.
|
165
|
-
last_response.body.
|
184
|
+
expect(last_response.status).to eq(200)
|
185
|
+
expect(last_response.body).to eq('{"example":{"id":1}}')
|
166
186
|
end
|
167
187
|
|
168
188
|
it 'presents with #{format} collection' do
|
@@ -183,8 +203,8 @@ describe Grape::Entity do
|
|
183
203
|
end
|
184
204
|
|
185
205
|
get '/examples'
|
186
|
-
last_response.status.
|
187
|
-
last_response.body.
|
206
|
+
expect(last_response.status).to eq(200)
|
207
|
+
expect(last_response.body).to eq('{"examples":[{"id":1},{"id":2}]}')
|
188
208
|
end
|
189
209
|
|
190
210
|
end
|
@@ -206,9 +226,9 @@ describe Grape::Entity do
|
|
206
226
|
present c.new(name: "johnnyiller"), with: entity
|
207
227
|
end
|
208
228
|
get '/example'
|
209
|
-
last_response.status.
|
210
|
-
last_response.headers['Content-type'].
|
211
|
-
last_response.body.
|
229
|
+
expect(last_response.status).to eq(200)
|
230
|
+
expect(last_response.headers['Content-type']).to eq("application/xml")
|
231
|
+
expect(last_response.body).to eq <<-XML
|
212
232
|
<?xml version="1.0" encoding="UTF-8"?>
|
213
233
|
<hash>
|
214
234
|
<example>
|
@@ -235,9 +255,9 @@ XML
|
|
235
255
|
present c.new(name: "johnnyiller"), with: entity
|
236
256
|
end
|
237
257
|
get '/example'
|
238
|
-
last_response.status.
|
239
|
-
last_response.headers['Content-type'].
|
240
|
-
last_response.body.
|
258
|
+
expect(last_response.status).to eq(200)
|
259
|
+
expect(last_response.headers['Content-type']).to eq("application/json")
|
260
|
+
expect(last_response.body).to eq('{"example":{"name":"johnnyiller"}}')
|
241
261
|
end
|
242
262
|
|
243
263
|
it 'presents with jsonp utilising Rack::JSONP' do
|
@@ -265,9 +285,9 @@ XML
|
|
265
285
|
end
|
266
286
|
|
267
287
|
get '/example?callback=abcDef'
|
268
|
-
last_response.status.
|
269
|
-
last_response.headers['Content-type'].
|
270
|
-
last_response.body.
|
288
|
+
expect(last_response.status).to eq(200)
|
289
|
+
expect(last_response.headers['Content-type']).to eq("application/javascript")
|
290
|
+
expect(last_response.body).to eq('abcDef({"example":{"name":"johnnyiller"}})')
|
271
291
|
end
|
272
292
|
|
273
293
|
context "present with multiple entities" do
|
@@ -296,7 +316,7 @@ XML
|
|
296
316
|
"user1" => { "name" => "user1" },
|
297
317
|
"user2" => { "name" => "user2" }
|
298
318
|
}
|
299
|
-
JSON(last_response.body).
|
319
|
+
expect(JSON(last_response.body)).to eq(expect_response_json)
|
300
320
|
end
|
301
321
|
|
302
322
|
end
|
@@ -8,11 +8,11 @@ describe Grape::Exceptions::MissingMimeType do
|
|
8
8
|
end
|
9
9
|
|
10
10
|
it "contains the problem in the message" do
|
11
|
-
error.message.
|
11
|
+
expect(error.message).to include "missing mime type for new_json"
|
12
12
|
end
|
13
13
|
|
14
14
|
it "contains the resolution in the message" do
|
15
|
-
error.message.
|
15
|
+
expect(error.message).to include "or add your own with content_type :new_json, 'application/new_json' "
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
@@ -16,16 +16,16 @@ describe Grape::Middleware::Auth::Basic do
|
|
16
16
|
it 'throws a 401 if no auth is given' do
|
17
17
|
@proc = lambda { false }
|
18
18
|
get '/whatever'
|
19
|
-
last_response.status.
|
19
|
+
expect(last_response.status).to eq(401)
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'authenticates if given valid creds' do
|
23
23
|
get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin')
|
24
|
-
last_response.status.
|
24
|
+
expect(last_response.status).to eq(200)
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'throws a 401 is wrong auth is given' do
|
28
28
|
get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong')
|
29
|
-
last_response.status.
|
29
|
+
expect(last_response.status).to eq(401)
|
30
30
|
end
|
31
31
|
end
|
@@ -25,23 +25,23 @@ describe Grape::Middleware::Auth::Digest do
|
|
25
25
|
|
26
26
|
it 'is a digest authentication challenge' do
|
27
27
|
get '/test'
|
28
|
-
last_response.
|
28
|
+
expect(last_response).to be_challenge
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'throws a 401 if no auth is given' do
|
32
32
|
get '/test'
|
33
|
-
last_response.status.
|
33
|
+
expect(last_response.status).to eq(401)
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'authenticates if given valid creds' do
|
37
37
|
digest_authorize "foo", "bar"
|
38
38
|
get '/test'
|
39
|
-
last_response.status.
|
39
|
+
expect(last_response.status).to eq(200)
|
40
40
|
end
|
41
41
|
|
42
42
|
it 'throws a 401 if given invalid creds' do
|
43
43
|
digest_authorize "bar", "foo"
|
44
44
|
get '/test'
|
45
|
-
last_response.status.
|
45
|
+
expect(last_response.status).to eq(401)
|
46
46
|
end
|
47
47
|
end
|
@@ -33,7 +33,7 @@ describe Grape::Middleware::Auth::OAuth2 do
|
|
33
33
|
before { get '/awesome?access_token=g123' }
|
34
34
|
|
35
35
|
it 'sets env["api.token"]' do
|
36
|
-
last_response.body.
|
36
|
+
expect(last_response.body).to eq('g123')
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
@@ -45,11 +45,11 @@ describe Grape::Middleware::Auth::OAuth2 do
|
|
45
45
|
end
|
46
46
|
|
47
47
|
it 'throws an error' do
|
48
|
-
@err[:status].
|
48
|
+
expect(@err[:status]).to eq(401)
|
49
49
|
end
|
50
50
|
|
51
51
|
it 'sets the WWW-Authenticate header in the response' do
|
52
|
-
@err[:headers]['WWW-Authenticate'].
|
52
|
+
expect(@err[:headers]['WWW-Authenticate']).to eq("OAuth realm='OAuth API', error='invalid_grant'")
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|
@@ -62,11 +62,11 @@ describe Grape::Middleware::Auth::OAuth2 do
|
|
62
62
|
end
|
63
63
|
|
64
64
|
it 'throws an error' do
|
65
|
-
@err[:status].
|
65
|
+
expect(@err[:status]).to eq(401)
|
66
66
|
end
|
67
67
|
|
68
68
|
it 'sets the WWW-Authenticate header in the response to error' do
|
69
|
-
@err[:headers]['WWW-Authenticate'].
|
69
|
+
expect(@err[:headers]['WWW-Authenticate']).to eq("OAuth realm='OAuth API', error='invalid_grant'")
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
@@ -77,7 +77,7 @@ describe Grape::Middleware::Auth::OAuth2 do
|
|
77
77
|
end
|
78
78
|
|
79
79
|
it 'sets env["api.token"]' do
|
80
|
-
last_response.body.
|
80
|
+
expect(last_response.body).to eq('g123')
|
81
81
|
end
|
82
82
|
end
|
83
83
|
end
|
@@ -88,7 +88,7 @@ describe Grape::Middleware::Auth::OAuth2 do
|
|
88
88
|
end
|
89
89
|
|
90
90
|
it 'sets env["api.token"]' do
|
91
|
-
last_response.body.
|
91
|
+
expect(last_response.body).to eq('g123')
|
92
92
|
end
|
93
93
|
end
|
94
94
|
|
@@ -100,11 +100,11 @@ describe Grape::Middleware::Auth::OAuth2 do
|
|
100
100
|
end
|
101
101
|
|
102
102
|
it 'throws an error' do
|
103
|
-
@err[:status].
|
103
|
+
expect(@err[:status]).to eq(403)
|
104
104
|
end
|
105
105
|
|
106
106
|
it 'sets the WWW-Authenticate header in the response to error' do
|
107
|
-
@err[:headers]['WWW-Authenticate'].
|
107
|
+
expect(@err[:headers]['WWW-Authenticate']).to eq("OAuth realm='OAuth API', error='insufficient_scope'")
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
@@ -120,7 +120,7 @@ describe Grape::Middleware::Auth::OAuth2 do
|
|
120
120
|
before { post '/awesome' }
|
121
121
|
|
122
122
|
it 'succeeds anyway' do
|
123
|
-
last_response.status.
|
123
|
+
expect(last_response.status).to eq(200)
|
124
124
|
end
|
125
125
|
end
|
126
126
|
|
@@ -128,7 +128,7 @@ describe Grape::Middleware::Auth::OAuth2 do
|
|
128
128
|
before { get '/awesome?access_token=g123' }
|
129
129
|
|
130
130
|
it 'sets env["api.token"]' do
|
131
|
-
last_response.body.
|
131
|
+
expect(last_response.body).to eq('g123')
|
132
132
|
end
|
133
133
|
end
|
134
134
|
end
|
@@ -6,24 +6,24 @@ describe Grape::Middleware::Base do
|
|
6
6
|
|
7
7
|
before do
|
8
8
|
# Keep it one object for testing.
|
9
|
-
subject.
|
9
|
+
allow(subject).to receive(:dup).and_return(subject)
|
10
10
|
end
|
11
11
|
|
12
12
|
it 'has the app as an accessor' do
|
13
|
-
subject.app.
|
13
|
+
expect(subject.app).to eq(blank_app)
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'calls through to the app' do
|
17
|
-
subject.call({}).
|
17
|
+
expect(subject.call({})).to eq([200, {}, 'Hi there.'])
|
18
18
|
end
|
19
19
|
|
20
20
|
context 'callbacks' do
|
21
21
|
it 'calls #before' do
|
22
|
-
subject.
|
22
|
+
expect(subject).to receive(:before)
|
23
23
|
end
|
24
24
|
|
25
25
|
it 'calls #after' do
|
26
|
-
subject.
|
26
|
+
expect(subject).to receive(:after)
|
27
27
|
end
|
28
28
|
|
29
29
|
after { subject.call!({}) }
|
@@ -31,12 +31,12 @@ describe Grape::Middleware::Base do
|
|
31
31
|
|
32
32
|
it 'is able to access the response' do
|
33
33
|
subject.call({})
|
34
|
-
subject.response.
|
34
|
+
expect(subject.response).to be_kind_of(Rack::Response)
|
35
35
|
end
|
36
36
|
|
37
37
|
context 'options' do
|
38
38
|
it 'persists options passed at initialization' do
|
39
|
-
Grape::Middleware::Base.new(blank_app, abc: true).options[:abc].
|
39
|
+
expect(Grape::Middleware::Base.new(blank_app, abc: true).options[:abc]).to be true
|
40
40
|
end
|
41
41
|
|
42
42
|
context 'defaults' do
|
@@ -47,11 +47,11 @@ describe Grape::Middleware::Base do
|
|
47
47
|
end
|
48
48
|
|
49
49
|
it 'persists the default options' do
|
50
|
-
ExampleWare.new(blank_app).options[:monkey].
|
50
|
+
expect(ExampleWare.new(blank_app).options[:monkey]).to be true
|
51
51
|
end
|
52
52
|
|
53
53
|
it 'overrides default options when provided' do
|
54
|
-
ExampleWare.new(blank_app, monkey: false).options[:monkey].
|
54
|
+
expect(ExampleWare.new(blank_app, monkey: false).options[:monkey]).to be false
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
@@ -22,24 +22,24 @@ describe Grape::Middleware::Error do
|
|
22
22
|
it 'sets the status code appropriately' do
|
23
23
|
ErrApp.error = { status: 410 }
|
24
24
|
get '/'
|
25
|
-
last_response.status.
|
25
|
+
expect(last_response.status).to eq(410)
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'sets the error message appropriately' do
|
29
29
|
ErrApp.error = { message: 'Awesome stuff.' }
|
30
30
|
get '/'
|
31
|
-
last_response.body.
|
31
|
+
expect(last_response.body).to eq('Awesome stuff.')
|
32
32
|
end
|
33
33
|
|
34
34
|
it 'defaults to a 500 status' do
|
35
35
|
ErrApp.error = {}
|
36
36
|
get '/'
|
37
|
-
last_response.status.
|
37
|
+
expect(last_response.status).to eq(500)
|
38
38
|
end
|
39
39
|
|
40
40
|
it 'has a default message' do
|
41
41
|
ErrApp.error = {}
|
42
42
|
get '/'
|
43
|
-
last_response.body.
|
43
|
+
expect(last_response.body).to eq('Aww, hamburgers.')
|
44
44
|
end
|
45
45
|
end
|