riak-client 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/Rakefile +74 -0
  2. data/lib/riak.rb +49 -0
  3. data/lib/riak/bucket.rb +176 -0
  4. data/lib/riak/cache_store.rb +82 -0
  5. data/lib/riak/client.rb +139 -0
  6. data/lib/riak/client/curb_backend.rb +82 -0
  7. data/lib/riak/client/http_backend.rb +209 -0
  8. data/lib/riak/client/net_http_backend.rb +49 -0
  9. data/lib/riak/failed_request.rb +37 -0
  10. data/lib/riak/i18n.rb +20 -0
  11. data/lib/riak/invalid_response.rb +25 -0
  12. data/lib/riak/link.rb +73 -0
  13. data/lib/riak/locale/en.yml +37 -0
  14. data/lib/riak/map_reduce.rb +248 -0
  15. data/lib/riak/map_reduce_error.rb +20 -0
  16. data/lib/riak/robject.rb +267 -0
  17. data/lib/riak/util/escape.rb +12 -0
  18. data/lib/riak/util/fiber1.8.rb +48 -0
  19. data/lib/riak/util/headers.rb +44 -0
  20. data/lib/riak/util/multipart.rb +52 -0
  21. data/lib/riak/util/translation.rb +29 -0
  22. data/lib/riak/walk_spec.rb +117 -0
  23. data/spec/fixtures/cat.jpg +0 -0
  24. data/spec/fixtures/multipart-blank.txt +7 -0
  25. data/spec/fixtures/multipart-with-body.txt +16 -0
  26. data/spec/integration/riak/cache_store_spec.rb +129 -0
  27. data/spec/riak/bucket_spec.rb +247 -0
  28. data/spec/riak/client_spec.rb +174 -0
  29. data/spec/riak/curb_backend_spec.rb +53 -0
  30. data/spec/riak/escape_spec.rb +21 -0
  31. data/spec/riak/headers_spec.rb +34 -0
  32. data/spec/riak/http_backend_spec.rb +131 -0
  33. data/spec/riak/link_spec.rb +82 -0
  34. data/spec/riak/map_reduce_spec.rb +352 -0
  35. data/spec/riak/multipart_spec.rb +36 -0
  36. data/spec/riak/net_http_backend_spec.rb +28 -0
  37. data/spec/riak/object_spec.rb +538 -0
  38. data/spec/riak/walk_spec_spec.rb +208 -0
  39. data/spec/spec_helper.rb +30 -0
  40. data/spec/support/http_backend_implementation_examples.rb +215 -0
  41. data/spec/support/mock_server.rb +61 -0
  42. data/spec/support/mocks.rb +3 -0
  43. metadata +187 -0
@@ -0,0 +1,36 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require File.expand_path("../spec_helper", File.dirname(__FILE__))
15
+
16
+ describe Riak::Util::Multipart do
17
+ it "should extract the boundary string from a header value" do
18
+ Riak::Util::Multipart.extract_boundary("multipart/mixed; boundary=123446677890").should == "123446677890"
19
+ end
20
+
21
+ it "should parse an empty multipart body into empty arrays" do
22
+ data = File.read(File.expand_path("#{File.dirname(__FILE__)}/../fixtures/multipart-blank.txt"))
23
+ Riak::Util::Multipart.parse(data, "73NmmA8dJxSB5nL2dVerpFIi8ze").should == [[]]
24
+ end
25
+
26
+ it "should parse multipart body into nested arrays with response-like results" do
27
+ data = File.read(File.expand_path("#{File.dirname(__FILE__)}/../fixtures/multipart-with-body.txt"))
28
+ results = Riak::Util::Multipart.parse(data, "5EiMOjuGavQ2IbXAqsJPLLfJNlA")
29
+ results.should be_kind_of(Array)
30
+ results.first.should be_kind_of(Array)
31
+ obj = results.first.first
32
+ obj.should be_kind_of(Hash)
33
+ obj.should have_key(:headers)
34
+ obj.should have_key(:body)
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require File.expand_path("../spec_helper", File.dirname(__FILE__))
15
+
16
+ describe Riak::Client::NetHTTPBackend do
17
+ before :each do
18
+ @client = Riak::Client.new
19
+ @backend = Riak::Client::NetHTTPBackend.new(@client)
20
+ FakeWeb.allow_net_connect = false
21
+ end
22
+
23
+ def setup_http_mock(method, uri, options={})
24
+ FakeWeb.register_uri(method, uri, options)
25
+ end
26
+
27
+ it_should_behave_like "HTTP backend"
28
+ end
@@ -0,0 +1,538 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require File.expand_path("../spec_helper", File.dirname(__FILE__))
15
+
16
+ describe Riak::RObject do
17
+ before :each do
18
+ @client = Riak::Client.new
19
+ @bucket = Riak::Bucket.new(@client, "foo")
20
+ end
21
+
22
+ describe "initialization" do
23
+ it "should set the bucket" do
24
+ @object = Riak::RObject.new(@bucket)
25
+ @object.bucket.should == @bucket
26
+ end
27
+
28
+ it "should set the key" do
29
+ @object = Riak::RObject.new(@bucket, "bar")
30
+ @object.key.should == "bar"
31
+ end
32
+
33
+ it "should initialize the links to an empty set" do
34
+ @object = Riak::RObject.new(@bucket, "bar")
35
+ @object.links.should == Set.new
36
+ end
37
+
38
+ it "should initialize the meta to an empty hash" do
39
+ @object = Riak::RObject.new(@bucket, "bar")
40
+ @object.meta.should == {}
41
+ end
42
+
43
+ it "should yield itself to a given block" do
44
+ Riak::RObject.new(@bucket, "bar") do |r|
45
+ r.key.should == "bar"
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "serialization" do
51
+ before :each do
52
+ @object = Riak::RObject.new(@bucket, "bar")
53
+ end
54
+
55
+ it "should change the data into a string by default when serializing" do
56
+ @object.serialize("foo").should == "foo"
57
+ @object.serialize(2).should == "2"
58
+ end
59
+
60
+ it "should not change the data when it is an IO" do
61
+ file = File.open("#{File.dirname(__FILE__)}/../fixtures/cat.jpg", "r")
62
+ file.should_not_receive(:to_s)
63
+ @object.serialize(file).should == file
64
+ end
65
+
66
+ it "should not modify the data by default when deserializing" do
67
+ @object.deserialize("foo").should == "foo"
68
+ end
69
+
70
+ describe "when the content type is YAML" do
71
+ before :each do
72
+ @object.content_type = "text/x-yaml"
73
+ end
74
+
75
+ it "should serialize into a YAML stream" do
76
+ @object.serialize({"foo" => "bar"}).should == "--- \nfoo: bar\n"
77
+ end
78
+
79
+ it "should deserialize a YAML stream" do
80
+ @object.deserialize("--- \nfoo: bar\n").should == {"foo" => "bar"}
81
+ end
82
+ end
83
+
84
+ describe "when the content type is JSON" do
85
+ before :each do
86
+ @object.content_type = "application/json"
87
+ end
88
+
89
+ it "should serialize into a JSON blob" do
90
+ @object.serialize({"foo" => "bar"}).should == '{"foo":"bar"}'
91
+ @object.serialize(2).should == "2"
92
+ @object.serialize("Some text").should == '"Some text"'
93
+ @object.serialize([1,2,3]).should == "[1,2,3]"
94
+ end
95
+
96
+ it "should deserialize a JSON blob" do
97
+ @object.deserialize('{"foo":"bar"}').should == {"foo" => "bar"}
98
+ @object.deserialize("2").should == 2
99
+ @object.deserialize('"Some text"').should == "Some text"
100
+ @object.deserialize('[1,2,3]').should == [1,2,3]
101
+ end
102
+ end
103
+
104
+ describe "when the content type is an octet-stream" do
105
+ before :each do
106
+ @object.content_type = "application/octet-stream"
107
+ end
108
+
109
+ describe "if the ruby-serialization meta field is set to Marshal" do
110
+ before :each do
111
+ @object.meta['ruby-serialization'] = "Marshal"
112
+ @payload = Marshal.dump({"foo" => "bar"})
113
+ end
114
+
115
+ it "should dump via Marshal" do
116
+ @object.serialize({"foo" => "bar"}).should == @payload
117
+ end
118
+
119
+ it "should load from Marshal" do
120
+ @object.deserialize(@payload).should == {"foo" => "bar"}
121
+ end
122
+ end
123
+
124
+ describe "if the ruby-serialization meta field is not set to Marshal" do
125
+ before :each do
126
+ @object.meta.delete("ruby-serialization")
127
+ end
128
+
129
+ it "should dump to a string" do
130
+ @object.serialize(2).should == "2"
131
+ @object.serialize("foo").should == "foo"
132
+ end
133
+
134
+ it "should load the body unmodified" do
135
+ @object.deserialize("foo").should == "foo"
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ describe "loading data from the response" do
142
+ before :each do
143
+ @object = Riak::RObject.new(@bucket, "bar")
144
+ end
145
+
146
+ it "should load the content type" do
147
+ @object.load({:headers => {"content-type" => ["application/json"]}})
148
+ @object.content_type.should == "application/json"
149
+ end
150
+
151
+ it "should load the body data" do
152
+ @object.load({:headers => {"content-type" => ["application/json"]}, :body => '{"foo":"bar"}'})
153
+ @object.data.should be_present
154
+ end
155
+
156
+ it "should deserialize the body data" do
157
+ @object.should_receive(:deserialize).with("{}").and_return({})
158
+ @object.load({:headers => {"content-type" => ["application/json"]}, :body => "{}"})
159
+ @object.data.should == {}
160
+ end
161
+
162
+ it "should leave the object data unchanged if the response body is blank" do
163
+ @object.data = "Original data"
164
+ @object.load({:headers => {"content-type" => ["application/json"]}, :body => ""})
165
+ @object.data.should == "Original data"
166
+ end
167
+
168
+ it "should load the vclock from the headers" do
169
+ @object.load({:headers => {"content-type" => ["application/json"], 'x-riak-vclock' => ["somereallylongbase64string=="]}, :body => "{}"})
170
+ @object.vclock.should == "somereallylongbase64string=="
171
+ end
172
+
173
+ it "should load links from the headers" do
174
+ @object.load({:headers => {"content-type" => ["application/json"], "link" => ['</riak/bar>; rel="up"']}, :body => "{}"})
175
+ @object.links.should have(1).item
176
+ @object.links.first.url.should == "/riak/bar"
177
+ @object.links.first.rel.should == "up"
178
+ end
179
+
180
+ it "should load the ETag from the headers" do
181
+ @object.load({:headers => {"content-type" => ["application/json"], "etag" => ["32748nvas83572934"]}, :body => "{}"})
182
+ @object.etag.should == "32748nvas83572934"
183
+ end
184
+
185
+ it "should load the modified date from the headers" do
186
+ time = Time.now
187
+ @object.load({:headers => {"content-type" => ["application/json"], "last-modified" => [time.httpdate]}, :body => "{}"})
188
+ @object.last_modified.to_s.should == time.to_s # bah, times are not equivalent unless equal
189
+ end
190
+
191
+ it "should load meta information from the headers" do
192
+ @object.load({:headers => {"content-type" => ["application/json"], "x-riak-meta-some-kind-of-robot" => ["for AWESOME"]}, :body => "{}"})
193
+ @object.meta["some-kind-of-robot"].should == ["for AWESOME"]
194
+ end
195
+
196
+ it "should parse the location header into the key when present" do
197
+ @object.load({:headers => {"content-type" => ["application/json"], "location" => ["/riak/foo/baz"]}})
198
+ @object.key.should == "baz"
199
+ end
200
+
201
+ it "should be in conflict when the response code is 300 and the content-type is multipart/mixed" do
202
+ @object.load({:headers => {"content-type" => ["multipart/mixed; boundary=foo"]}, :code => 300 })
203
+ @object.should be_conflict
204
+ end
205
+
206
+ it "should unescape the key given in the location header" do
207
+ @object.load({:headers => {"content-type" => ["application/json"], "location" => ["/riak/foo/baz%20"]}})
208
+ @object.key.should == "baz "
209
+ end
210
+ end
211
+
212
+ describe "extracting siblings" do
213
+ before :each do
214
+ @object = Riak::RObject.new(@bucket, "bar").load({:headers => {"x-riak-vclock" => ["merged"], "content-type" => ["multipart/mixed; boundary=foo"]}, :code => 300, :body => "\n--foo\nContent-Type: text/plain\n\nbar\n--foo\nContent-Type: text/plain\n\nbaz\n--foo--\n"})
215
+ end
216
+
217
+ it "should extract the siblings" do
218
+ @object.should have(2).siblings
219
+ siblings = @object.siblings
220
+ siblings[0].data.should == "bar"
221
+ siblings[1].data.should == "baz"
222
+ end
223
+
224
+ it "should set the key on both siblings" do
225
+ @object.siblings.should be_all {|s| s.key == "bar" }
226
+ end
227
+
228
+ it "should set the vclock on both siblings to the merged vclock" do
229
+ @object.siblings.should be_all {|s| s.vclock == "merged" }
230
+ end
231
+ end
232
+
233
+ describe "headers used for storing the object" do
234
+ before :each do
235
+ @object = Riak::RObject.new(@bucket, "bar")
236
+ end
237
+
238
+ it "should include the content type" do
239
+ @object.content_type = "application/json"
240
+ @object.store_headers["Content-Type"].should == "application/json"
241
+ end
242
+
243
+ it "should include the vclock when present" do
244
+ @object.vclock = "123445678990"
245
+ @object.store_headers["X-Riak-Vclock"].should == "123445678990"
246
+ end
247
+
248
+ it "should exclude the vclock when nil" do
249
+ @object.vclock = nil
250
+ @object.store_headers.should_not have_key("X-Riak-Vclock")
251
+ end
252
+
253
+ describe "when links are defined" do
254
+ before :each do
255
+ @object.links << Riak::Link.new("/riak/foo/baz", "next")
256
+ end
257
+
258
+ it "should include a Link header with references to other objects" do
259
+ @object.store_headers.should have_key("Link")
260
+ @object.store_headers["Link"].should include('</riak/foo/baz>; riaktag="next"')
261
+ end
262
+
263
+ it "should exclude the 'up' link to the bucket from the header" do
264
+ @object.links << Riak::Link.new("/riak/foo", "up")
265
+ @object.store_headers.should have_key("Link")
266
+ @object.store_headers["Link"].should_not include('riaktag="up"')
267
+ end
268
+
269
+ it "should not allow duplicate links" do
270
+ @object.links << Riak::Link.new("/riak/foo/baz", "next")
271
+ @object.links.length.should == 1
272
+ end
273
+ end
274
+
275
+ it "should exclude the Link header when no links are present" do
276
+ @object.links = Set.new
277
+ @object.store_headers.should_not have_key("Link")
278
+ end
279
+
280
+ describe "when meta fields are present" do
281
+ before :each do
282
+ @object.meta = {"some-kind-of-robot" => true, "powers" => "for awesome", "cold-ones" => 10}
283
+ end
284
+
285
+ it "should include X-Riak-Meta-* headers for each meta key" do
286
+ @object.store_headers.should have_key("X-Riak-Meta-some-kind-of-robot")
287
+ @object.store_headers.should have_key("X-Riak-Meta-cold-ones")
288
+ @object.store_headers.should have_key("X-Riak-Meta-powers")
289
+ end
290
+
291
+ it "should turn non-string meta values into strings" do
292
+ @object.store_headers["X-Riak-Meta-some-kind-of-robot"].should == "true"
293
+ @object.store_headers["X-Riak-Meta-cold-ones"].should == "10"
294
+ end
295
+
296
+ it "should leave string meta values unchanged in the header" do
297
+ @object.store_headers["X-Riak-Meta-powers"].should == "for awesome"
298
+ end
299
+ end
300
+ end
301
+
302
+ describe "headers used for reloading the object" do
303
+ before :each do
304
+ @object = Riak::RObject.new(@bucket, "bar")
305
+ end
306
+
307
+ it "should be blank when the etag and last_modified properties are blank" do
308
+ @object.etag.should be_blank
309
+ @object.last_modified.should be_blank
310
+ @object.reload_headers.should be_blank
311
+ end
312
+
313
+ it "should include the If-None-Match key when the etag is present" do
314
+ @object.etag = "etag!"
315
+ @object.reload_headers['If-None-Match'].should == "etag!"
316
+ end
317
+
318
+ it "should include the If-Modified-Since header when the last_modified time is present" do
319
+ time = Time.now
320
+ @object.last_modified = time
321
+ @object.reload_headers['If-Modified-Since'].should == time.httpdate
322
+ end
323
+ end
324
+
325
+ describe "when storing the object normally" do
326
+ before :each do
327
+ @http = mock("HTTPBackend")
328
+ @client.stub!(:http).and_return(@http)
329
+ @object = Riak::RObject.new(@bucket)
330
+ @object.content_type = "text/plain"
331
+ @object.data = "This is some text."
332
+ @headers = @object.store_headers
333
+ end
334
+
335
+ it "should raise an error when the content_type is blank" do
336
+ lambda { @object.content_type = nil; @object.store }.should raise_error(ArgumentError)
337
+ lambda { @object.content_type = " "; @object.store }.should raise_error(ArgumentError)
338
+ end
339
+
340
+ describe "when the object has no key" do
341
+ it "should issue a POST request to the bucket, and update the object properties (returning the body by default)" do
342
+ @http.should_receive(:post).with(201, "/riak/", "foo", {:returnbody => true}, "This is some text.", @headers).and_return({:headers => {'location' => ["/riak/foo/somereallylongstring"], "x-riak-vclock" => ["areallylonghashvalue"]}, :code => 201})
343
+ @object.store
344
+ @object.key.should == "somereallylongstring"
345
+ @object.vclock.should == "areallylonghashvalue"
346
+ end
347
+
348
+ it "should include persistence-tuning parameters in the query string" do
349
+ @http.should_receive(:post).with(201, "/riak/", "foo", {:dw => 2, :returnbody => true}, "This is some text.", @headers).and_return({:headers => {'location' => ["/riak/foo/somereallylongstring"], "x-riak-vclock" => ["areallylonghashvalue"]}, :code => 201})
350
+ @object.store(:dw => 2)
351
+ end
352
+
353
+ it "should escape the bucket name" do
354
+ @bucket.should_receive(:name).and_return("foo ")
355
+ @http.should_receive(:post).with(201, "/riak/", "foo%20", {:returnbody => true}, "This is some text.", @headers).and_return({:headers => {'location' => ["/riak/foo/somereallylongstring"], "x-riak-vclock" => ["areallylonghashvalue"]}, :code => 201})
356
+ @object.store
357
+ end
358
+ end
359
+
360
+ describe "when the object has a key" do
361
+ before :each do
362
+ @object.key = "bar"
363
+ end
364
+
365
+ it "should issue a PUT request to the bucket, and update the object properties (returning the body by default)" do
366
+ @http.should_receive(:put).with([200,204,300], "/riak/", "foo/bar", {:returnbody => true}, "This is some text.", @headers).and_return({:headers => {'location' => ["/riak/foo/somereallylongstring"], "x-riak-vclock" => ["areallylonghashvalue"]}, :code => 204})
367
+ @object.store
368
+ @object.key.should == "somereallylongstring"
369
+ @object.vclock.should == "areallylonghashvalue"
370
+ end
371
+
372
+ it "should include persistence-tuning parameters in the query string" do
373
+ @http.should_receive(:put).with([200,204,300], "/riak/", "foo/bar", {:dw => 2, :returnbody => true}, "This is some text.", @headers).and_return({:headers => {'location' => ["/riak/foo/somereallylongstring"], "x-riak-vclock" => ["areallylonghashvalue"]}, :code => 204})
374
+ @object.store(:dw => 2)
375
+ end
376
+
377
+ it "should escape the bucket and key names" do
378
+ @http.should_receive(:put).with([200,204,300], "/riak/", "foo%20/bar%2Fbaz", {:returnbody => true}, "This is some text.", @headers).and_return({:headers => {'location' => ["/riak/foo/somereallylongstring"], "x-riak-vclock" => ["areallylonghashvalue"]}, :code => 204})
379
+ @bucket.instance_variable_set(:@name, "foo ")
380
+ @object.key = "bar/baz"
381
+ @object.store
382
+ end
383
+ end
384
+ end
385
+
386
+ describe "when reloading the object" do
387
+ before :each do
388
+ @http = mock("HTTPBackend")
389
+ @client.stub!(:http).and_return(@http)
390
+ @object = Riak::RObject.new(@bucket, "bar")
391
+ @object.vclock = "somereallylongstring"
392
+ @object.stub!(:reload_headers).and_return({})
393
+ end
394
+
395
+ it "should return without requesting if the key is blank" do
396
+ @object.key = nil
397
+ @http.should_not_receive(:get)
398
+ @object.reload
399
+ end
400
+
401
+ it "should return without requesting if the vclock is blank" do
402
+ @object.vclock = nil
403
+ @http.should_not_receive(:get)
404
+ @object.reload
405
+ end
406
+
407
+ it "should make the request if the key is present and the :force option is given" do
408
+ @http.should_receive(:get).and_return({:headers => {}, :code => 304})
409
+ @object.reload :force => true
410
+ end
411
+
412
+ it "should pass along the reload_headers" do
413
+ @headers = {"If-None-Match" => "etag"}
414
+ @object.should_receive(:reload_headers).and_return(@headers)
415
+ @http.should_receive(:get).with([200,304], "/riak/", "foo", "bar", {}, @headers).and_return({:code => 304})
416
+ @object.reload
417
+ end
418
+
419
+ it "should return without modifying the object if the response is 304 Not Modified" do
420
+ @http.should_receive(:get).and_return({:code => 304})
421
+ @object.should_not_receive(:load)
422
+ @object.reload
423
+ end
424
+
425
+ it "should raise an exception when the response code is not 200 or 304" do
426
+ @http.should_receive(:get).and_raise(Riak::FailedRequest.new(:get, 200, 500, {}, ''))
427
+ @object.should_not_receive(:load)
428
+ lambda { @object.reload }.should raise_error(Riak::FailedRequest)
429
+ end
430
+
431
+ it "should include 300 in valid responses if the bucket has allow_mult set" do
432
+ @object.bucket.should_receive(:allow_mult).and_return(true)
433
+ @http.should_receive(:get).with([200,300,304], "/riak/", "foo", "bar", {}, {}).and_return({:code => 304})
434
+ @object.reload
435
+ end
436
+
437
+ it "should escape the bucket and key names" do
438
+ @bucket.should_receive(:name).and_return("some/deep/path")
439
+ @object.key = "another/deep/path"
440
+ @http.should_receive(:get).with([200,304], "/riak/", "some%2Fdeep%2Fpath", "another%2Fdeep%2Fpath", {}, {}).and_return({:code => 304})
441
+ @object.reload
442
+ end
443
+ end
444
+
445
+ describe "walking from the object to linked objects" do
446
+ before :each do
447
+ @http = mock("HTTPBackend")
448
+ @client.stub!(:http).and_return(@http)
449
+ @client.stub!(:bucket).and_return(@bucket)
450
+ @object = Riak::RObject.new(@bucket, "bar")
451
+ @body = File.read(File.expand_path("#{File.dirname(__FILE__)}/../fixtures/multipart-with-body.txt"))
452
+ end
453
+
454
+ it "should issue a GET request to the given walk spec" do
455
+ @http.should_receive(:get).with(200, "/riak/", "foo", "bar", "_,next,1").and_return(:headers => {"content-type" => ["multipart/mixed; boundary=12345"]}, :body => "\n--12345\nContent-Type: multipart/mixed; boundary=09876\n\n--09876--\n\n--12345--\n")
456
+ @object.walk(nil,"next",true)
457
+ end
458
+
459
+ it "should parse the results into arrays of objects" do
460
+ @http.stub!(:get).and_return(:headers => {"content-type" => ["multipart/mixed; boundary=5EiMOjuGavQ2IbXAqsJPLLfJNlA"]}, :body => @body)
461
+ results = @object.walk(nil,"next",true)
462
+ results.should be_kind_of(Array)
463
+ results.first.should be_kind_of(Array)
464
+ obj = results.first.first
465
+ obj.should be_kind_of(Riak::RObject)
466
+ obj.content_type.should == "text/plain"
467
+ obj.key.should == "baz"
468
+ obj.bucket.should == @bucket
469
+ end
470
+
471
+ it "should assign the bucket for newly parsed objects" do
472
+ @http.stub!(:get).and_return(:headers => {"content-type" => ["multipart/mixed; boundary=5EiMOjuGavQ2IbXAqsJPLLfJNlA"]}, :body => @body)
473
+ @client.should_receive(:bucket).with("foo", :keys => false).and_return(@bucket)
474
+ @object.walk(nil,"next",true)
475
+ end
476
+
477
+ it "should escape the bucket, key and link specs" do
478
+ @object.key = "bar/baz"
479
+ @bucket.should_receive(:name).and_return("quin/quux")
480
+ @http.should_receive(:get).with(200, "/riak/", "quin%2Fquux", "bar%2Fbaz", "_,next%2F2,1").and_return(:headers => {"content-type" => ["multipart/mixed; boundary=12345"]}, :body => "\n--12345\nContent-Type: multipart/mixed; boundary=09876\n\n--09876--\n\n--12345--\n")
481
+ @object.walk(:tag => "next/2", :keep => true)
482
+ end
483
+ end
484
+
485
+ describe "when deleting" do
486
+ before :each do
487
+ @http = mock("HTTPBackend")
488
+ @client.stub!(:http).and_return(@http)
489
+ @object = Riak::RObject.new(@bucket, "bar")
490
+ end
491
+
492
+ it "should make a DELETE request to the Riak server and freeze the object" do
493
+ @http.should_receive(:delete).with([204,404], "/riak/", "foo", "bar", {},{}).and_return({:code => 204, :headers => {}})
494
+ @object.delete
495
+ @object.should be_frozen
496
+ end
497
+
498
+ it "should do nothing when the key is blank" do
499
+ @http.should_not_receive(:delete)
500
+ @object.key = nil
501
+ @object.delete
502
+ end
503
+
504
+ it "should pass through a failed request exception" do
505
+ @http.should_receive(:delete).and_raise(Riak::FailedRequest.new(:delete, [204,404], 500, {}, ""))
506
+ lambda { @object.delete }.should raise_error(Riak::FailedRequest)
507
+ end
508
+
509
+ it "should escape the bucket and key names" do
510
+ @object.key = "deep/path"
511
+ @bucket.should_receive(:name).and_return("bucket spaces")
512
+ @http.should_receive(:delete).with([204,404], "/riak/", "bucket%20spaces", "deep%2Fpath",{},{}).and_return({:code => 204, :headers => {}})
513
+ @object.delete
514
+ end
515
+ end
516
+
517
+ it "should convert to a link having the same url and an empty tag" do
518
+ @object = Riak::RObject.new(@bucket, "bar")
519
+ @object.to_link.should == Riak::Link.new("/riak/foo/bar", nil)
520
+ end
521
+
522
+ it "should convert to a link having the same url and a supplied tag" do
523
+ @object = Riak::RObject.new(@bucket, "bar")
524
+ @object.to_link("next").should == Riak::Link.new("/riak/foo/bar", "next")
525
+ end
526
+
527
+ it "should escape the bucket and key when converting to a link" do
528
+ @object = Riak::RObject.new(@bucket, "deep/path")
529
+ @bucket.should_receive(:name).and_return("bucket spaces")
530
+ @object.to_link("bar").url.should == "/riak/bucket%20spaces/deep%2Fpath"
531
+ end
532
+
533
+ it "should provide a useful inspect output even when the key is nil" do
534
+ @object = Riak::RObject.new(@bucket)
535
+ lambda { @object.inspect }.should_not raise_error
536
+ @object.inspect.should be_kind_of(String)
537
+ end
538
+ end