require 'spec_helper' describe Riak::RObject do before :each do @client = Riak::Client.new @bucket = Riak::Bucket.new(@client, "foo") end describe "initialization" do it "should set the bucket" do @object = Riak::RObject.new(@bucket) expect(@object.bucket).to eq(@bucket) end it "should set the key" do @object = Riak::RObject.new(@bucket, "bar") expect(@object.key).to eq("bar") end it "should initialize the links to an empty set" do @object = Riak::RObject.new(@bucket, "bar") expect(@object.links).to eq(Set.new) end it "should initialize the meta to an empty hash" do @object = Riak::RObject.new(@bucket, "bar") expect(@object.meta).to eq({}) end it "should initialize indexes to an empty hash with a Set for the default value" do @object = Riak::RObject.new(@bucket, "bar") expect(@object.indexes).to be_kind_of(Hash) expect(@object.indexes).to be_empty expect(@object.indexes['foo_bin']).to be_kind_of(Set) end it "should yield itself to a given block" do Riak::RObject.new(@bucket, "bar") do |r| expect(r.key).to eq("bar") end end end describe "serialization" do before :each do @object = Riak::RObject.new(@bucket, "bar") end it 'delegates #serialize to the appropriate serializer for the content type' do @object.content_type = 'text/plain' expect(Riak::Serializers).to respond_to(:serialize).with(2).arguments expect(Riak::Serializers).to receive(:serialize).with('text/plain', "foo").and_return("serialized foo") expect(@object.content.serialize("foo")).to eq("serialized foo") end it 'delegates #deserialize to the appropriate serializer for the content type' do @object.content_type = 'text/plain' expect(Riak::Serializers).to respond_to(:deserialize).with(2).arguments expect(Riak::Serializers).to receive(:deserialize).with('text/plain', "foo").and_return("deserialized foo") expect(@object.content.deserialize("foo")).to eq("deserialized foo") end end describe "data access methods" do before :each do @object = Riak::RObject.new(@bucket, "bar") @object.content_type = "application/json" end describe "for raw data" do describe "when unserialized data was already provided" do before do @object.data = { 'some' => 'data' } end it "should reset unserialized forms when stored" do @object.raw_data = value = '{ "raw": "json" }' expect(@object.raw_data).to eq(value) expect(@object.data).to eq({ "raw" => "json" }) end it "should lazily serialize when read" do expect(@object.raw_data).to eq('{"some":"data"}') end end it "should not unnecessarily marshal/demarshal" do expect(@object).not_to receive(:serialize) expect(@object).not_to receive(:deserialize) @object.raw_data = value = "{not even valid json!}}" expect(@object.raw_data).to eq(value) end end describe "for unserialized data" do describe "when raw data was already provided" do before do @object.raw_data = '{"some":"data"}' end it "should reset previously stored raw data" do @object.data = value = { "new" => "data" } expect(@object.raw_data).to eq('{"new":"data"}') expect(@object.data).to eq(value) end it "should lazily deserialize when read" do expect(@object.data).to eq({ "some" => "data" }) end context 'for an IO-like object' do let(:io_object) { double(:read => 'the io object') } it 'reads the object before deserializing it' do expect(@object.content).to receive(:deserialize).with('the io object').and_return('deserialized') @object.raw_data = io_object expect(@object.data).to eq('deserialized') end it 'does not allow it to be assigned directly to data since it should be assigned to raw_data instead' do expect { @object.data = io_object }.to raise_error(ArgumentError) end end end it "should not unnecessarily marshal/demarshal" do expect(@object).not_to receive(:serialize) expect(@object).not_to receive(:deserialize) @object.data = value = { "some" => "data" } expect(@object.data).to eq(value) end end end describe "instantiating new object from a map reduce operation" do before :each do allow(@client).to receive(:[]).and_return(@bucket) @sample_response = [ {"bucket"=>"users", "key"=>"A2IbUQ2KEMbe4WGtdL97LoTi1DN%5B%28%5C%2F%29%5D", "vclock"=> "a85hYGBgzmDKBVIsCfs+fc9gSN9wlA8q/hKosDpIOAsA", "values"=> [ {"metadata"=> {"Links"=>[["addresses", "A2cbUQ2KEMbeyWGtdz97LoTi1DN", "home_address"]], "X-Riak-VTag"=>"5bnavU3rrubcxLI8EvFXhB", "content-type"=>"application/json", "X-Riak-Last-Modified"=>"Mon, 12 Jul 2010 21:37:43 GMT", "X-Riak-Meta"=>{"X-Riak-Meta-King-Of-Robots"=>"I"}, "index" => { "email_bin" => ["sean@basho.com","seancribbs@gmail.com"], "rank_int" => 50 } }, "data"=> "{\"email\":\"mail@test.com\",\"_type\":\"User\"}" } ] } ] @object = Riak::RObject.load_from_mapreduce(@client,@sample_response).first expect(@object).to be_kind_of(Riak::RObject) end it "should load the content type" do expect(@object.content_type).to eq("application/json") end it "should load the body data" do expect(@object.data).to be_present end it "should deserialize the body data" do expect(@object.data).to eq({"email" => "mail@test.com", "_type" => "User"}) end it "should set the vclock" do expect(@object.vclock).to eq("a85hYGBgzmDKBVIsCfs+fc9gSN9wlA8q/hKosDpIOAsA") end it "should load and parse links" do expect(@object.links.size).to eq(1) expect(@object.links.first.url).to eq("/riak/addresses/A2cbUQ2KEMbeyWGtdz97LoTi1DN") expect(@object.links.first.rel).to eq("home_address") end it "should load and parse indexes" do expect(@object.indexes.size).to eq(2) expect(@object.indexes['email_bin'].size).to eq(2) expect(@object.indexes['rank_int'].size).to eq(1) end it "should set the ETag" do expect(@object.etag).to eq("5bnavU3rrubcxLI8EvFXhB") end it "should set modified date" do expect(@object.last_modified.to_i).to eq(Time.httpdate("Mon, 12 Jul 2010 21:37:43 GMT").to_i) end it "should load meta information" do expect(@object.meta["King-Of-Robots"]).to eq(["I"]) end it "should set the key" do expect(@object.key).to eq("A2IbUQ2KEMbe4WGtdL97LoTi1DN[(\\/)]") end it "should not set conflict when there is none" do expect(@object.conflict?).to be_falsey end it 'should return [RContent] for siblings' do expect(@object.siblings).to eq([@object.content]) end describe "when there are multiple values in an object" do before :each do response = @sample_response.dup response[0]['values'] << { "metadata"=> { "Links"=>[], "X-Riak-VTag"=>"7jDZLdu0fIj2iRsjGD8qq8", "content-type"=>"application/json", "X-Riak-Last-Modified"=>"Mon, 14 Jul 2010 19:28:27 GMT", "X-Riak-Meta"=>[] }, "data"=> "{\"email\":\"mail@domain.com\",\"_type\":\"User\"}" } @object = Riak::RObject.load_from_mapreduce( @client, response ).first end it "should expose siblings" do expect(@object.siblings.size).to eq(2) expect(@object.siblings[0].etag).to eq("5bnavU3rrubcxLI8EvFXhB") expect(@object.siblings[1].etag).to eq("7jDZLdu0fIj2iRsjGD8qq8") end it "should be in conflict" do expect { @object.data }.to raise_error(Riak::Conflict) expect(@object).to be_conflict end end end it "should not allow duplicate links" do @object = Riak::RObject.new(@bucket, "foo") @object.links << Riak::Link.new("/riak/foo/baz", "next") @object.links << Riak::Link.new("/riak/foo/baz", "next") expect(@object.links.length).to eq(1) end it "should allow mass-overwriting indexes while preserving default behavior" do @object = described_class.new(@bucket, 'foo') @object.indexes = {"ts_int" => [12345], "foo_bin" => "bar"} expect(@object.indexes['ts_int']).to eq(Set.new([12345])) expect(@object.indexes['foo_bin']).to eq(Set.new(["bar"])) expect(@object.indexes['unset_bin']).to eq(Set.new) end describe "when storing the object normally" do before :each do @backend = double("Backend") allow(@client).to receive(:backend).and_yield(@backend) @object = Riak::RObject.new(@bucket) @object.content_type = "text/plain" @object.data = "This is some text." # @headers = @object.store_headers end it "should raise an error when the content_type is blank" do expect { @object.content_type = nil; @object.store }.to raise_error(ArgumentError) expect { @object.content_type = " "; @object.store }.to raise_error(ArgumentError) end it "should raise an error when given an empty string as key" do expect { @object.key = ''; @object.store }.to raise_error(ArgumentError) end it "should pass along quorum parameters and returnbody to the backend" do expect(@backend).to receive(:store_object).with(@object, :returnbody => false, :w => 3, :dw => 2).and_return(true) @object.store(:returnbody => false, :w => 3, :dw => 2) end it "should raise an error if the object is in conflict" do @object.siblings << Riak::RContent.new(@object) expect { @object.store }.to raise_error(Riak::Conflict) end end describe "when reloading the object" do before :each do @backend = double("Backend") allow(@client).to receive(:backend).and_yield(@backend) @object = Riak::RObject.new(@bucket, "bar") @object.vclock = "somereallylongstring" end it "should return without requesting if the key is blank" do @object.key = nil expect(@backend).not_to receive(:reload_object) @object.reload end it "should return without requesting if the vclock is blank" do @object.vclock = nil expect(@backend).not_to receive(:reload_object) @object.reload end it "should reload the object if the key is present" do expect(@backend).to receive(:reload_object).with(@object, {}).and_return(@object) @object.reload end it "should pass along the requested R quorum" do expect(@backend).to receive(:reload_object).with(@object, :r => 2).and_return(@object) @object.reload :r => 2 end it "should disable matching conditions if the key is present and the :force option is given" do expect(@backend).to receive(:reload_object) do |obj, _| expect(obj.etag).to be_nil expect(obj.last_modified).to be_nil obj end @object.reload :force => true end end describe "when deleting" do before :each do @backend = double("Backend") allow(@client).to receive(:backend).and_yield(@backend) @object = Riak::RObject.new(@bucket, "bar") end it "should make a DELETE request to the Riak server and freeze the object" do expect(@backend).to receive(:delete_object).with(@bucket, "bar", {}) @object.delete expect(@object).to be_frozen end it "should do nothing when the key is blank" do expect(@backend).not_to receive(:delete_object) @object.key = nil @object.delete end it "should pass through a failed request exception" do expect(@backend).to receive(:delete_object). and_raise(Riak::ProtobuffsFailedRequest.new(:server_error, "server error")) expect { @object.delete }.to raise_error(Riak::FailedRequest) end it "should send the vector clock if present" do @object.vclock = "somevclock" expect(@backend).to receive(:delete_object).with(@bucket, "bar", {:vclock => "somevclock"}) @object.delete end end it "should not convert to link without a tag" do @object = Riak::RObject.new(@bucket, "bar") expect { @object.to_link }.to raise_error end it "should convert to a link having the same url and a supplied tag" do @object = Riak::RObject.new(@bucket, "bar") expect(@object.to_link("next")).to eq(Riak::Link.new("/riak/foo/bar", "next")) end it "should escape the bucket and key when converting to a link" do @object = Riak::RObject.new(@bucket, "deep/path") expect(@bucket).to receive(:name).and_return("bucket spaces") expect(@object.to_link("bar").url).to eq("/riak/bucket%20spaces/deep%2Fpath") end describe "#inspect" do let(:object) { Riak::RObject.new(@bucket) } it "provides useful output even when the key is nil" do expect { object.inspect }.not_to raise_error expect(object.inspect).to be_kind_of(String) end it 'uses the serializer output in inspect' do object.raw_data = { 'a' => 7 } object.content_type = 'inspect/type' Riak::Serializers['inspect/type'] = Object.new.tap do |o| def o.load(object); "serialize for inspect"; end end expect(object.inspect).to match(/serialize for inspect/) end end describe '.on_conflict' do it 'adds the hook to the list of on conflict hooks' do hook_run = false expect(described_class.on_conflict_hooks).to be_empty described_class.on_conflict { hook_run = true } expect(described_class.on_conflict_hooks.size).to eq(1) described_class.on_conflict_hooks.first.call expect(hook_run).to eq(true) end end describe '#attempt_conflict_resolution' do let(:conflicted_robject) { Riak::RObject.new(@bucket, "conflicted") { |r| r.siblings = [ Riak::RContent.new(r), Riak::RContent.new(r)] } } let(:resolved_robject) { Riak::RObject.new(@bucket, "resolved") } let(:invoked_resolvers) { [] } let(:resolver_1) { lambda { |r| invoked_resolvers << :resolver_1; nil } } let(:resolver_2) { lambda { |r| invoked_resolvers << :resolver_2; :not_an_robject } } let(:resolver_3) { lambda { |r| invoked_resolvers << :resolver_3; r } } let(:resolver_4) { lambda { |r| invoked_resolvers << :resolver_4; resolved_robject } } before(:each) do described_class.on_conflict(&resolver_1) described_class.on_conflict(&resolver_2) end it 'calls each resolver until one of them returns an robject' do described_class.on_conflict(&resolver_3) described_class.on_conflict(&resolver_4) conflicted_robject.attempt_conflict_resolution expect(invoked_resolvers).to eq([:resolver_1, :resolver_2, :resolver_3]) end it 'returns the robject returned by the last invoked resolver' do described_class.on_conflict(&resolver_4) expect(conflicted_robject.attempt_conflict_resolution).to be(resolved_robject) end it 'allows the resolver to return the original robject' do described_class.on_conflict(&resolver_3) expect(conflicted_robject.attempt_conflict_resolution).to be(conflicted_robject) end it 'returns the robject and does not call any resolvers if the robject is not in conflict' do expect(resolved_robject.attempt_conflict_resolution).to be(resolved_robject) expect(invoked_resolvers).to eq([]) end it 'returns the original robject if none of the resolvers returns an robject' do expect(conflicted_robject.attempt_conflict_resolution).to be(conflicted_robject) expect(invoked_resolvers).to eq([:resolver_1, :resolver_2]) end end end