seomoz-riak-client 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/Gemfile +27 -0
  2. data/Guardfile +14 -0
  3. data/Rakefile +76 -0
  4. data/erl_src/riak_kv_test_backend.beam +0 -0
  5. data/erl_src/riak_kv_test_backend.erl +174 -0
  6. data/erl_src/riak_search_test_backend.beam +0 -0
  7. data/erl_src/riak_search_test_backend.erl +175 -0
  8. data/lib/active_support/cache/riak_store.rb +2 -0
  9. data/lib/riak.rb +21 -0
  10. data/lib/riak/bucket.rb +215 -0
  11. data/lib/riak/cache_store.rb +84 -0
  12. data/lib/riak/client.rb +415 -0
  13. data/lib/riak/client/beefcake/messages.rb +147 -0
  14. data/lib/riak/client/beefcake/object_methods.rb +92 -0
  15. data/lib/riak/client/beefcake_protobuffs_backend.rb +176 -0
  16. data/lib/riak/client/excon_backend.rb +65 -0
  17. data/lib/riak/client/http_backend.rb +203 -0
  18. data/lib/riak/client/http_backend/configuration.rb +46 -0
  19. data/lib/riak/client/http_backend/key_streamer.rb +43 -0
  20. data/lib/riak/client/http_backend/object_methods.rb +94 -0
  21. data/lib/riak/client/http_backend/request_headers.rb +34 -0
  22. data/lib/riak/client/http_backend/transport_methods.rb +218 -0
  23. data/lib/riak/client/net_http_backend.rb +79 -0
  24. data/lib/riak/client/protobuffs_backend.rb +97 -0
  25. data/lib/riak/client/pump.rb +30 -0
  26. data/lib/riak/client/search.rb +94 -0
  27. data/lib/riak/core_ext.rb +6 -0
  28. data/lib/riak/core_ext/blank.rb +53 -0
  29. data/lib/riak/core_ext/extract_options.rb +7 -0
  30. data/lib/riak/core_ext/json.rb +15 -0
  31. data/lib/riak/core_ext/slice.rb +18 -0
  32. data/lib/riak/core_ext/stringify_keys.rb +10 -0
  33. data/lib/riak/core_ext/symbolize_keys.rb +10 -0
  34. data/lib/riak/core_ext/to_param.rb +31 -0
  35. data/lib/riak/encoding.rb +6 -0
  36. data/lib/riak/failed_request.rb +81 -0
  37. data/lib/riak/i18n.rb +3 -0
  38. data/lib/riak/json.rb +28 -0
  39. data/lib/riak/link.rb +85 -0
  40. data/lib/riak/locale/en.yml +48 -0
  41. data/lib/riak/map_reduce.rb +206 -0
  42. data/lib/riak/map_reduce/filter_builder.rb +94 -0
  43. data/lib/riak/map_reduce/phase.rb +98 -0
  44. data/lib/riak/map_reduce_error.rb +7 -0
  45. data/lib/riak/robject.rb +290 -0
  46. data/lib/riak/search.rb +3 -0
  47. data/lib/riak/serializers.rb +74 -0
  48. data/lib/riak/stamp.rb +77 -0
  49. data/lib/riak/test_server.rb +252 -0
  50. data/lib/riak/util/escape.rb +45 -0
  51. data/lib/riak/util/fiber1.8.rb +48 -0
  52. data/lib/riak/util/headers.rb +53 -0
  53. data/lib/riak/util/multipart.rb +52 -0
  54. data/lib/riak/util/multipart/stream_parser.rb +62 -0
  55. data/lib/riak/util/tcp_socket_extensions.rb +58 -0
  56. data/lib/riak/util/translation.rb +19 -0
  57. data/lib/riak/walk_spec.rb +105 -0
  58. data/riak-client.gemspec +55 -0
  59. data/seomoz-riak-client.gemspec +55 -0
  60. data/spec/fixtures/cat.jpg +0 -0
  61. data/spec/fixtures/multipart-blank.txt +7 -0
  62. data/spec/fixtures/multipart-mapreduce.txt +10 -0
  63. data/spec/fixtures/multipart-with-body.txt +16 -0
  64. data/spec/fixtures/server.cert.crt +15 -0
  65. data/spec/fixtures/server.cert.key +15 -0
  66. data/spec/fixtures/test.pem +1 -0
  67. data/spec/integration/riak/cache_store_spec.rb +154 -0
  68. data/spec/integration/riak/http_backends_spec.rb +58 -0
  69. data/spec/integration/riak/protobuffs_backends_spec.rb +32 -0
  70. data/spec/integration/riak/test_server_spec.rb +161 -0
  71. data/spec/riak/beefcake_protobuffs_backend_spec.rb +59 -0
  72. data/spec/riak/bucket_spec.rb +205 -0
  73. data/spec/riak/client_spec.rb +517 -0
  74. data/spec/riak/core_ext/to_param_spec.rb +15 -0
  75. data/spec/riak/escape_spec.rb +69 -0
  76. data/spec/riak/excon_backend_spec.rb +64 -0
  77. data/spec/riak/headers_spec.rb +38 -0
  78. data/spec/riak/http_backend/configuration_spec.rb +51 -0
  79. data/spec/riak/http_backend/object_methods_spec.rb +217 -0
  80. data/spec/riak/http_backend/transport_methods_spec.rb +117 -0
  81. data/spec/riak/http_backend_spec.rb +269 -0
  82. data/spec/riak/link_spec.rb +71 -0
  83. data/spec/riak/map_reduce/filter_builder_spec.rb +32 -0
  84. data/spec/riak/map_reduce/phase_spec.rb +136 -0
  85. data/spec/riak/map_reduce_spec.rb +310 -0
  86. data/spec/riak/multipart_spec.rb +23 -0
  87. data/spec/riak/net_http_backend_spec.rb +16 -0
  88. data/spec/riak/robject_spec.rb +427 -0
  89. data/spec/riak/search_spec.rb +178 -0
  90. data/spec/riak/serializers_spec.rb +93 -0
  91. data/spec/riak/stamp_spec.rb +54 -0
  92. data/spec/riak/stream_parser_spec.rb +53 -0
  93. data/spec/riak/walk_spec_spec.rb +195 -0
  94. data/spec/spec_helper.rb +39 -0
  95. data/spec/support/drb_mock_server.rb +39 -0
  96. data/spec/support/http_backend_implementation_examples.rb +266 -0
  97. data/spec/support/integration_setup.rb +10 -0
  98. data/spec/support/mock_server.rb +81 -0
  99. data/spec/support/mocks.rb +4 -0
  100. data/spec/support/test_server.yml.example +2 -0
  101. data/spec/support/unified_backend_examples.rb +255 -0
  102. metadata +271 -0
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe Riak::Util::Multipart do
4
+ it "should extract the boundary string from a header value" do
5
+ Riak::Util::Multipart.extract_boundary("multipart/mixed; boundary=123446677890").should == "123446677890"
6
+ end
7
+
8
+ it "should parse an empty multipart body into empty arrays" do
9
+ data = File.read(File.expand_path("#{File.dirname(__FILE__)}/../fixtures/multipart-blank.txt"))
10
+ Riak::Util::Multipart.parse(data, "73NmmA8dJxSB5nL2dVerpFIi8ze").should == [[]]
11
+ end
12
+
13
+ it "should parse multipart body into nested arrays with response-like results" do
14
+ data = File.read(File.expand_path("#{File.dirname(__FILE__)}/../fixtures/multipart-with-body.txt"))
15
+ results = Riak::Util::Multipart.parse(data, "5EiMOjuGavQ2IbXAqsJPLLfJNlA")
16
+ results.should be_kind_of(Array)
17
+ results.first.should be_kind_of(Array)
18
+ obj = results.first.first
19
+ obj.should be_kind_of(Hash)
20
+ obj.should have_key(:headers)
21
+ obj.should have_key(:body)
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Riak::Client::NetHTTPBackend do
4
+ before :each do
5
+ @client = Riak::Client.new(:http_backend => :NetHTTP)
6
+ @backend = @client.http
7
+ FakeWeb.allow_net_connect = false
8
+ end
9
+
10
+ def setup_http_mock(method, uri, options={})
11
+ FakeWeb.register_uri(method, uri, options)
12
+ end
13
+
14
+ it_should_behave_like "HTTP backend"
15
+
16
+ end
@@ -0,0 +1,427 @@
1
+ require 'spec_helper'
2
+
3
+ describe Riak::RObject do
4
+ before :each do
5
+ @client = Riak::Client.new
6
+ @bucket = Riak::Bucket.new(@client, "foo")
7
+ end
8
+
9
+ describe "initialization" do
10
+ it "should set the bucket" do
11
+ @object = Riak::RObject.new(@bucket)
12
+ @object.bucket.should == @bucket
13
+ end
14
+
15
+ it "should set the key" do
16
+ @object = Riak::RObject.new(@bucket, "bar")
17
+ @object.key.should == "bar"
18
+ end
19
+
20
+ it "should initialize the links to an empty set" do
21
+ @object = Riak::RObject.new(@bucket, "bar")
22
+ @object.links.should == Set.new
23
+ end
24
+
25
+ it "should initialize the meta to an empty hash" do
26
+ @object = Riak::RObject.new(@bucket, "bar")
27
+ @object.meta.should == {}
28
+ end
29
+
30
+ it "should yield itself to a given block" do
31
+ Riak::RObject.new(@bucket, "bar") do |r|
32
+ r.key.should == "bar"
33
+ end
34
+ end
35
+ end
36
+
37
+ describe "serialization" do
38
+ before :each do
39
+ @object = Riak::RObject.new(@bucket, "bar")
40
+ end
41
+
42
+ it 'delegates #serialize to the appropriate serializer for the content type' do
43
+ @object.content_type = 'text/plain'
44
+ Riak::Serializers.should respond_to(:serialize).with(2).arguments
45
+ Riak::Serializers.should_receive(:serialize).with('text/plain', "foo").and_return("serialized foo")
46
+ @object.serialize("foo").should == "serialized foo"
47
+ end
48
+
49
+ it 'delegates #deserialize to the appropriate serializer for the content type' do
50
+ @object.content_type = 'text/plain'
51
+ Riak::Serializers.should respond_to(:deserialize).with(2).arguments
52
+ Riak::Serializers.should_receive(:deserialize).with('text/plain', "foo").and_return("deserialized foo")
53
+ @object.deserialize("foo").should == "deserialized foo"
54
+ end
55
+ end
56
+
57
+ describe "data access methods" do
58
+ before :each do
59
+ @object = Riak::RObject.new(@bucket, "bar")
60
+ @object.content_type = "application/json"
61
+ end
62
+
63
+ describe "for raw data" do
64
+ describe "when unserialized data was already provided" do
65
+ before do
66
+ @object.data = { 'some' => 'data' }
67
+ end
68
+
69
+ it "should reset unserialized forms when stored" do
70
+ @object.raw_data = value = '{ "raw": "json" }'
71
+
72
+ @object.raw_data.should == value
73
+ @object.data.should == { "raw" => "json" }
74
+ end
75
+
76
+ it "should lazily serialize when read" do
77
+ @object.raw_data.should == '{"some":"data"}'
78
+ end
79
+ end
80
+
81
+ it "should not unnecessarily marshal/demarshal" do
82
+ @object.should_not_receive(:serialize)
83
+ @object.should_not_receive(:deserialize)
84
+ @object.raw_data = value = "{not even valid json!}}"
85
+ @object.raw_data.should == value
86
+ end
87
+ end
88
+
89
+ describe "for unserialized data" do
90
+ describe "when raw data was already provided" do
91
+ before do
92
+ @object.raw_data = '{"some":"data"}'
93
+ end
94
+
95
+ it "should reset previously stored raw data" do
96
+ @object.data = value = { "new" => "data" }
97
+ @object.raw_data.should == '{"new":"data"}'
98
+ @object.data.should == value
99
+ end
100
+
101
+ it "should lazily deserialize when read" do
102
+ @object.data.should == { "some" => "data" }
103
+ end
104
+
105
+ context 'for an IO-like object' do
106
+ let(:io_object) { stub(:read => 'the io object') }
107
+
108
+ it 'reads the object before deserializing it' do
109
+ @object.should_receive(:deserialize).with('the io object').and_return('deserialized')
110
+ @object.raw_data = io_object
111
+ @object.data.should == 'deserialized'
112
+ end
113
+
114
+ it 'does not allow it to be assigned directly to data since it should be assigned to raw_data instead' do
115
+ expect {
116
+ @object.data = io_object
117
+ }.to raise_error(ArgumentError)
118
+ end
119
+ end
120
+ end
121
+
122
+ it "should not unnecessarily marshal/demarshal" do
123
+ @object.should_not_receive(:serialize)
124
+ @object.should_not_receive(:deserialize)
125
+ @object.data = value = { "some" => "data" }
126
+ @object.data.should == value
127
+ end
128
+ end
129
+ end
130
+
131
+
132
+ describe "instantiating new object from a map reduce operation" do
133
+ before :each do
134
+ @client.stub!(:[]).and_return(@bucket)
135
+
136
+ @sample_response = [
137
+ {"bucket"=>"users",
138
+ "key"=>"A2IbUQ2KEMbe4WGtdL97LoTi1DN%5B%28%5C%2F%29%5D",
139
+ "vclock"=> "a85hYGBgzmDKBVIsCfs+fc9gSN9wlA8q/hKosDpIOAsA",
140
+ "values"=> [
141
+ {"metadata"=>
142
+ {"Links"=>[["addresses", "A2cbUQ2KEMbeyWGtdz97LoTi1DN", "home_address"]],
143
+ "X-Riak-VTag"=>"5bnavU3rrubcxLI8EvFXhB",
144
+ "content-type"=>"application/json",
145
+ "X-Riak-Last-Modified"=>"Mon, 12 Jul 2010 21:37:43 GMT",
146
+ "X-Riak-Meta"=>{"X-Riak-Meta-King-Of-Robots"=>"I"}},
147
+ "data"=>
148
+ "{\"email\":\"mail@test.com\",\"_type\":\"User\"}"
149
+ }
150
+ ]
151
+ }
152
+ ]
153
+ @object = Riak::RObject.load_from_mapreduce(@client,@sample_response).first
154
+ @object.should be_kind_of(Riak::RObject)
155
+ end
156
+
157
+ it "should load the content type" do
158
+ @object.content_type.should == "application/json"
159
+ end
160
+
161
+ it "should load the body data" do
162
+ @object.data.should be_present
163
+ end
164
+
165
+ it "should deserialize the body data" do
166
+ @object.data.should == {"email" => "mail@test.com", "_type" => "User"}
167
+ end
168
+
169
+ it "should set the vclock" do
170
+ @object.vclock.should == "a85hYGBgzmDKBVIsCfs+fc9gSN9wlA8q/hKosDpIOAsA"
171
+ end
172
+
173
+ it "should load and parse links" do
174
+ @object.links.should have(1).item
175
+ @object.links.first.url.should == "/riak/addresses/A2cbUQ2KEMbeyWGtdz97LoTi1DN"
176
+ @object.links.first.rel.should == "home_address"
177
+ end
178
+
179
+ it "should set the ETag" do
180
+ @object.etag.should == "5bnavU3rrubcxLI8EvFXhB"
181
+ end
182
+
183
+ it "should set modified date" do
184
+ @object.last_modified.to_i.should == Time.httpdate("Mon, 12 Jul 2010 21:37:43 GMT").to_i
185
+ end
186
+
187
+ it "should load meta information" do
188
+ @object.meta["King-Of-Robots"].should == ["I"]
189
+ end
190
+
191
+ it "should set the key" do
192
+ @object.key.should == "A2IbUQ2KEMbe4WGtdL97LoTi1DN[(\\/)]"
193
+ end
194
+
195
+ it "should not set conflict when there is none" do
196
+ @object.conflict?.should be_false
197
+ end
198
+
199
+ describe "when there are multiple values in an object" do
200
+ before :each do
201
+ response = @sample_response.dup
202
+ response[0]['values'] << {
203
+ "metadata"=> {
204
+ "Links"=>[],
205
+ "X-Riak-VTag"=>"7jDZLdu0fIj2iRsjGD8qq8",
206
+ "content-type"=>"application/json",
207
+ "X-Riak-Last-Modified"=>"Mon, 14 Jul 2010 19:28:27 GMT",
208
+ "X-Riak-Meta"=>[]
209
+ },
210
+ "data"=> "{\"email\":\"mail@domain.com\",\"_type\":\"User\"}"
211
+ }
212
+ @object = Riak::RObject.load_from_mapreduce( @client, response ).first
213
+ end
214
+
215
+ it "should expose siblings" do
216
+ @object.should have(2).siblings
217
+ @object.siblings[0].etag.should == "5bnavU3rrubcxLI8EvFXhB"
218
+ @object.siblings[1].etag.should == "7jDZLdu0fIj2iRsjGD8qq8"
219
+ end
220
+
221
+ it "should be in conflict" do
222
+ @object.data.should_not be_present
223
+ @object.should be_conflict
224
+ end
225
+
226
+ it "should assign the same vclock to all the siblings" do
227
+ @object.siblings.should be_all {|s| s.vclock == @object.vclock }
228
+ end
229
+ end
230
+ end
231
+
232
+ it "should not allow duplicate links" do
233
+ @object = Riak::RObject.new(@bucket, "foo")
234
+ @object.links << Riak::Link.new("/riak/foo/baz", "next")
235
+ @object.links << Riak::Link.new("/riak/foo/baz", "next")
236
+ @object.links.length.should == 1
237
+ end
238
+
239
+ describe "when storing the object normally" do
240
+ before :each do
241
+ @backend = mock("Backend")
242
+ @client.stub!(:backend).and_return(@backend)
243
+ @object = Riak::RObject.new(@bucket)
244
+ @object.content_type = "text/plain"
245
+ @object.data = "This is some text."
246
+ # @headers = @object.store_headers
247
+ end
248
+
249
+ it "should raise an error when the content_type is blank" do
250
+ lambda { @object.content_type = nil; @object.store }.should raise_error(ArgumentError)
251
+ lambda { @object.content_type = " "; @object.store }.should raise_error(ArgumentError)
252
+ end
253
+
254
+ it "should pass along quorum parameters and returnbody to the backend" do
255
+ @backend.should_receive(:store_object).with(@object, false, 3, 2).and_return(true)
256
+ @object.store(:returnbody => false, :w => 3, :dw => 2)
257
+ end
258
+ end
259
+
260
+ describe "when reloading the object" do
261
+ before :each do
262
+ @backend = mock("Backend")
263
+ @client.stub!(:backend).and_return(@backend)
264
+ @object = Riak::RObject.new(@bucket, "bar")
265
+ @object.vclock = "somereallylongstring"
266
+ end
267
+
268
+ it "should return without requesting if the key is blank" do
269
+ @object.key = nil
270
+ @backend.should_not_receive(:reload_object)
271
+ @object.reload
272
+ end
273
+
274
+ it "should return without requesting if the vclock is blank" do
275
+ @object.vclock = nil
276
+ @backend.should_not_receive(:reload_object)
277
+ @object.reload
278
+ end
279
+
280
+ it "should reload the object if the key is present" do
281
+ @backend.should_receive(:reload_object).with(@object, nil).and_return(@object)
282
+ @object.reload
283
+ end
284
+
285
+ it "should pass along the requested R quorum" do
286
+ @backend.should_receive(:reload_object).with(@object, 2).and_return(@object)
287
+ @object.reload :r => 2
288
+ end
289
+
290
+ it "should disable matching conditions if the key is present and the :force option is given" do
291
+ @backend.should_receive(:reload_object) do |obj, _|
292
+ obj.etag.should be_nil
293
+ obj.last_modified.should be_nil
294
+ obj
295
+ end
296
+ @object.reload :force => true
297
+ end
298
+ end
299
+
300
+ describe "walking from the object to linked objects" do
301
+ before :each do
302
+ @http = mock("HTTPBackend")
303
+ @client.stub!(:http).and_return(@http)
304
+ @client.stub!(:bucket).and_return(@bucket)
305
+ @object = Riak::RObject.new(@bucket, "bar")
306
+ end
307
+
308
+ it "should normalize the walk specs and submit the link-walking request to the HTTP backend" do
309
+ @http.should_receive(:link_walk).with(@object, [instance_of(Riak::WalkSpec)]).and_return([])
310
+ @object.walk(nil,"next",true).should == []
311
+ end
312
+ end
313
+
314
+ describe "when deleting" do
315
+ before :each do
316
+ @backend = mock("Backend")
317
+ @client.stub!(:backend).and_return(@backend)
318
+ @object = Riak::RObject.new(@bucket, "bar")
319
+ end
320
+
321
+ it "should make a DELETE request to the Riak server and freeze the object" do
322
+ @backend.should_receive(:delete_object).with(@bucket, "bar", nil)
323
+ @object.delete
324
+ @object.should be_frozen
325
+ end
326
+
327
+ it "should do nothing when the key is blank" do
328
+ @backend.should_not_receive(:delete_object)
329
+ @object.key = nil
330
+ @object.delete
331
+ end
332
+
333
+ it "should pass through a failed request exception" do
334
+ @backend.should_receive(:delete_object).and_raise(Riak::HTTPFailedRequest.new(:delete, [204,404], 500, {}, ""))
335
+ lambda { @object.delete }.should raise_error(Riak::FailedRequest)
336
+ end
337
+ end
338
+
339
+ it "should not convert to link without a tag" do
340
+ @object = Riak::RObject.new(@bucket, "bar")
341
+ lambda { @object.to_link }.should raise_error
342
+ end
343
+
344
+ it "should convert to a link having the same url and a supplied tag" do
345
+ @object = Riak::RObject.new(@bucket, "bar")
346
+ @object.to_link("next").should == Riak::Link.new("/riak/foo/bar", "next")
347
+ end
348
+
349
+ it "should escape the bucket and key when converting to a link" do
350
+ @object = Riak::RObject.new(@bucket, "deep/path")
351
+ @bucket.should_receive(:name).and_return("bucket spaces")
352
+ @object.to_link("bar").url.should == "/riak/bucket%20spaces/deep%2Fpath"
353
+ end
354
+
355
+ describe "#inspect" do
356
+ let(:object) { Riak::RObject.new(@bucket) }
357
+
358
+ it "provides useful output even when the key is nil" do
359
+ expect { object.inspect }.not_to raise_error
360
+ object.inspect.should be_kind_of(String)
361
+ end
362
+
363
+ it 'uses the serializer output in inspect' do
364
+ object.raw_data = { 'a' => 7 }
365
+ object.content_type = 'inspect/type'
366
+ Riak::Serializers['inspect/type'] = Object.new.tap do |o|
367
+ def o.load(object); "serialize for inspect"; end
368
+ end
369
+
370
+ object.inspect.should =~ /serialize for inspect/
371
+ end
372
+ end
373
+
374
+ describe '.on_conflict' do
375
+ it 'adds the hook to the list of on conflict hooks' do
376
+ hook_run = false
377
+ described_class.on_conflict_hooks.should be_empty
378
+ described_class.on_conflict { hook_run = true }
379
+ described_class.on_conflict_hooks.size.should == 1
380
+ described_class.on_conflict_hooks.first.call
381
+ hook_run.should == true
382
+ end
383
+ end
384
+
385
+ describe '#attempt_conflict_resolution' do
386
+ let(:conflicted_robject) { Riak::RObject.new(@bucket, "conflicted") { |r| r.conflict = true } }
387
+ let(:resolved_robject) { Riak::RObject.new(@bucket, "resolved") }
388
+ let(:invoked_resolvers) { [] }
389
+ let(:resolver_1) { lambda { |r| invoked_resolvers << :resolver_1; nil } }
390
+ let(:resolver_2) { lambda { |r| invoked_resolvers << :resolver_2; :not_an_robject } }
391
+ let(:resolver_3) { lambda { |r| invoked_resolvers << :resolver_3; r } }
392
+ let(:resolver_4) { lambda { |r| invoked_resolvers << :resolver_4; resolved_robject } }
393
+
394
+ before(:each) do
395
+ described_class.on_conflict(&resolver_1)
396
+ described_class.on_conflict(&resolver_2)
397
+ end
398
+
399
+ it 'calls each resolver until one of them returns an robject' do
400
+ described_class.on_conflict(&resolver_3)
401
+ described_class.on_conflict(&resolver_4)
402
+ conflicted_robject.attempt_conflict_resolution
403
+ invoked_resolvers.should == [:resolver_1, :resolver_2, :resolver_3]
404
+ end
405
+
406
+ it 'returns the robject returned by the last invoked resolver' do
407
+ described_class.on_conflict(&resolver_4)
408
+ conflicted_robject.attempt_conflict_resolution.should be(resolved_robject)
409
+ end
410
+
411
+ it 'allows the resolver to return the original robject' do
412
+ described_class.on_conflict(&resolver_3)
413
+ conflicted_robject.attempt_conflict_resolution.should be(conflicted_robject)
414
+ end
415
+
416
+ it 'returns the robject and does not call any resolvers if the robject is not in conflict' do
417
+ resolved_robject.attempt_conflict_resolution.should be(resolved_robject)
418
+ invoked_resolvers.should == []
419
+ end
420
+
421
+ it 'returns the original robject if none of the resolvers returns an robject' do
422
+ conflicted_robject.attempt_conflict_resolution.should be(conflicted_robject)
423
+ invoked_resolvers.should == [:resolver_1, :resolver_2]
424
+ end
425
+ end
426
+ end
427
+