grape 0.5.0 → 0.6.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/.travis.yml +2 -6
- data/CHANGELOG.md +20 -0
- data/README.md +43 -11
- data/lib/grape.rb +6 -0
- data/lib/grape/api.rb +27 -14
- data/lib/grape/endpoint.rb +33 -34
- data/lib/grape/exceptions/base.rb +4 -2
- data/lib/grape/exceptions/validation.rb +13 -3
- data/lib/grape/exceptions/validation_errors.rb +42 -0
- data/lib/grape/http/request.rb +1 -1
- data/lib/grape/locale/en.yml +4 -3
- data/lib/grape/middleware/auth/base.rb +30 -0
- data/lib/grape/middleware/auth/basic.rb +2 -19
- data/lib/grape/middleware/auth/digest.rb +2 -19
- data/lib/grape/middleware/error.rb +10 -1
- data/lib/grape/middleware/formatter.rb +1 -1
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/middleware/versioner/header.rb +2 -2
- data/lib/grape/path.rb +72 -0
- data/lib/grape/route.rb +6 -1
- data/lib/grape/validations.rb +25 -8
- data/lib/grape/validations/coerce.rb +1 -2
- data/lib/grape/validations/presence.rb +6 -2
- data/lib/grape/validations/regexp.rb +1 -2
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +71 -6
- data/spec/grape/entity_spec.rb +5 -5
- data/spec/grape/middleware/base_spec.rb +1 -1
- data/spec/grape/middleware/formatter_spec.rb +3 -3
- data/spec/grape/middleware/versioner/header_spec.rb +25 -0
- data/spec/grape/path_spec.rb +219 -0
- data/spec/grape/validations/coerce_spec.rb +31 -13
- data/spec/grape/validations/presence_spec.rb +12 -12
- data/spec/grape/validations/zh-CN.yml +4 -3
- data/spec/grape/validations_spec.rb +154 -10
- data/spec/support/versioned_helpers.rb +5 -2
- metadata +10 -45
data/spec/grape/entity_spec.rb
CHANGED
@@ -25,7 +25,7 @@ describe Grape::Entity do
|
|
25
25
|
|
26
26
|
it 'pulls a representation from the class options if it exists' do
|
27
27
|
entity = Class.new(Grape::Entity)
|
28
|
-
entity.stub
|
28
|
+
entity.stub(:represent).and_return("Hiya")
|
29
29
|
|
30
30
|
subject.represent Object, :with => entity
|
31
31
|
subject.get '/example' do
|
@@ -37,7 +37,7 @@ describe Grape::Entity do
|
|
37
37
|
|
38
38
|
it 'pulls a representation from the class options if the presented object is a collection of objects' do
|
39
39
|
entity = Class.new(Grape::Entity)
|
40
|
-
entity.stub
|
40
|
+
entity.stub(:represent).and_return("Hiya")
|
41
41
|
|
42
42
|
class TestObject; end
|
43
43
|
class FakeCollection
|
@@ -62,7 +62,7 @@ describe Grape::Entity do
|
|
62
62
|
|
63
63
|
it 'pulls a representation from the class ancestor if it exists' do
|
64
64
|
entity = Class.new(Grape::Entity)
|
65
|
-
entity.stub
|
65
|
+
entity.stub(:represent).and_return("Hiya")
|
66
66
|
|
67
67
|
subclass = Class.new(Object)
|
68
68
|
|
@@ -77,7 +77,7 @@ describe Grape::Entity do
|
|
77
77
|
it 'automatically uses Klass::Entity if that exists' do
|
78
78
|
some_model = Class.new
|
79
79
|
entity = Class.new(Grape::Entity)
|
80
|
-
entity.stub
|
80
|
+
entity.stub(:represent).and_return("Auto-detect!")
|
81
81
|
|
82
82
|
some_model.const_set :Entity, entity
|
83
83
|
|
@@ -91,7 +91,7 @@ describe Grape::Entity do
|
|
91
91
|
it 'automatically uses Klass::Entity based on the first object in the collection being presented' do
|
92
92
|
some_model = Class.new
|
93
93
|
entity = Class.new(Grape::Entity)
|
94
|
-
entity.stub
|
94
|
+
entity.stub(:represent).and_return("Auto-detect!")
|
95
95
|
|
96
96
|
some_model.const_set :Entity, entity
|
97
97
|
|
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Grape::Middleware::Formatter do
|
4
4
|
subject{ Grape::Middleware::Formatter.new(app) }
|
5
|
-
before{ subject.stub
|
5
|
+
before{ subject.stub(:dup).and_return(subject) }
|
6
6
|
|
7
7
|
let(:app){ lambda{|env| [200, {}, [@body || { "foo" => "bar" }]]} }
|
8
8
|
|
@@ -37,7 +37,7 @@ describe Grape::Middleware::Formatter do
|
|
37
37
|
end
|
38
38
|
|
39
39
|
context 'error handling' do
|
40
|
-
let(:formatter) {
|
40
|
+
let(:formatter) { double(:formatter) }
|
41
41
|
before do
|
42
42
|
Grape::Formatter::Base.stub(:formatter_for) { formatter }
|
43
43
|
end
|
@@ -172,7 +172,7 @@ describe Grape::Middleware::Formatter do
|
|
172
172
|
end
|
173
173
|
|
174
174
|
context 'input' do
|
175
|
-
[ "POST", "PATCH", "PUT" ].each do |method|
|
175
|
+
[ "POST", "PATCH", "PUT", "DELETE" ].each do |method|
|
176
176
|
[ "application/json", "application/json; charset=utf-8" ].each do |content_type|
|
177
177
|
context content_type do
|
178
178
|
it 'parses the body from #{method} and copies values into rack.request.form_hash' do
|
@@ -274,4 +274,29 @@ describe Grape::Middleware::Versioner::Header do
|
|
274
274
|
subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first.should == 200
|
275
275
|
end
|
276
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
|
+
subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first.should == 200
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'succeeds with v2' do
|
288
|
+
subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').first.should == 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
|
277
302
|
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
describe Path do
|
5
|
+
|
6
|
+
describe "#initialize" do
|
7
|
+
it "remembers the path" do
|
8
|
+
path = Path.new('/:id', anything, anything)
|
9
|
+
expect(path.raw_path).to eql('/:id')
|
10
|
+
end
|
11
|
+
|
12
|
+
it "remembers the namespace" do
|
13
|
+
path = Path.new(anything, '/users', anything)
|
14
|
+
expect(path.namespace).to eql('/users')
|
15
|
+
end
|
16
|
+
|
17
|
+
it "remebers the settings" do
|
18
|
+
path = Path.new(anything, anything, foo: 'bar')
|
19
|
+
expect(path.settings).to eql(foo: 'bar')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#mount_path" do
|
24
|
+
it "is nil when no mount path setting exists" do
|
25
|
+
path = Path.new(anything, anything, {})
|
26
|
+
expect(path.mount_path).to be_nil
|
27
|
+
end
|
28
|
+
|
29
|
+
it "is nil when the mount path is nil" do
|
30
|
+
path = Path.new(anything, anything, mount_path: nil)
|
31
|
+
expect(path.mount_path).to be_nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it "splits the mount path" do
|
35
|
+
path = Path.new(anything, anything, mount_path: 'foo/bar')
|
36
|
+
expect(path.mount_path).to eql(['foo', 'bar'])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#root_prefix" do
|
41
|
+
it "is nil when no root prefix setting exists" do
|
42
|
+
path = Path.new(anything, anything, {})
|
43
|
+
expect(path.root_prefix).to be_nil
|
44
|
+
end
|
45
|
+
|
46
|
+
it "is nil when the mount path is nil" do
|
47
|
+
path = Path.new(anything, anything, root_prefix: nil)
|
48
|
+
expect(path.root_prefix).to be_nil
|
49
|
+
end
|
50
|
+
|
51
|
+
it "splits the mount path" do
|
52
|
+
path = Path.new(anything, anything, root_prefix: 'hello/world')
|
53
|
+
expect(path.root_prefix).to eql(['hello', 'world'])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#uses_path_versioning?" do
|
58
|
+
it "is false when the version setting is nil" do
|
59
|
+
path = Path.new(anything, anything, version: nil)
|
60
|
+
expect(path.uses_path_versioning?).to be_false
|
61
|
+
end
|
62
|
+
|
63
|
+
it "is false when the version option is header" do
|
64
|
+
path = Path.new(anything, anything, {
|
65
|
+
version: 'v1',
|
66
|
+
version_options: { using: :header }
|
67
|
+
})
|
68
|
+
|
69
|
+
expect(path.uses_path_versioning?).to be_false
|
70
|
+
end
|
71
|
+
|
72
|
+
it "is true when the version option is path" do
|
73
|
+
path = Path.new(anything, anything, {
|
74
|
+
version: 'v1',
|
75
|
+
version_options: { using: :path }
|
76
|
+
})
|
77
|
+
|
78
|
+
expect(path.uses_path_versioning?).to be_true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#has_namespace?" do
|
83
|
+
it "is false when the namespace is nil" do
|
84
|
+
path = Path.new(anything, nil, anything)
|
85
|
+
expect(path).not_to have_namespace
|
86
|
+
end
|
87
|
+
|
88
|
+
it "is false when the namespace starts with whitespace" do
|
89
|
+
path = Path.new(anything, ' /foo', anything)
|
90
|
+
expect(path).not_to have_namespace
|
91
|
+
end
|
92
|
+
|
93
|
+
it "is false when the namespace is the root path" do
|
94
|
+
path = Path.new(anything, '/', anything)
|
95
|
+
expect(path).not_to have_namespace
|
96
|
+
end
|
97
|
+
|
98
|
+
it "is true otherwise" do
|
99
|
+
path = Path.new(anything, '/world', anything)
|
100
|
+
expect(path).to have_namespace
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "#has_path?" do
|
105
|
+
it "is false when the path is nil" do
|
106
|
+
path = Path.new(nil, anything, anything)
|
107
|
+
expect(path).not_to have_path
|
108
|
+
end
|
109
|
+
|
110
|
+
it "is false when the path starts with whitespace" do
|
111
|
+
path = Path.new(' /foo', anything, anything)
|
112
|
+
expect(path).not_to have_path
|
113
|
+
end
|
114
|
+
|
115
|
+
it "is false when the path is the root path" do
|
116
|
+
path = Path.new('/', anything, anything)
|
117
|
+
expect(path).not_to have_path
|
118
|
+
end
|
119
|
+
|
120
|
+
it "is true otherwise" do
|
121
|
+
path = Path.new('/hello', anything, anything)
|
122
|
+
expect(path).to have_path
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "#path" do
|
127
|
+
context "mount_path" do
|
128
|
+
it "is not included when it is nil" do
|
129
|
+
path = Path.new(nil, nil, { mount_path: '/foo/bar' })
|
130
|
+
expect(path.path).to eql '/foo/bar'
|
131
|
+
end
|
132
|
+
|
133
|
+
it "is included when it is not nil" do
|
134
|
+
path = Path.new(nil, nil, {})
|
135
|
+
expect(path.path).to eql('/')
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context "root_prefix" do
|
140
|
+
it "is not included when it is nil" do
|
141
|
+
path = Path.new(nil, nil, {})
|
142
|
+
expect(path.path).to eql('/')
|
143
|
+
end
|
144
|
+
|
145
|
+
it "is included after the mount path" do
|
146
|
+
path = Path.new(nil, nil, {
|
147
|
+
mount_path: '/foo',
|
148
|
+
root_prefix: '/hello'
|
149
|
+
})
|
150
|
+
|
151
|
+
expect(path.path).to eql('/foo/hello')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
it "uses the namespace after the mount path and root prefix" do
|
156
|
+
path = Path.new(nil, 'namespace', {
|
157
|
+
mount_path: '/foo',
|
158
|
+
root_prefix: '/hello'
|
159
|
+
})
|
160
|
+
|
161
|
+
expect(path.path).to eql('/foo/hello/namespace')
|
162
|
+
end
|
163
|
+
|
164
|
+
it "uses the raw path after the namespace" do
|
165
|
+
path = Path.new('raw_path', 'namespace', {
|
166
|
+
mount_path: '/foo',
|
167
|
+
root_prefix: '/hello'
|
168
|
+
})
|
169
|
+
|
170
|
+
expect(path.path).to eql('/foo/hello/namespace/raw_path')
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe "#suffix" do
|
175
|
+
context "when path versioning is used" do
|
176
|
+
it "includes a '/'" do
|
177
|
+
path = Path.new(nil, nil, {})
|
178
|
+
path.stub(:uses_path_versioning?) { true }
|
179
|
+
|
180
|
+
expect(path.suffix).to eql('(/.:format)')
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context "when path versioning is not used" do
|
185
|
+
it "does not include a '/' when the path has a namespace" do
|
186
|
+
path = Path.new(nil, 'namespace', {})
|
187
|
+
path.stub(:uses_path_versioning?) { true }
|
188
|
+
|
189
|
+
expect(path.suffix).to eql('(.:format)')
|
190
|
+
end
|
191
|
+
|
192
|
+
it "does not include a '/' when the path has a path" do
|
193
|
+
path = Path.new('/path', nil, {})
|
194
|
+
path.stub(:uses_path_versioning?) { true }
|
195
|
+
|
196
|
+
expect(path.suffix).to eql('(.:format)')
|
197
|
+
end
|
198
|
+
|
199
|
+
it "includes a '/' otherwise" do
|
200
|
+
path = Path.new(nil, nil, {})
|
201
|
+
path.stub(:uses_path_versioning?) { true }
|
202
|
+
|
203
|
+
expect(path.suffix).to eql('(/.:format)')
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
describe "#path_with_suffix" do
|
209
|
+
it "combines the path and suffix" do
|
210
|
+
path = Path.new(nil, nil, {})
|
211
|
+
path.stub(:path) { '/the/path' }
|
212
|
+
path.stub(:suffix) { 'suffix' }
|
213
|
+
|
214
|
+
expect(path.path_with_suffix).to eql('/the/pathsuffix')
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
end
|
@@ -6,26 +6,44 @@ describe Grape::Validations::CoerceValidator do
|
|
6
6
|
def app; subject end
|
7
7
|
|
8
8
|
describe 'coerce' do
|
9
|
-
it "i18n error on malformed input" do
|
10
|
-
I18n.load_path << File.expand_path('../zh-CN.yml',__FILE__)
|
11
|
-
I18n.reload!
|
12
|
-
I18n.locale = :'zh-CN'
|
13
|
-
subject.params { requires :age, :type => Integer }
|
14
|
-
subject.get '/single' do 'int works'; end
|
15
9
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
10
|
+
context "i18n" do
|
11
|
+
|
12
|
+
after :each do
|
13
|
+
I18n.locale = :en
|
14
|
+
end
|
15
|
+
|
16
|
+
it "i18n error on malformed input" do
|
17
|
+
I18n.load_path << File.expand_path('../zh-CN.yml',__FILE__)
|
18
|
+
I18n.reload!
|
19
|
+
I18n.locale = :'zh-CN'
|
20
|
+
subject.params { requires :age, :type => Integer }
|
21
|
+
subject.get '/single' do 'int works'; end
|
22
|
+
|
23
|
+
get '/single', :age => '43a'
|
24
|
+
last_response.status.should == 400
|
25
|
+
last_response.body.should == '年龄格式不正确'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'gives an english fallback error when default locale message is blank' do
|
29
|
+
I18n.locale = :'pt-BR'
|
30
|
+
subject.params { requires :age, :type => Integer }
|
31
|
+
subject.get '/single' do 'int works'; end
|
32
|
+
|
33
|
+
get '/single', :age => '43a'
|
34
|
+
last_response.status.should == 400
|
35
|
+
last_response.body.should == 'age is invalid'
|
36
|
+
end
|
20
37
|
|
21
38
|
end
|
39
|
+
|
22
40
|
it 'error on malformed input' do
|
23
41
|
subject.params { requires :int, :type => Integer }
|
24
42
|
subject.get '/single' do 'int works'; end
|
25
43
|
|
26
44
|
get '/single', :int => '43a'
|
27
45
|
last_response.status.should == 400
|
28
|
-
last_response.body.should == '
|
46
|
+
last_response.body.should == 'int is invalid'
|
29
47
|
|
30
48
|
get '/single', :int => '43'
|
31
49
|
last_response.status.should == 200
|
@@ -38,7 +56,7 @@ describe Grape::Validations::CoerceValidator do
|
|
38
56
|
|
39
57
|
get 'array', { :ids => ['1', '2', 'az'] }
|
40
58
|
last_response.status.should == 400
|
41
|
-
last_response.body.should == '
|
59
|
+
last_response.body.should == 'ids is invalid'
|
42
60
|
|
43
61
|
get 'array', { :ids => ['1', '2', '890'] }
|
44
62
|
last_response.status.should == 200
|
@@ -60,7 +78,7 @@ describe Grape::Validations::CoerceValidator do
|
|
60
78
|
|
61
79
|
get '/user', :user => "32"
|
62
80
|
last_response.status.should == 400
|
63
|
-
last_response.body.should == '
|
81
|
+
last_response.body.should == 'user is invalid'
|
64
82
|
|
65
83
|
get '/user', :user => { :id => 32, :name => 'Bob' }
|
66
84
|
last_response.status.should == 200
|
@@ -66,13 +66,13 @@ describe Grape::Validations::PresenceValidator do
|
|
66
66
|
it 'validates id' do
|
67
67
|
post '/'
|
68
68
|
last_response.status.should == 400
|
69
|
-
last_response.body.should == '{"error":"
|
69
|
+
last_response.body.should == '{"error":"id is missing"}'
|
70
70
|
|
71
71
|
io = StringIO.new('{"id" : "a56b"}')
|
72
72
|
post '/', {}, 'rack.input' => io,
|
73
73
|
'CONTENT_TYPE' => 'application/json',
|
74
74
|
'CONTENT_LENGTH' => io.length
|
75
|
-
last_response.body.should == '{"error":"
|
75
|
+
last_response.body.should == '{"error":"id is invalid"}'
|
76
76
|
last_response.status.should == 400
|
77
77
|
|
78
78
|
io = StringIO.new('{"id" : 56}')
|
@@ -86,11 +86,11 @@ describe Grape::Validations::PresenceValidator do
|
|
86
86
|
it 'validates name, company' do
|
87
87
|
get('/')
|
88
88
|
last_response.status.should == 400
|
89
|
-
last_response.body.should == '{"error":"
|
89
|
+
last_response.body.should == '{"error":"name is missing"}'
|
90
90
|
|
91
91
|
get('/', :name => "Bob")
|
92
92
|
last_response.status.should == 400
|
93
|
-
last_response.body.should == '{"error":"
|
93
|
+
last_response.body.should == '{"error":"company is missing"}'
|
94
94
|
|
95
95
|
get('/', :name => "Bob", :company => "TestCorp")
|
96
96
|
last_response.status.should == 200
|
@@ -100,11 +100,11 @@ describe Grape::Validations::PresenceValidator do
|
|
100
100
|
it 'validates nested parameters' do
|
101
101
|
get('/nested')
|
102
102
|
last_response.status.should == 400
|
103
|
-
last_response.body.should == '{"error":"
|
103
|
+
last_response.body.should == '{"error":"user[first_name] is missing"}'
|
104
104
|
|
105
105
|
get('/nested', :user => {:first_name => "Billy"})
|
106
106
|
last_response.status.should == 400
|
107
|
-
last_response.body.should == '{"error":"
|
107
|
+
last_response.body.should == '{"error":"user[last_name] is missing"}'
|
108
108
|
|
109
109
|
get('/nested', :user => {:first_name => "Billy", :last_name => "Bob"})
|
110
110
|
last_response.status.should == 200
|
@@ -114,27 +114,27 @@ describe Grape::Validations::PresenceValidator do
|
|
114
114
|
it 'validates triple nested parameters' do
|
115
115
|
get('/nested_triple')
|
116
116
|
last_response.status.should == 400
|
117
|
-
last_response.body.should == '{"error":"missing
|
117
|
+
last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
|
118
118
|
|
119
119
|
get('/nested_triple', :user => {:first_name => "Billy"})
|
120
120
|
last_response.status.should == 400
|
121
|
-
last_response.body.should == '{"error":"missing
|
121
|
+
last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
|
122
122
|
|
123
123
|
get('/nested_triple', :admin => {:super => {:first_name => "Billy"}})
|
124
124
|
last_response.status.should == 400
|
125
|
-
last_response.body.should == '{"error":"missing
|
125
|
+
last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
|
126
126
|
|
127
127
|
get('/nested_triple', :super => {:user => {:first_name => "Billy", :last_name => "Bob"}})
|
128
128
|
last_response.status.should == 400
|
129
|
-
last_response.body.should == '{"error":"missing
|
129
|
+
last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
|
130
130
|
|
131
131
|
get('/nested_triple', :admin => {:super => {:user => {:first_name => "Billy"}}})
|
132
132
|
last_response.status.should == 400
|
133
|
-
last_response.body.should == '{"error":"missing
|
133
|
+
last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][last_name] is missing"}'
|
134
134
|
|
135
135
|
get('/nested_triple', :admin => { :admin_name => 'admin', :super => {:user => {:first_name => "Billy"}}})
|
136
136
|
last_response.status.should == 400
|
137
|
-
last_response.body.should == '{"error":"
|
137
|
+
last_response.body.should == '{"error":"admin[super][user][last_name] is missing"}'
|
138
138
|
|
139
139
|
get('/nested_triple', :admin => { :admin_name => 'admin', :super => {:user => {:first_name => "Billy", :last_name => "Bob"}}})
|
140
140
|
last_response.status.should == 200
|