require File.dirname(__FILE__) + '/spec_helper' describe "Document class" do before(:each) do @store = setup_default_store setup_index end it "should be able to find document by UUID" do @document = Document.create! Document.find(@document.uuid).should == @document Document.find(@store,@document.uuid).should == @document end it "should be able to find document by query" do @document = Document.create! Document.find(:uuid => @document.uuid).should == [@document] Document.find(@store, :uuid => @document.uuid).should == [@document] end it "should raise ArgumentError when invoking #find with wrong argument" do @document = Document.create! [ [], nil, 1 ].each do |arg| lambda { Document.find(arg) }.should raise_error(ArgumentError) end end end describe "Document", :shared => true do it "should create new slot" do lambda do @document[:new_slot] = "someval" @document[:new_slot].should == "someval" end.should change(@document,:slotnames) end it "should be able to remove slot" do original_slotnames = @document.slotnames lambda do @document[:new_slot] = "someval" @document[:new_slot].should == "someval" end.should change(@document,:slotnames) lambda do @document.remove_slot!(:new_slot) end.should change(@document,:slotnames) (@document.slotnames - ['previous_version']).should == original_slotnames # TODO: check this exclusion end it "should call when_slot_not_found callback on missing slot" do @document.callbacks['when_slot_not_found'] = [mock("callback")] @document.should_receive(:execute_callbacks).with(:when_slot_not_found,'slot_that_surely_does_not_exist').and_return("Yes!") @document.slot_that_surely_does_not_exist.should == "Yes!" end it "should raise an exception if slot not found when trying to read it" do lambda { @document.slot_that_never_can_exist }.should raise_error(SlotNotFoundError) end it "should allow to write slot by writer method" do @document.slot1 = 2 @document[:slot1].should == 2 end it "should allow to read slot by reader method" do @document[:slot1] = 1 @document.slot1.should == 1 end it "should allow to read slot by reader? method" do @document[:slot1] = 1 @document[:slot2] = 0 @document[:slot3] = nil @document[:slot4] = false @document.slot1?.should be_true @document.slot2?.should be_true @document.slot3?.should be_false @document.slot4?.should be_false end # update_slots it "should batch update slots" do @document.update_slots(:aaa => "aaa", :bbb => true) @document.aaa.should == "aaa" @document.bbb.should == true end it "should not save batch update slots" do @document.save! # ensure it is not new doc = @document.reload @document.update_slots(:aaa1 => "aaa", :bbb1 => true) doc = @document.reload doc[:aaa1].should be_nil doc[:bbb1].should be_nil end it "should support batch update slots with saving" do doc = @document.update_slots!(:aaa => "aaa", :bbb => true) doc.aaa.should == "aaa" doc.bbb.should == true doc = doc.reload doc.aaa.should == "aaa" doc.bbb.should == true end # reverse_update_slots it "should batch update slots in reverse (||=)" do @document.aaa = "before" @document.reverse_update_slots(:aaa => "after", :bbb => false) @document.aaa.should == "before" @document.bbb.should == false end it "should support batch reverse_update_slots with saving" do @document.aaa = "before" doc = @document.reverse_update_slots!(:aaa => "after", :bbb => false) doc.aaa.should == "before" doc.bbb.should == false doc = doc.reload doc.aaa.should == "before" doc.bbb.should == false end # callbacks it "should add callbacks" do cb1 = Callback.new(nil,:callback_name) {} cb2 = Callback.new(nil,:another_callback_name) {} @document.add_callback(cb1) @document.add_callback(cb2) @document.callbacks[:callback_name].should include(cb1) @document.callbacks[:another_callback_name].should include(cb2) end it "should replace uniquely identified callbacks" do cb1 = Callback.new(nil,:callback_name, :special) {} cb2 = Callback.new(nil,:callback_name, :special) {} @document.add_callback(cb1) @document.add_callback(cb2) @document.callbacks[:callback_name].should have(1).item @document.callbacks[:callback_name].should include(cb2) end it "should report existing slot as existing" do @document[:existing_slot] = 1 @document.should have_slot(:existing_slot) end it "should report existing slot with nil value as existing" do @document[:existing_slot] = nil @document.should have_slot(:existing_slot) end it "should report non-existing slot as non-existing" do @document.should_not have_slot(:existing_slot) end it "should report existing 'virtual' slot as existing" do @document.should_receive(:method_missing).with(:existing_slot).and_return 1 @document.should have_slot(:existing_slot) end it "should report non-existing 'virtual' slot as non-existing" do @document.should_receive(:method_missing).with(:existing_slot).and_return { raise SlotNotFoundError.new(:existing_slot)} @document.should_not have_slot(:existing_slot) end it "should convert Symbol values to String instantly (including Symbol usage in structures)" do @document.symbol_slot = :a @document.symbol_slot.should == "a" @document.symbol_slot = [[:a]] @document.symbol_slot.should == [["a"]] @document.symbol_slot = {:a => :b} @document.symbol_slot.should == {"a" => "b"} @document.symbol_slot = [{:a => :b}] @document.symbol_slot.should == [{"a" => "b"}] end it "should convert Symbol values to String (including Symbol usage in structures)" do @document.symbol_slot = :a @document = @document.save!.reload @document.symbol_slot.should == "a" @document.symbol_slot = [[:a]] @document = @document.save!.reload @document.symbol_slot.should == [["a"]] @document.symbol_slot = {:a => :b} @document = @document.save!.reload @document.symbol_slot.should == {"a" => "b"} @document.symbol_slot = [{:a => :b}] @document = @document.save!.reload @document.symbol_slot.should == [{"a" => "b"}] end it "should convert Meta values to Documents instantly" do @document.meta_slot = Meta @document.meta_slot.should == Meta.document(@document.store) @document.metas_slot = [Meta] @document.metas_slot.should == [Meta.document(@document.store)] end it "should convert Meta values to Documents" do @document.meta_slot = Meta @document.metas_slot = [Meta] @document = @document.save!.reload @document.meta_slot.should == Meta.document(@document.store) @document.metas_slot.should == [Meta.document(@document.store)] end it "should not save itself once declared immutable" do @document.make_immutable! @document.store.should_not_receive(:save!) @document.save! end it "should be able to return current version" do @document.should_not be_a_kind_of(VersionedDocument) @document.versions.current.should == @document @document.versions.current.should be_a_kind_of(VersionedDocument) end it "should have #hash calculated from uuid" do hash = @document.hash another_doc = Document.from_raw(@document.store,@document.to_raw) another_doc_with_different_uuid = Document.from_raw(@document.store,@document.to_raw.merge('uuid' => Util.random_uuid)) another_doc.hash.should == hash another_doc_with_different_uuid.hash.should_not == hash end end describe "New Document" do before(:each) do setup_default_store @document = Document.new end it "should have UUID" do @document.uuid.should match(UUID_RE) end it "should be new" do @document.should be_new end it "should not be head" do @document.should_not be_head end it "should have version" do @document.version.should_not be_nil end it "should have NIL UUID version" do @document.version.should == NIL_UUID end it "should have no previous version" do @document.previous_version.should be_nil @document.versions.previous.should be_nil end it "should have only version slotname" do @document.slotnames.to_set.should == ['version','uuid'].to_set end it "should have no versions" do @document.versions.should be_empty end it "should be reloadable to itself" do reloaded_doc = @document.reload reloaded_doc.object_id.should == @document.object_id reloaded_doc.should be_new end it "should be both first and head version" do @document.versions.first.should == @document @document.versions.head.should == @document end it "should return string with Document's JSON representation" do @document.to_json.should == "{\"uuid\":\"#{@document.uuid}\",\"version\":\"#{@document.version}\"}" end it "should return string with Document's XML representation" do pending('bug') do @document.to_xml.should == "FIXME" end end it_should_behave_like "Document" end describe "New Document with slots supplied" do before(:each) do setup_default_store @document = Document.new(:slot1 => "val1", :slot2 => "val2") end it "should have corresponding slotnames" do @document.slotnames.to_set.should == ['slot1','slot2','version','uuid'].to_set end it "should update slot value" do @document[:slot1] = "someval" @document[:slot1].should == "someval" end it "should be saveable" do @document.save! @document.should_not be_new end it_should_behave_like "Document" end describe "Forked documents" do before(:each) do setup_default_store end it "should have the same previous_version " do @doc1 = Document.create!(:a => 11) @doc1.save! @first_version = @doc1.version.dup @doc1.a = 12 @doc1.save! # clone @doc2 = @doc1.versions[@first_version] @doc2.a = 21 @doc2.save! @doc1.previous_version.should == @first_version @doc2.previous_version.should == @first_version end end describe "Saved Document" do before(:each) do @store = setup_default_store @document = Document.create!(:some_data => 1) end it "should have version" do @document.version.should match(/#{VERSION_RE}/) end it "should not be new" do @document.should_not be_new end it "should be head" do @document.should be_head end it "should be reloadable" do reloaded_doc = @document.reload reloaded_doc.should == @document reloaded_doc.object_id.should_not == @document.object_id end it "should not change version and previous_version once not modified and saved" do old_version = @document.version old_previos_version = @document.previous_version @document.save! @document.version.should == old_version @document.previous_version.should == old_previos_version end it "should change version once modified; previous version should be set to original version" do old_version = @document.version @document[:a] = 1 @document.version.should_not == old_version @document.previous_version.should == old_version end it "should not change version once Array slot is accessed" do @document[:a] = [1] @document.save! old_version = @document.version @document[:a].index(0) @document.version.should == old_version end it "should change version once Array slot is modified; previous version should be set to original version" do @document[:a] = [] @document.save! old_version = @document.version @document = @document.reload @document[:a] << 1 @document.version.should_not == old_version @document.previous_version.should == old_version @document = @document.reload @document[:a].unshift 1 @document.version.should_not == old_version @document.previous_version.should == old_version @document = @document.reload @document[:a][0] = 1 @document.version.should_not == old_version @document.previous_version.should == old_version end it "should not change version once Hash slot is accessed" do @document[:a] = {} @document.save! old_version = @document.version val = @document[:a][:b] @document.version.should == old_version end it "should change version once Hash slot is modified; previous version should be set to original version" do @document[:a] = {} @document.save! old_version = @document.version @document[:a][:b] = 1 @document.version.should_not == old_version @document.previous_version.should == old_version end it "should change version once some slot is removed; previous version should be set to original version" do old_version = @document.version @document.remove_slot!(:some_data) @document.version.should_not == old_version @document.previous_version.should == old_version end it "should be deleteable" do old_version = @document.version @document.delete! @document.should be_a_kind_of(DeletedDocument) @document.should_not be_mutable @document.version.should_not == old_version @document.previous_version.should == old_version end it_should_behave_like "Document" end describe "Deleted document" do before(:each) do @store = setup_default_store @document = Document.create!(:some_data => 1) @old_version = @document.version @document.delete! end it "once reloaded shouldn't be mutable" do @document = @document.reload @document.should_not be_mutable end it "should be undeletable" do @document = @document.undelete! @document.should_not be_a_kind_of(DeletedDocument) @document.should be_mutable @store.find(@document.uuid).should == @document end it "should have old version after undeletion" do @document = @document.undelete! @document.version.should == @old_version end end describe "Head Document with references" do before(:each) do setup_default_store @doc1 = Document.create!(:one => 1) @doc2 = Document.create!(:two => 2) @doc3 = Document.new(:three => 3) @document = Document.create!(:some_link => @doc1, :some_indirect_link => [@doc2], :some_other_link => @doc3) @document.test = :yes @document.save! @doc3.save! end it "should not link to specific versions" do @document.should be_head @document.some_link.should_not be_a_kind_of(VersionedDocument) @document.some_other_link.should_not be_a_kind_of(VersionedDocument) @document.some_indirect_link.first.should_not be_a_kind_of(VersionedDocument) end it "should not link to specific versions when reloaded" do @document = @document.reload @document.should be_head @document.some_link.should_not be_a_kind_of(VersionedDocument) @document.some_other_link.should_not be_a_kind_of(VersionedDocument) @document.some_indirect_link.first.should_not be_a_kind_of(VersionedDocument) end end describe "Saved VersionedDocument" do before(:each) do setup_default_store @document = Document.create!(:some_data => 1) @versioned_document = @document.versions[@document.version] end it "should not be head" do @versioned_document.should_not be_head end it "should be reloadable" do StrokeDB.default_store.should_receive(:find).with(@document.uuid,@document.version) @versioned_document.reload end end describe "VersionedDocument with references" do before(:each) do setup_default_store @doc1 = Document.create!(:one => 1) @doc2 = Document.create!(:two => 2) @doc3 = Document.new(:three => 3) @document = Document.create!(:some_link => @doc1, :some_indirect_link => [@doc2], :some_other_link => @doc3) @doc3.save! @versioned_document = @document.versions[@document.version] @versioned_document.should be_a_kind_of(VersionedDocument) @versioned_document.should_not be_head end it "should link to specific versions" do @versioned_document.some_link.should be_a_kind_of(VersionedDocument) @versioned_document.some_other_link.should be_a_kind_of(VersionedDocument) @versioned_document.some_indirect_link.first.should be_a_kind_of(VersionedDocument) end end describe "Document with previous version" do before(:each) do @store = setup_default_store @document = Document.create! @first_version = Document.find(@document.uuid) @document.new_slot = 1 @document.save! @second_version = Document.find(@document.uuid) end it "should have versions" do @document.version.should_not be_empty end it "should be able to access previous version" do prev_version = @store.find(@document.uuid,@document.previous_version) @document.versions[@document.previous_version].should == prev_version @document.versions.previous.should == prev_version end it "should be able to access first version" do @document.versions.first.should == @document.versions.previous end it "should return all previous versions of document" do @document.new_slot2 = 'foo' @document.save! @document.versions.all_preceding.length.should == 2 @document.versions.all_preceding.class.should == Array @document.versions.all_preceding.should include(@first_version) @document.versions.all_preceding.should include(@second_version) @document.versions.all_preceding.should_not include(@document) end end describe "Non-head version of document" do before(:each) do @store = setup_default_store @document = Document.create! @document.new_slot = 1 @document.save! @non_head_document = @document.versions.previous end it "should be able to access head version" do @non_head_document.versions.head.should == @document end it "should not be deletable" do lambda { @non_head_document.delete! }.should raise_error(DocumentDeletionError) end end describe "Document with a single meta" do before(:each) do @store = setup_default_store setup_default_store setup_index Object.send!(:remove_const, "SomeMeta") if defined? ::SomeMeta ::SomeMeta = Meta.new(@store) @meta = ::SomeMeta @document = Document.create!(@store, :meta => @meta) end it "but specified within array should return single meta which should be mutable" do @document = Document.create!(@store, :meta => [@meta]) @document.meta.should == @meta.document @document.meta.should be_mutable end it "should return single meta which should be mutable" do @document.meta.should == @meta.document @document.meta.should be_mutable end end describe "Document with multiple metas" do before(:each) do @store = setup_default_store setup_index @metas = [] 3.times do |i| @metas << Document.create!(:a => i, i => i, :name => i.to_s) end Object.send!(:remove_const,'SomeMeta') if defined?(SomeMeta) SomeMeta = Meta.new do on_initialization do |doc| doc.hello = 'world' end end @document = Document.new(:meta => @metas) end it "should return single merged meta" do meta = @document.meta meta.should be_a_kind_of(Document) meta[:a].should == 2 meta[0].should == 0 meta[1].should == 1 meta[2].should == 2 meta.name.should == "0,1,2" @document[:meta].should be_a_kind_of(Array) end it "should make single merged meta immutable" do meta = @document.meta meta.should_not be_mutable end it "should be able to return metas collection" do @document.metas.should be_a_kind_of(Array) end it "should be able to add meta by pushing its document to metas" do @document.metas << SomeMeta.document @document.hello.should == 'world' @document.metas.should include(SomeMeta.document) @document.should be_a_kind_of(SomeMeta) end it "should be able to add meta by pushing its module to metas" do @document.metas << SomeMeta @document.hello.should == 'world' @document.metas.should include(SomeMeta.document) @document.should be_a_kind_of(SomeMeta) end it "should be able to remove meta by removing its document from metas" do @document.metas << SomeMeta.document @document.metas.delete SomeMeta.document @document.metas.should_not include(SomeMeta.document) @document.should_not be_a_kind_of(SomeMeta) end it "should be able to remove meta by removing its module from metas" do @document.metas << SomeMeta @document.metas.delete SomeMeta @document.metas.should_not include(SomeMeta.document) @document.should_not be_a_kind_of(SomeMeta) end it "should raise ArgumentError when pushing neither document nor module" do lambda { @document.metas << 1 }.should raise_error(ArgumentError) end end describe "Document initialization with store omitted", :shared => true do it "should raise an exception if no default store available" do StrokeDB.stub!(:default_store).and_return(nil) lambda { Document.new(*@args) }.should raise_error(NoDefaultStoreError) end it "should use default store if available" do StrokeDB.stub!(:default_store).and_return(mock("Store")) doc = Document.new(*@args) doc.store.should == StrokeDB.default_store end end describe "Document initialization with store omitted but with some slots specified" do before(:each) do @args = [{:slot1 => 1}] end it_should_behave_like "Document initialization with store omitted" end describe "Document initialization with store omitted but with no slots specified" do before(:each) do @args = [] end it_should_behave_like "Document initialization with store omitted" end describe "Document with version" do before(:each) do setup_default_store @document = Document.new(:some_data => 1) end it "should be equal to another document with the same version and uuid" do @another_document = Document.new(:some_data => 1, :uuid => @document.uuid) @another_document.version = @document.version @document.should == @another_document @document.should be_eql(@another_document) end it "should not be equal to another document with the same version but another uuid" do @another_document = Document.new(:some_data => 1) @another_document.version = @document.version @document.should_not == @another_document end end describe "Valid Document's JSON" do before(:each) do @store = mock("Store") @document = Document.new(@store,:slot1 => "val1", :slot2 => "val2") @json = @document.to_raw.to_json @decoded_json = JSON.parse(@json) end it "should be loadable into Document" do doc = Document.from_raw(@store,@decoded_json) doc.uuid.should == @document.uuid doc.slotnames.to_set.should == ['slot1','slot2','version','uuid'].to_set end it "should reuse cached previous version at first modification" do doc = Document.from_raw(@store,@decoded_json) doc[:hello] = 'world' doc[:hello] = 'world!' doc[:previous_version].should == @document.version end end describe "Valid Document's JSON with meta name specified" do before(:each) do @store = setup_default_store @meta = Document.create!(:name => 'SomeDocument') @document = Document.new(@store,:slot1 => "val1", :slot2 => "val2", :meta => @meta) @json = @document.to_raw.to_json @decoded_json = JSON.parse(@json) end it "should load meta's module if it is available" do Object.send!(:remove_const,'SomeDocument') if defined?(SomeDocument) SomeDocument = Module.new doc = Document.from_raw(@store,@decoded_json) doc.should be_a_kind_of(SomeDocument) end it "should not load meta's module if it is not available" do Object.send!(:remove_const,'SomeDocument') if defined?(SomeDocument) lambda do doc = Document.from_raw(@store,@decoded_json) end.should_not raise_error end end describe "Valid Document's JSON with multiple meta names specified" do before(:each) do @store = setup_default_store @metas = [] 3.times do |i| @metas << Document.create!(@store, :name => "SomeDocument#{i}") end @document = Document.new(@store,:slot1 => "val1", :slot2 => "val2", :meta => @metas) @json = @document.to_raw.to_json @decoded_json = JSON.parse(@json) end it "should load all available meta modules" do Object.send!(:remove_const,'SomeDocument0') if defined?(SomeDocument0) SomeDocument0 = Meta.new Object.send!(:remove_const,'SomeDocument2') if defined?(SomeDocument2) SomeDocument2 = Meta.new doc = Document.from_raw(@store,@decoded_json) doc.should be_a_kind_of(SomeDocument0) doc.should be_a_kind_of(SomeDocument2) end it "should call all on_initialization callbacks for all available meta modules" do Object.send!(:remove_const,'SomeDocument0') if defined?(SomeDocument0) SomeDocument0 = Meta.new do on_initialization do |doc| Kernel.send(:callback_0_called) end end Object.send!(:remove_const,'SomeDocument2') if defined?(SomeDocument2) SomeDocument2 = Meta.new do on_initialization do |doc| Kernel.send(:callback_2_called) end end Kernel.should_receive(:callback_0_called) Kernel.should_receive(:callback_2_called) doc = Document.from_raw(@store,@decoded_json) end end describe "Composite document ( result of Document#+(document) )" do before(:each) do setup_default_store @document1 = Document.new :slot1 => 1, :x => 1 @document2 = Document.new :slot1 => 2, :slot2 => 2 @composite = @document1+@document2 end it "should be a Document" do @composite.should be_a_kind_of(Document) end it "should have new UUID" do @composite.uuid.should match(UUID_RE) @composite.uuid.should_not == @document1.uuid @composite.uuid.should_not == @document2.uuid end it "should have new version" do @composite.version.should_not == @document1.version @composite.version.should_not == @document2.version end it "should update identical slots" do @composite.slot1.should == 2 end it "should add different slots" do @composite.slot2.should == 2 end it "should not remove missing slots" do @composite.x.should == 1 end end describe "Saved document with validations" do before(:each) do Object.send!(:remove_const,'Foo') if defined?(Foo) setup_default_store end it "should be deletable with validates_presence_of" do Foo = Meta.new { validates_presence_of :name } doc = Foo.create! :name => 'foo' doc.delete! end it "should be deletable with validates_type_of" do Foo = Meta.new { validates_type_of :name, :as => 'String' } doc = Foo.create! :name => 'foo' doc.delete! end it "should be deletable with validates_uniqueness_of" do Foo = Meta.new { validates_uniqueness_of :name } doc = Foo.create! :name => 'foo' doc.delete! end it "should be deletable with validates_inclusion_of" do Foo = Meta.new { validates_inclusion_of :name, :in => ['foo','bar'] } doc = Foo.create! :name => 'foo' doc.delete! end it "should be deletable with validates_exclusion_of" do Foo = Meta.new { validates_exclusion_of :name, :in => ['foo','bar'] } doc = Foo.create! :name => 'ueep' doc.delete! end it "should be deletable with validates_numericality_of" do Foo = Meta.new { validates_numericality_of :number } doc = Foo.create! :number => '1' doc.delete! end it "should be deletable with validates_format_of" do Foo = Meta.new { validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i } doc = Foo.create! :email => 'foo@bar.org' doc.delete! end it "should be deletable with validates_confirmation_of" do Foo = Meta.new { validates_confirmation_of :password } doc = Foo.create! :password => "sekret", :password_confirmation => "sekret" doc.delete! end it "should be deletable with validates_acceptance_of" do Foo = Meta.new { validates_acceptance_of :eula, :accept => "yep" } doc = Foo.create! :eula => "yep" doc.delete! end it "should be deletable with validates_length_of" do Foo = Meta.new { validates_length_of :name, :within => 10..50 } doc = Foo.create! :name => "supercalifragilistico" doc.delete! end it "should be deletable with validates_associated" do Foo = Meta.new { has_many :bars; validates_associated :bars } Bar = Meta.new doc = Foo.create! doc.delete! end end