require 'spec_helper.rb' describe "ManyDocumentsProxy" do before do Project.collection.drop Status.collection.drop @pet_class = Doc do key :name, String key :owner_id, ObjectId end @owner_class = Doc do key :name, String end @owner_class.many :pets, :class => @pet_class, :foreign_key => :owner_id, :order => 'name' end it "should return results if found via method_missing" do @pet_class.class_eval do def self.from_param(name) find_by_name(name) end def self.all_from_param(names) where(:name => names).all end end instance = @owner_class.new instance.pets.build(:name => "Foo") instance.pets.build(:name => "Bar") instance.save instance.reload.pets.from_param("Foo").tap do |pet| pet.should be_a @pet_class pet.name.should == "Foo" end instance.reload.pets.all_from_param(["Foo", "Bar"]).tap do |pet| pet.should be_a Array pet.map(&:name).should =~ %w(Foo Bar) end end it "should default reader to empty array" do project = Project.new project.statuses.should == [] end it "should allow overriding association methods" do @owner_class.class_eval do def pets super end end instance = @owner_class.new instance.pets.should == [] instance.pets.build instance.pets.should_not be_empty end it "should be able to iterate associated documents in a callback" do @owner_class.class_eval do before_save :search_pets def search_pets pets.each { |p| p.name = "Animal" } end end owner = @owner_class.new sophie = owner.pets.build(:name => "Sophie") pippa = owner.pets.build(:name => "Pippa") owner.save owner.reload owner.pets.reload pets = [] owner.pets.each { |p| pets << p } pets.size.should == 2 pets.should include(sophie) pets.should include(pippa) sophie.reload.name.should == "Animal" pippa.reload.name.should == "Animal" end it "should allow assignment of many associated documents using a hash" do person_attributes = { 'name' => 'Mr. Pet Lover', 'pets' => [ {'name' => 'Jimmy', 'species' => 'Cocker Spainel'}, {'name' => 'Sasha', 'species' => 'Siberian Husky'}, ] } owner = @owner_class.new(person_attributes) owner.name.should == 'Mr. Pet Lover' owner.pets[0].name.should == 'Jimmy' owner.pets[0].species.should == 'Cocker Spainel' owner.pets[1].name.should == 'Sasha' owner.pets[1].species.should == 'Siberian Husky' owner.save.should be_truthy owner.reload owner.name.should == 'Mr. Pet Lover' owner.pets[0].name.should == 'Jimmy' owner.pets[0].species.should == 'Cocker Spainel' owner.pets[1].name.should == 'Sasha' owner.pets[1].species.should == 'Siberian Husky' end it "should allow adding to association like it was an array" do project = Project.new project.statuses << Status.new(:name => 'Foo1!') project.statuses.push Status.new(:name => 'Foo2!') project.statuses.concat Status.new(:name => 'Foo3!') project.statuses.size.should == 3 end context "replacing the association" do context "with objects of the class" do it "should work" do project = Project.new project.statuses = [Status.new(:name => "ready")] project.save.should be_truthy project.reload project.statuses.size.should == 1 project.statuses[0].name.should == "ready" end end context "with Hashes" do it "should convert to objects of the class and work" do project = Project.new project.statuses = [{ 'name' => 'ready' }] project.save.should be_truthy project.reload project.statuses.size.should == 1 project.statuses[0].name.should == "ready" end end context "with :dependent" do before do @broker_class = Doc('Broker') @property_class = Doc('Property') do key :broker_id, ObjectId belongs_to :broker end end context "=> destroy" do before do @broker_class.many :properties, :class => @property_class, :dependent => :destroy @broker = @broker_class.create(:name => "Bob") @property1 = @property_class.create @property2 = @property_class.create @property3 = @property_class.create @broker.properties << @property1 @broker.properties << @property2 @broker.properties << @property3 end it "should call destroy the existing documents" do @broker.properties[0].should_receive(:destroy).once @broker.properties[1].should_receive(:destroy).once @broker.properties[2].should_receive(:destroy).once @broker.properties = [@property_class.new] end it "should remove the existing document from the database" do @property_class.count.should == 3 @broker.properties = [] @property_class.count.should == 0 end it "should skip over documents that are the same" do @broker.properties[0].should_receive(:destroy).never @broker.properties[1].should_receive(:destroy).once @broker.properties[2].should_receive(:destroy).never @broker.properties = [@property3, @property1] end end context "=> delete_all" do before do @broker_class.many :properties, :class => @property_class, :dependent => :delete_all @broker = @broker_class.create(:name => "Bob") @property1 = @property_class.create @property2 = @property_class.create @property3 = @property_class.create @broker.properties << @property1 @broker.properties << @property2 @broker.properties << @property3 end it "should call delete the existing documents" do @broker.properties[0].should_receive(:delete).once @broker.properties[1].should_receive(:delete).once @broker.properties[2].should_receive(:delete).once @broker.properties = [@property_class.new] end it "should remove the existing document from the database" do @property_class.count.should == 3 @broker.properties = [] @property_class.count.should == 0 end it "should skip over documents that are the same" do @broker.properties[0].should_receive(:delete).never @broker.properties[1].should_receive(:delete).once @broker.properties[2].should_receive(:delete).never @broker.properties = [@property3, @property1] end end context "=> nullify" do before do @broker_class.many :properties, :class => @property_class, :dependent => :nullify @broker = @broker_class.create(:name => "Bob") @property1 = @property_class.create @property2 = @property_class.create @property3 = @property_class.create @broker.properties << @property1 @broker.properties << @property2 @broker.properties << @property3 end it "should nullify the existing documents" do @property1.reload.broker_id.should == @broker.id @property2.reload.broker_id.should == @broker.id @property3.reload.broker_id.should == @broker.id @broker.properties = [@property_class.new] @property1.reload.broker_id.should be_nil @property2.reload.broker_id.should be_nil @property3.reload.broker_id.should be_nil end it "should skip over documents that are the same" do @broker.properties = [@property3, @property1] @property1.reload.broker_id.should == @broker.id @property2.reload.broker_id.should be_nil @property3.reload.broker_id.should == @broker.id end it "should work" do old_properties = @broker.properties @broker.properties = [@property1, @property2, @property3] old_properties.should == @broker.properties end end context "unspecified" do it "should nullify the existing documents" do @broker_class.many :properties, :class => @property_class @broker = @broker_class.create(:name => "Bob") @property1 = @property_class.create @property2 = @property_class.create @property3 = @property_class.create @broker.properties << @property1 @broker.properties << @property2 @broker.properties << @property3 @broker.properties = [@property_class.new] @property1.reload.broker_id.should be_nil @property2.reload.broker_id.should be_nil @property3.reload.broker_id.should be_nil end end end end context "using <<, push and concat" do context "with objects of the class" do it "should correctly assign foreign key" do project = Project.new project.statuses << Status.new(:name => '<<') project.statuses.push Status.new(:name => 'push') project.statuses.concat Status.new(:name => 'concat') project.reload project.statuses[0].project_id.should == project.id project.statuses[1].project_id.should == project.id project.statuses[2].project_id.should == project.id end end context "with Hashes" do it "should correctly convert to objects and assign foreign key" do project = Project.new project.statuses << { 'name' => '<<' } project.statuses.push( { 'name' => 'push' }) project.statuses.concat({ 'name' => 'concat' }) project.reload project.statuses[0].project_id.should == project.id project.statuses[1].project_id.should == project.id project.statuses[2].project_id.should == project.id end end end context "#build" do it "should assign foreign key" do project = Project.create status = project.statuses.build status.project_id.should == project.id end it "should allow assigning attributes" do project = Project.create status = project.statuses.build(:name => 'Foo') status.name.should == 'Foo' end it "should reset cache" do project = Project.create project.statuses.size.should == 0 status = project.statuses.build(:name => 'Foo') status.save! project.statuses.size.should == 1 end it "should update collection without save" do project = Project.create project.statuses.build(:name => 'Foo') project.statuses.size.should == 1 end it "should save built document when saving parent" do project = Project.create status = project.statuses.build(:name => 'Foo') project.save! status.should_not be_new end it "should not save the parent when building associations" do project = Project.new status = project.statuses.build(:name => 'Foo') project.should be_new end it "should not save the built object" do project = Project.new status = project.statuses.build(:name => 'Foo') status.should be_new end it "should accept a block" do project = Project.new status = project.statuses.build do |doc| doc.name = "Foo" end project.statuses[0].name.should == "Foo" end end context "#create" do it "should assign foreign key" do project = Project.create status = project.statuses.create(:name => 'Foo!') status.project_id.should == project.id end it "should save record" do project = Project.create lambda { project.statuses.create(:name => 'Foo!') }.should change { Status.count } end it "should allow passing attributes" do project = Project.create status = project.statuses.create(:name => 'Foo!') status.name.should == 'Foo!' end it "should reset cache" do project = Project.create project.statuses.size.should == 0 project.statuses.create(:name => 'Foo!') project.statuses.size.should == 1 end it "should accept a block" do project = Project.new status = project.statuses.create do |doc| doc.name = "Foo" end project.statuses.first.name.should == "Foo" end end context "#create!" do it "should assign foreign key" do project = Project.create status = project.statuses.create!(:name => 'Foo!') status.project_id.should == project.id end it "should save record" do project = Project.create lambda { project.statuses.create!(:name => 'Foo!') }.should change { Status.count } end it "should allow passing attributes" do project = Project.create status = project.statuses.create!(:name => 'Foo!') status.name.should == 'Foo!' end it "should raise exception if not valid" do project = Project.create lambda { project.statuses.create!(:name => nil) }.should raise_error(MongoMapper::DocumentNotValid) end it "should reset cache" do project = Project.create project.statuses.size.should == 0 project.statuses.create!(:name => 'Foo!') project.statuses.size.should == 1 end it "should accept a block" do project = Project.new status = project.statuses.create! do |doc| doc.name = "Foo" end status.name.should == "Foo" end end context "count" do it "should work scoped to association" do project = Project.create 3.times { project.statuses.create(:name => 'Foo!') } other_project = Project.create 2.times { other_project.statuses.create(:name => 'Foo!') } project.statuses.count.should == 3 other_project.statuses.count.should == 2 end it "should work with conditions" do project = Project.create project.statuses.create(:name => 'Foo') project.statuses.create(:name => 'Other 1') project.statuses.create(:name => 'Other 2') project.statuses.count(:name => 'Foo').should == 1 end it "should ignore unpersisted documents" do project = Project.create project.statuses.build(:name => 'Foo') project.statuses.count.should == 0 end end context "size" do it "should reflect both persisted and new documents" do project = Project.create 3.times { project.statuses.create(:name => 'Foo!') } 2.times { project.statuses.build(:name => 'Foo!') } project.statuses.size.should == 5 end end context "empty?" do it "should be true with no associated docs" do project = Project.create project.statuses.empty?.should be_truthy end it "should be false if a document is built" do project = Project.create project.statuses.build(:name => 'Foo!') project.statuses.empty?.should be_falsey end it "should be false if a document is created" do project = Project.create project.statuses.create(:name => 'Foo!') project.statuses.empty?.should be_falsey end end context "#to_a" do it "should include persisted and new documents" do project = Project.create 3.times { project.statuses.create(:name => 'Foo!') } 2.times { project.statuses.build(:name => 'Foo!') } project.statuses.to_a.size.should == 5 end end context "#map" do it "should include persisted and new documents" do project = Project.create 3.times { project.statuses.create(:name => 'Foo!') } 2.times { project.statuses.build(:name => 'Foo!') } project.statuses.map(&:name).size.should == 5 end end context "to_json" do it "should work on association" do project = Project.create 3.times { |i| project.statuses.create(:name => i.to_s) } JSON.parse(project.statuses.to_json).collect{|status| status["name"] }.sort.should == ["0","1","2"] end end context "as_json" do it "should work on association" do project = Project.create 3.times { |i| project.statuses.create(:name => i.to_s) } project.statuses.as_json.collect{|status| status["name"] }.sort.should == ["0","1","2"] end end context "Unassociating documents" do before do @project = Project.create @project.statuses << Status.create(:name => '1') @project.statuses << Status.create(:name => '2') @project2 = Project.create @project2.statuses << Status.create(:name => '1') @project2.statuses << Status.create(:name => '2') end it "should work with destroy all" do @project.statuses.count.should == 2 @project.statuses.destroy_all @project.statuses.count.should == 0 @project2.statuses.count.should == 2 Status.count.should == 2 end it "should work with destroy all and conditions" do @project.statuses.count.should == 2 @project.statuses.destroy_all(:name => '1') @project.statuses.count.should == 1 @project2.statuses.count.should == 2 Status.count.should == 3 end it "should work with delete all" do @project.statuses.count.should == 2 @project.statuses.delete_all @project.statuses.count.should == 0 @project2.statuses.count.should == 2 Status.count.should == 2 end it "should work with delete all and conditions" do @project.statuses.count.should == 2 @project.statuses.delete_all(:name => '1') @project.statuses.count.should == 1 @project2.statuses.count.should == 2 Status.count.should == 3 end it "should work with nullify" do @project.statuses.count.should == 2 @project.statuses.nullify @project.statuses.count.should == 0 @project2.statuses.count.should == 2 Status.count.should == 4 Status.count(:name => '1').should == 2 Status.count(:name => '2').should == 2 end end context "Finding scoped to association" do before do @project1 = Project.new(:name => 'Project 1') @brand_new = Status.create(:name => 'New', :position => 1 ) @complete = Status.create(:name => 'Complete', :position => 2) @project1.statuses = [@brand_new, @complete] @project1.save @project2 = Project.create(:name => 'Project 2') @in_progress = Status.create(:name => 'In Progress') @archived = Status.create(:name => 'Archived') @another_complete = Status.create(:name => 'Complete') @project2.statuses = [@in_progress, @archived, @another_complete] @project2.save end context "include?" do it "should return true if in association" do @project1.statuses.should include(@brand_new) end it "should return false if not in association" do @project1.statuses.should_not include(@in_progress) end end context "dynamic finders" do it "should work with single key" do @project1.statuses.find_by_name('New').should == @brand_new @project1.statuses.find_by_name!('New').should == @brand_new @project2.statuses.find_by_name('In Progress').should == @in_progress @project2.statuses.find_by_name!('In Progress').should == @in_progress end it "should work with multiple keys" do @project1.statuses.find_by_name_and_position('New', 1).should == @brand_new @project1.statuses.find_by_name_and_position!('New', 1).should == @brand_new @project1.statuses.find_by_name_and_position('New', 2).should be_nil end it "should raise error when using !" do lambda { @project1.statuses.find_by_name!('Fake') }.should raise_error(MongoMapper::DocumentNotFound) end context "find_or_create_by" do it "should not create document if found" do lambda { status = @project1.statuses.find_or_create_by_name('New') status.project.should == @project1 status.should == @brand_new }.should_not change { Status.count } end it "should create document if not found" do lambda { status = @project1.statuses.find_or_create_by_name('Delivered') status.project.should == @project1 }.should change { Status.count } end end end context "sexy querying" do it "should work with where" do @project1.statuses.where(:name => 'New').all.should == [@brand_new] end it "should work with sort" do @project1.statuses.sort(:name).all.should == [@complete, @brand_new] end it "should work with limit" do @project1.statuses.sort(:name).limit(1).all.should == [@complete] end it "should work with skip" do @project1.statuses.sort(:name).skip(1).all.should == [@brand_new] end it "should work with fields" do @project1.statuses.fields(:position).all.each do |status| status.position.should_not be_nil status.name.should be_nil end end it "should work with scopes" do @project1.statuses.complete.all.should == [@complete] end it "should work with methods on class that return query" do @project1.statuses.by_position(1).first.should == @brand_new end it "should not work with methods on class that do not return query" do Status.class_eval { def self.foo; 'foo' end } lambda { @project1.statuses.foo }. should raise_error(NoMethodError) end end context "all" do it "should work" do @project1.statuses.all(:order => "position asc").should == [@brand_new, @complete] end it "should work with conditions" do @project1.statuses.all(:name => 'Complete').should == [@complete] end end context "first" do it "should work" do @project1.statuses.first(:order => 'name').should == @complete end it "should work with conditions" do @project1.statuses.first(:name => 'Complete').should == @complete end end context "last" do it "should work" do @project1.statuses.last(:order => "position asc").should == @complete end it "should work with conditions" do @project1.statuses.last(:order => 'position', :name => 'New').should == @brand_new end end context "with one id" do it "should work for id in association" do @project1.statuses.find(@complete.id).should == @complete end it "should not work for id not in association" do lambda { @project1.statuses.find!(@archived.id) }.should raise_error(MongoMapper::DocumentNotFound) end end context "with multiple ids" do it "should work for ids in association" do statuses = @project1.statuses.find(@brand_new.id, @complete.id) statuses.should == [@brand_new, @complete] end it "should not work for ids not in association" do lambda { @project1.statuses.find!(@brand_new.id, @complete.id, @archived.id) }.should raise_error(MongoMapper::DocumentNotFound) end end context "with #paginate" do before do @statuses = @project2.statuses.paginate(:per_page => 2, :page => 1, :order => 'name asc') end it "should return total pages" do @statuses.total_pages.should == 2 end it "should return total entries" do @statuses.total_entries.should == 3 end it "should return the subject" do @statuses.collect(&:name).should == %w(Archived Complete) end end end context "extending the association" do it "should work using a block passed to many" do project = Project.new(:name => "Some Project") status1 = Status.new(:name => "New") status2 = Status.new(:name => "Assigned") status3 = Status.new(:name => "Closed") project.statuses = [status1, status2, status3] project.save open_statuses = project.statuses.open open_statuses.should include(status1) open_statuses.should include(status2) open_statuses.should_not include(status3) end it "should work using many's :extend option" do project = Project.new(:name => "Some Project") collaborator1 = Collaborator.new(:name => "zing") collaborator2 = Collaborator.new(:name => "zang") project.collaborators = [collaborator1, collaborator2] project.save project.collaborators.top.should == collaborator1 end end context ":dependent" do before do # FIXME: make use of already defined models class ::Property include MongoMapper::Document end Property.collection.drop class ::Thing include MongoMapper::Document key :name, String end Thing.collection.drop end after do Object.send :remove_const, 'Property' if defined?(::Property) Object.send :remove_const, 'Thing' if defined?(::Thing) end context "=> destroy" do before do Property.key :thing_id, ObjectId Property.belongs_to :thing, :dependent => :destroy Thing.many :properties, :dependent => :destroy @thing = Thing.create(:name => "Tree") @property1 = Property.create @property2 = Property.create @property3 = Property.create @thing.properties << @property1 @thing.properties << @property2 @thing.properties << @property3 end it "should should destroy the associated documents" do @thing.properties.count.should == 3 @thing.destroy @thing.properties.count.should == 0 Property.count.should == 0 end end context "=> delete_all" do before do Property.key :thing_id, ObjectId Property.belongs_to :thing Thing.has_many :properties, :dependent => :delete_all @thing = Thing.create(:name => "Tree") @property1 = Property.create @property2 = Property.create @property3 = Property.create @thing.properties << @property1 @thing.properties << @property2 @thing.properties << @property3 end it "should should delete associated documents" do @thing.properties.count.should == 3 @thing.destroy @thing.properties.count.should == 0 Property.count.should == 0 end end context "=> nullify" do before do Property.key :thing_id, ObjectId Property.belongs_to :thing Thing.has_many :properties, :dependent => :nullify @thing = Thing.create(:name => "Tree") @property1 = Property.create @property2 = Property.create @property3 = Property.create @thing.properties << @property1 @thing.properties << @property2 @thing.properties << @property3 end it "should should nullify relationship but not destroy associated documents" do @thing.properties.count.should == 3 @thing.destroy @thing.properties.count.should == 0 Property.count.should == 3 end end context "unspecified" do before do Property.key :thing_id, ObjectId Property.belongs_to :thing Thing.has_many :properties, :dependent => :nullify @thing = Thing.create(:name => "Tree") @property1 = Property.create @property2 = Property.create @property3 = Property.create @thing.properties << @property1 @thing.properties << @property2 @thing.properties << @property3 end it "should should nullify relationship but not destroy associated documents" do @thing.properties.count.should == 3 @thing.destroy @thing.properties.count.should == 0 Property.count.should == 3 end end end context "namespaced foreign keys" do before do News::Paper.many :articles, :class_name => 'News::Article' News::Article.belongs_to :paper, :class_name => 'News::Paper' @paper = News::Paper.create end it "should properly infer the foreign key" do article = @paper.articles.create article.should respond_to(:paper_id) article.paper_id.should == @paper.id end end context "criteria" do before do News::Paper.many :articles, :class_name => 'News::Article' News::Article.belongs_to :paper, :class_name => 'News::Paper' @paper = News::Paper.create end it "should should find associated instances by an object ID" do article = News::Article.create(:paper_id => @paper.id) @paper.articles.should include(article) end it "should should find associated instances by a string" do article = News::Article.create(:paper_id => @paper.id.to_s) @paper.articles.should include(article) end end describe "regression with association proxy scoping" do before do @job_title_class = Doc do set_collection_name "job_titles" end @training_class = Doc do set_collection_name "trainings" key :slug, String key :is_active, Boolean, default: true def self.find_by_slug!(the_slug) if res = first(:slug => the_slug) res else raise "MissingSlugError" end end validates_presence_of :job_title_id validates_presence_of :slug validates_uniqueness_of :slug, :scope => :job_title_id end @job_title_class.has_many :trainings, :class => @training_class, :foreign_key => :job_title_id @training_class.belongs_to :job_title, :class => @job_title_class end it "should scope queries that return a single method on has many association with the right parent id" do @job_title_1 = @job_title_class.create! @job_title_2 = @job_title_class.create! @training_1 = @training_class.create!(:slug => 'foo', :job_title_id => @job_title_1.id) @training_2 = @training_class.create!(:slug => 'foo', :job_title_id => @job_title_2.id) @job_title_1.reload @job_title_2.reload @job_title_1.trainings.count.should == 1 @job_title_2.trainings.count.should == 1 @job_title_1.trainings.find_by_slug!('foo').should == @training_1 @job_title_2.trainings.find_by_slug!('foo').should == @training_2 lambda do @job_title_2.trainings.find_by_slug!('bar') end.should raise_error(RuntimeError) end it "should scope with an extra where clause on the proxy (regression #2)" do @job_title_1 = @job_title_class.create! @job_title_2 = @job_title_class.create! @training_1 = @training_class.create!(:slug => 'foo', :job_title_id => @job_title_1.id) @training_2 = @training_class.create!(:slug => 'foo', :job_title_id => @job_title_2.id) @job_title_1.reload @job_title_2.reload @job_title_1.trainings.count.should == 1 @job_title_2.trainings.count.should == 1 @job_title_1.trainings.where(:is_active => true).find_by_slug!('foo').should == @training_1 @job_title_2.trainings.where(:is_active => true).find_by_slug!('foo').should == @training_2 lambda do @job_title_2.trainings.find_by_slug!('bar') end.should raise_error(RuntimeError) end end end