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