PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/test/functional/associations/test_many_documents_proxy.rb

http://github.com/jnunemaker/mongomapper
Ruby | 804 lines | 691 code | 113 blank | 0 comment | 108 complexity | 35000c00440f299440504901e7d91f41 MD5 | raw file
  1. require 'test_helper.rb'
  2. require 'models'
  3. class ManyDocumentsProxyTest < Test::Unit::TestCase
  4. def setup
  5. Project.collection.remove
  6. Status.collection.remove
  7. @pet_class = Doc do
  8. key :name, String
  9. key :owner_id, ObjectId
  10. end
  11. @owner_class = Doc do
  12. key :name, String
  13. end
  14. @owner_class.many :pets, :class => @pet_class, :foreign_key => :owner_id, :order => 'name'
  15. end
  16. should "default reader to empty array" do
  17. project = Project.new
  18. project.statuses.should == []
  19. end
  20. should "allow overriding association methods" do
  21. @owner_class.class_eval do
  22. def pets
  23. super
  24. end
  25. end
  26. instance = @owner_class.new
  27. instance.pets.should == []
  28. instance.pets.build
  29. instance.pets.should_not be_empty
  30. end
  31. should "allow assignment of many associated documents using a hash" do
  32. person_attributes = {
  33. 'name' => 'Mr. Pet Lover',
  34. 'pets' => [
  35. {'name' => 'Jimmy', 'species' => 'Cocker Spainel'},
  36. {'name' => 'Sasha', 'species' => 'Siberian Husky'},
  37. ]
  38. }
  39. owner = @owner_class.new(person_attributes)
  40. owner.name.should == 'Mr. Pet Lover'
  41. owner.pets[0].name.should == 'Jimmy'
  42. owner.pets[0].species.should == 'Cocker Spainel'
  43. owner.pets[1].name.should == 'Sasha'
  44. owner.pets[1].species.should == 'Siberian Husky'
  45. owner.save.should be_true
  46. owner.reload
  47. owner.name.should == 'Mr. Pet Lover'
  48. owner.pets[0].name.should == 'Jimmy'
  49. owner.pets[0].species.should == 'Cocker Spainel'
  50. owner.pets[1].name.should == 'Sasha'
  51. owner.pets[1].species.should == 'Siberian Husky'
  52. end
  53. should "allow adding to association like it was an array" do
  54. project = Project.new
  55. project.statuses << Status.new(:name => 'Foo1!')
  56. project.statuses.push Status.new(:name => 'Foo2!')
  57. project.statuses.concat Status.new(:name => 'Foo3!')
  58. project.statuses.size.should == 3
  59. end
  60. context "replacing the association" do
  61. context "with objects of the class" do
  62. should "work" do
  63. project = Project.new
  64. project.statuses = [Status.new(:name => "ready")]
  65. project.save.should be_true
  66. project.reload
  67. project.statuses.size.should == 1
  68. project.statuses[0].name.should == "ready"
  69. end
  70. end
  71. context "with Hashes" do
  72. should "convert to objects of the class and work" do
  73. project = Project.new
  74. project.statuses = [{ 'name' => 'ready' }]
  75. project.save.should be_true
  76. project.reload
  77. project.statuses.size.should == 1
  78. project.statuses[0].name.should == "ready"
  79. end
  80. end
  81. context "with :dependent" do
  82. setup do
  83. @broker_class = Doc('Broker')
  84. @property_class = Doc('Property') do
  85. key :broker_id, ObjectId
  86. belongs_to :broker
  87. end
  88. end
  89. context "=> destroy" do
  90. setup do
  91. @broker_class.many :properties, :class => @property_class, :dependent => :destroy
  92. @broker = @broker_class.create(:name => "Bob")
  93. @property1 = @property_class.create
  94. @property2 = @property_class.create
  95. @property3 = @property_class.create
  96. @broker.properties << @property1
  97. @broker.properties << @property2
  98. @broker.properties << @property3
  99. end
  100. should "call destroy the existing documents" do
  101. @broker.properties[0].expects(:destroy).once
  102. @broker.properties[1].expects(:destroy).once
  103. @broker.properties[2].expects(:destroy).once
  104. @broker.properties = [@property_class.new]
  105. end
  106. should "remove the existing document from the database" do
  107. @property_class.count.should == 3
  108. @broker.properties = []
  109. @property_class.count.should == 0
  110. end
  111. should "skip over documents that are the same" do
  112. @broker.properties[0].expects(:destroy).never
  113. @broker.properties[1].expects(:destroy).once
  114. @broker.properties[2].expects(:destroy).never
  115. @broker.properties = [@property3, @property1]
  116. end
  117. end
  118. context "=> delete_all" do
  119. setup do
  120. @broker_class.many :properties, :class => @property_class, :dependent => :delete_all
  121. @broker = @broker_class.create(:name => "Bob")
  122. @property1 = @property_class.create
  123. @property2 = @property_class.create
  124. @property3 = @property_class.create
  125. @broker.properties << @property1
  126. @broker.properties << @property2
  127. @broker.properties << @property3
  128. end
  129. should "call delete the existing documents" do
  130. @broker.properties[0].expects(:delete).once
  131. @broker.properties[1].expects(:delete).once
  132. @broker.properties[2].expects(:delete).once
  133. @broker.properties = [@property_class.new]
  134. end
  135. should "remove the existing document from the database" do
  136. @property_class.count.should == 3
  137. @broker.properties = []
  138. @property_class.count.should == 0
  139. end
  140. should "skip over documents that are the same" do
  141. @broker.properties[0].expects(:delete).never
  142. @broker.properties[1].expects(:delete).once
  143. @broker.properties[2].expects(:delete).never
  144. @broker.properties = [@property3, @property1]
  145. end
  146. end
  147. context "=> nullify" do
  148. setup do
  149. @broker_class.many :properties, :class => @property_class, :dependent => :nullify
  150. @broker = @broker_class.create(:name => "Bob")
  151. @property1 = @property_class.create
  152. @property2 = @property_class.create
  153. @property3 = @property_class.create
  154. @broker.properties << @property1
  155. @broker.properties << @property2
  156. @broker.properties << @property3
  157. end
  158. should "nullify the existing documents" do
  159. @property1.reload.broker_id.should == @broker.id
  160. @property2.reload.broker_id.should == @broker.id
  161. @property3.reload.broker_id.should == @broker.id
  162. @broker.properties = [@property_class.new]
  163. @property1.reload.broker_id.should be_nil
  164. @property2.reload.broker_id.should be_nil
  165. @property3.reload.broker_id.should be_nil
  166. end
  167. should "skip over documents that are the same" do
  168. @broker.properties = [@property3, @property1]
  169. @property1.reload.broker_id.should == @broker.id
  170. @property2.reload.broker_id.should be_nil
  171. @property3.reload.broker_id.should == @broker.id
  172. end
  173. should "work" do
  174. old_properties = @broker.properties
  175. @broker.properties = [@property1, @property2, @property3]
  176. old_properties.should == @broker.properties
  177. end
  178. end
  179. context "unspecified" do
  180. should "nullify the existing documents" do
  181. @broker_class.many :properties, :class => @property_class
  182. @broker = @broker_class.create(:name => "Bob")
  183. @property1 = @property_class.create
  184. @property2 = @property_class.create
  185. @property3 = @property_class.create
  186. @broker.properties << @property1
  187. @broker.properties << @property2
  188. @broker.properties << @property3
  189. @broker.properties = [@property_class.new]
  190. @property1.reload.broker_id.should be_nil
  191. @property2.reload.broker_id.should be_nil
  192. @property3.reload.broker_id.should be_nil
  193. end
  194. end
  195. end
  196. end
  197. context "using <<, push and concat" do
  198. context "with objects of the class" do
  199. should "correctly assign foreign key" do
  200. project = Project.new
  201. project.statuses << Status.new(:name => '<<')
  202. project.statuses.push Status.new(:name => 'push')
  203. project.statuses.concat Status.new(:name => 'concat')
  204. project.reload
  205. project.statuses[0].project_id.should == project.id
  206. project.statuses[1].project_id.should == project.id
  207. project.statuses[2].project_id.should == project.id
  208. end
  209. end
  210. context "with Hashes" do
  211. should "correctly convert to objects and assign foreign key" do
  212. project = Project.new
  213. project.statuses << { 'name' => '<<' }
  214. project.statuses.push( { 'name' => 'push' })
  215. project.statuses.concat({ 'name' => 'concat' })
  216. project.reload
  217. project.statuses[0].project_id.should == project.id
  218. project.statuses[1].project_id.should == project.id
  219. project.statuses[2].project_id.should == project.id
  220. end
  221. end
  222. end
  223. context "build" do
  224. should "assign foreign key" do
  225. project = Project.create
  226. status = project.statuses.build
  227. status.project_id.should == project.id
  228. end
  229. should "allow assigning attributes" do
  230. project = Project.create
  231. status = project.statuses.build(:name => 'Foo')
  232. status.name.should == 'Foo'
  233. end
  234. should "reset cache" do
  235. project = Project.create
  236. project.statuses.size.should == 0
  237. status = project.statuses.build(:name => 'Foo')
  238. status.save!
  239. project.statuses.size.should == 1
  240. end
  241. should "update collection without save" do
  242. project = Project.create
  243. project.statuses.build(:name => 'Foo')
  244. project.statuses.size.should == 1
  245. end
  246. should "save built document when saving parent" do
  247. project = Project.create
  248. status = project.statuses.build(:name => 'Foo')
  249. project.save!
  250. status.should_not be_new
  251. end
  252. should "not save the parent when building associations" do
  253. project = Project.new
  254. status = project.statuses.build(:name => 'Foo')
  255. project.should be_new
  256. end
  257. should "not save the built object" do
  258. project = Project.new
  259. status = project.statuses.build(:name => 'Foo')
  260. status.should be_new
  261. end
  262. end
  263. context "create" do
  264. should "assign foreign key" do
  265. project = Project.create
  266. status = project.statuses.create(:name => 'Foo!')
  267. status.project_id.should == project.id
  268. end
  269. should "save record" do
  270. project = Project.create
  271. lambda {
  272. project.statuses.create(:name => 'Foo!')
  273. }.should change { Status.count }
  274. end
  275. should "allow passing attributes" do
  276. project = Project.create
  277. status = project.statuses.create(:name => 'Foo!')
  278. status.name.should == 'Foo!'
  279. end
  280. should "reset cache" do
  281. project = Project.create
  282. project.statuses.size.should == 0
  283. project.statuses.create(:name => 'Foo!')
  284. project.statuses.size.should == 1
  285. end
  286. end
  287. context "create!" do
  288. should "assign foreign key" do
  289. project = Project.create
  290. status = project.statuses.create!(:name => 'Foo!')
  291. status.project_id.should == project.id
  292. end
  293. should "save record" do
  294. project = Project.create
  295. lambda {
  296. project.statuses.create!(:name => 'Foo!')
  297. }.should change { Status.count }
  298. end
  299. should "allow passing attributes" do
  300. project = Project.create
  301. status = project.statuses.create!(:name => 'Foo!')
  302. status.name.should == 'Foo!'
  303. end
  304. should "raise exception if not valid" do
  305. project = Project.create
  306. lambda {
  307. project.statuses.create!(:name => nil)
  308. }.should raise_error(MongoMapper::DocumentNotValid)
  309. end
  310. should "reset cache" do
  311. project = Project.create
  312. project.statuses.size.should == 0
  313. project.statuses.create!(:name => 'Foo!')
  314. project.statuses.size.should == 1
  315. end
  316. end
  317. context "count" do
  318. should "work scoped to association" do
  319. project = Project.create
  320. 3.times { project.statuses.create(:name => 'Foo!') }
  321. other_project = Project.create
  322. 2.times { other_project.statuses.create(:name => 'Foo!') }
  323. project.statuses.count.should == 3
  324. other_project.statuses.count.should == 2
  325. end
  326. should "work with conditions" do
  327. project = Project.create
  328. project.statuses.create(:name => 'Foo')
  329. project.statuses.create(:name => 'Other 1')
  330. project.statuses.create(:name => 'Other 2')
  331. project.statuses.count(:name => 'Foo').should == 1
  332. end
  333. end
  334. context "to_json" do
  335. should "work on association" do
  336. project = Project.create
  337. 3.times { |i| project.statuses.create(:name => i.to_s) }
  338. JSON.parse(project.statuses.to_json).collect{|status| status["name"] }.sort.should == ["0","1","2"]
  339. end
  340. end
  341. context "as_json" do
  342. should "work on association" do
  343. project = Project.create
  344. 3.times { |i| project.statuses.create(:name => i.to_s) }
  345. project.statuses.as_json.collect{|status| status["name"] }.sort.should == ["0","1","2"]
  346. end
  347. end
  348. context "Unassociating documents" do
  349. setup do
  350. @project = Project.create
  351. @project.statuses << Status.create(:name => '1')
  352. @project.statuses << Status.create(:name => '2')
  353. @project2 = Project.create
  354. @project2.statuses << Status.create(:name => '1')
  355. @project2.statuses << Status.create(:name => '2')
  356. end
  357. should "work with destroy all" do
  358. @project.statuses.count.should == 2
  359. @project.statuses.destroy_all
  360. @project.statuses.count.should == 0
  361. @project2.statuses.count.should == 2
  362. Status.count.should == 2
  363. end
  364. should "work with destroy all and conditions" do
  365. @project.statuses.count.should == 2
  366. @project.statuses.destroy_all(:name => '1')
  367. @project.statuses.count.should == 1
  368. @project2.statuses.count.should == 2
  369. Status.count.should == 3
  370. end
  371. should "work with delete all" do
  372. @project.statuses.count.should == 2
  373. @project.statuses.delete_all
  374. @project.statuses.count.should == 0
  375. @project2.statuses.count.should == 2
  376. Status.count.should == 2
  377. end
  378. should "work with delete all and conditions" do
  379. @project.statuses.count.should == 2
  380. @project.statuses.delete_all(:name => '1')
  381. @project.statuses.count.should == 1
  382. @project2.statuses.count.should == 2
  383. Status.count.should == 3
  384. end
  385. should "work with nullify" do
  386. @project.statuses.count.should == 2
  387. @project.statuses.nullify
  388. @project.statuses.count.should == 0
  389. @project2.statuses.count.should == 2
  390. Status.count.should == 4
  391. Status.count(:name => '1').should == 2
  392. Status.count(:name => '2').should == 2
  393. end
  394. end
  395. context "Finding scoped to association" do
  396. setup do
  397. @project1 = Project.new(:name => 'Project 1')
  398. @brand_new = Status.create(:name => 'New', :position => 1 )
  399. @complete = Status.create(:name => 'Complete', :position => 2)
  400. @project1.statuses = [@brand_new, @complete]
  401. @project1.save
  402. @project2 = Project.create(:name => 'Project 2')
  403. @in_progress = Status.create(:name => 'In Progress')
  404. @archived = Status.create(:name => 'Archived')
  405. @another_complete = Status.create(:name => 'Complete')
  406. @project2.statuses = [@in_progress, @archived, @another_complete]
  407. @project2.save
  408. end
  409. context "include?" do
  410. should "return true if in association" do
  411. @project1.statuses.should include(@brand_new)
  412. end
  413. should "return false if not in association" do
  414. @project1.statuses.should_not include(@in_progress)
  415. end
  416. end
  417. context "dynamic finders" do
  418. should "work with single key" do
  419. @project1.statuses.find_by_name('New').should == @brand_new
  420. @project1.statuses.find_by_name!('New').should == @brand_new
  421. @project2.statuses.find_by_name('In Progress').should == @in_progress
  422. @project2.statuses.find_by_name!('In Progress').should == @in_progress
  423. end
  424. should "work with multiple keys" do
  425. @project1.statuses.find_by_name_and_position('New', 1).should == @brand_new
  426. @project1.statuses.find_by_name_and_position!('New', 1).should == @brand_new
  427. @project1.statuses.find_by_name_and_position('New', 2).should be_nil
  428. end
  429. should "raise error when using !" do
  430. lambda {
  431. @project1.statuses.find_by_name!('Fake')
  432. }.should raise_error(MongoMapper::DocumentNotFound)
  433. end
  434. context "find_or_create_by" do
  435. should "not create document if found" do
  436. lambda {
  437. status = @project1.statuses.find_or_create_by_name('New')
  438. status.project.should == @project1
  439. status.should == @brand_new
  440. }.should_not change { Status.count }
  441. end
  442. should "create document if not found" do
  443. lambda {
  444. status = @project1.statuses.find_or_create_by_name('Delivered')
  445. status.project.should == @project1
  446. }.should change { Status.count }
  447. end
  448. end
  449. end
  450. context "sexy querying" do
  451. should "work with where" do
  452. @project1.statuses.where(:name => 'New').all.should == [@brand_new]
  453. end
  454. should "work with sort" do
  455. @project1.statuses.sort(:name).all.should == [@complete, @brand_new]
  456. end
  457. should "work with limit" do
  458. @project1.statuses.sort(:name).limit(1).all.should == [@complete]
  459. end
  460. should "work with skip" do
  461. @project1.statuses.sort(:name).skip(1).all.should == [@brand_new]
  462. end
  463. should "work with fields" do
  464. @project1.statuses.fields(:position).all.each do |status|
  465. status.position.should_not be_nil
  466. status.name.should be_nil
  467. end
  468. end
  469. should "work with scopes" do
  470. @project1.statuses.complete.all.should == [@complete]
  471. end
  472. should "work with methods on class that return query" do
  473. @project1.statuses.by_position(1).first.should == @brand_new
  474. end
  475. should "not work with methods on class that do not return query" do
  476. Status.class_eval { def self.foo; 'foo' end }
  477. lambda { @project1.statuses.foo }.
  478. should raise_error(NoMethodError)
  479. end
  480. end
  481. context "all" do
  482. should "work" do
  483. @project1.statuses.all(:order => "position asc").should == [@brand_new, @complete]
  484. end
  485. should "work with conditions" do
  486. @project1.statuses.all(:name => 'Complete').should == [@complete]
  487. end
  488. end
  489. context "first" do
  490. should "work" do
  491. @project1.statuses.first(:order => 'name').should == @complete
  492. end
  493. should "work with conditions" do
  494. @project1.statuses.first(:name => 'Complete').should == @complete
  495. end
  496. end
  497. context "last" do
  498. should "work" do
  499. @project1.statuses.last(:order => "position asc").should == @complete
  500. end
  501. should "work with conditions" do
  502. @project1.statuses.last(:order => 'position', :name => 'New').should == @brand_new
  503. end
  504. end
  505. context "with one id" do
  506. should "work for id in association" do
  507. @project1.statuses.find(@complete.id).should == @complete
  508. end
  509. should "not work for id not in association" do
  510. lambda {
  511. @project1.statuses.find!(@archived.id)
  512. }.should raise_error(MongoMapper::DocumentNotFound)
  513. end
  514. end
  515. context "with multiple ids" do
  516. should "work for ids in association" do
  517. statuses = @project1.statuses.find(@brand_new.id, @complete.id)
  518. statuses.should == [@brand_new, @complete]
  519. end
  520. should "not work for ids not in association" do
  521. assert_raises(MongoMapper::DocumentNotFound) do
  522. @project1.statuses.find!(@brand_new.id, @complete.id, @archived.id)
  523. end
  524. end
  525. end
  526. context "with #paginate" do
  527. setup do
  528. @statuses = @project2.statuses.paginate(:per_page => 2, :page => 1, :order => 'name asc')
  529. end
  530. should "return total pages" do
  531. @statuses.total_pages.should == 2
  532. end
  533. should "return total entries" do
  534. @statuses.total_entries.should == 3
  535. end
  536. should "return the subject" do
  537. @statuses.collect(&:name).should == %w(Archived Complete)
  538. end
  539. end
  540. end
  541. context "extending the association" do
  542. should "work using a block passed to many" do
  543. project = Project.new(:name => "Some Project")
  544. status1 = Status.new(:name => "New")
  545. status2 = Status.new(:name => "Assigned")
  546. status3 = Status.new(:name => "Closed")
  547. project.statuses = [status1, status2, status3]
  548. project.save
  549. open_statuses = project.statuses.open
  550. open_statuses.should include(status1)
  551. open_statuses.should include(status2)
  552. open_statuses.should_not include(status3)
  553. end
  554. should "work using many's :extend option" do
  555. project = Project.new(:name => "Some Project")
  556. collaborator1 = Collaborator.new(:name => "zing")
  557. collaborator2 = Collaborator.new(:name => "zang")
  558. project.collaborators = [collaborator1, collaborator2]
  559. project.save
  560. project.collaborators.top.should == collaborator1
  561. end
  562. end
  563. context ":dependent" do
  564. setup do
  565. # FIXME: make use of already defined models
  566. class ::Property
  567. include MongoMapper::Document
  568. end
  569. Property.collection.remove
  570. class ::Thing
  571. include MongoMapper::Document
  572. key :name, String
  573. end
  574. Thing.collection.remove
  575. end
  576. teardown do
  577. Object.send :remove_const, 'Property' if defined?(::Property)
  578. Object.send :remove_const, 'Thing' if defined?(::Thing)
  579. end
  580. context "=> destroy" do
  581. setup do
  582. Property.key :thing_id, ObjectId
  583. Property.belongs_to :thing, :dependent => :destroy
  584. Thing.many :properties, :dependent => :destroy
  585. @thing = Thing.create(:name => "Tree")
  586. @property1 = Property.create
  587. @property2 = Property.create
  588. @property3 = Property.create
  589. @thing.properties << @property1
  590. @thing.properties << @property2
  591. @thing.properties << @property3
  592. end
  593. should "should destroy the associated documents" do
  594. @thing.properties.count.should == 3
  595. @thing.destroy
  596. @thing.properties.count.should == 0
  597. Property.count.should == 0
  598. end
  599. end
  600. context "=> delete_all" do
  601. setup do
  602. Property.key :thing_id, ObjectId
  603. Property.belongs_to :thing
  604. Thing.has_many :properties, :dependent => :delete_all
  605. @thing = Thing.create(:name => "Tree")
  606. @property1 = Property.create
  607. @property2 = Property.create
  608. @property3 = Property.create
  609. @thing.properties << @property1
  610. @thing.properties << @property2
  611. @thing.properties << @property3
  612. end
  613. should "should delete associated documents" do
  614. @thing.properties.count.should == 3
  615. @thing.destroy
  616. @thing.properties.count.should == 0
  617. Property.count.should == 0
  618. end
  619. end
  620. context "=> nullify" do
  621. setup do
  622. Property.key :thing_id, ObjectId
  623. Property.belongs_to :thing
  624. Thing.has_many :properties, :dependent => :nullify
  625. @thing = Thing.create(:name => "Tree")
  626. @property1 = Property.create
  627. @property2 = Property.create
  628. @property3 = Property.create
  629. @thing.properties << @property1
  630. @thing.properties << @property2
  631. @thing.properties << @property3
  632. end
  633. should "should nullify relationship but not destroy associated documents" do
  634. @thing.properties.count.should == 3
  635. @thing.destroy
  636. @thing.properties.count.should == 0
  637. Property.count.should == 3
  638. end
  639. end
  640. context "unspecified" do
  641. setup do
  642. Property.key :thing_id, ObjectId
  643. Property.belongs_to :thing
  644. Thing.has_many :properties, :dependent => :nullify
  645. @thing = Thing.create(:name => "Tree")
  646. @property1 = Property.create
  647. @property2 = Property.create
  648. @property3 = Property.create
  649. @thing.properties << @property1
  650. @thing.properties << @property2
  651. @thing.properties << @property3
  652. end
  653. should "should nullify relationship but not destroy associated documents" do
  654. @thing.properties.count.should == 3
  655. @thing.destroy
  656. @thing.properties.count.should == 0
  657. Property.count.should == 3
  658. end
  659. end
  660. end
  661. context "namespaced foreign keys" do
  662. setup do
  663. News::Paper.many :articles, :class_name => 'News::Article'
  664. News::Article.belongs_to :paper, :class_name => 'News::Paper'
  665. @paper = News::Paper.create
  666. end
  667. should "properly infer the foreign key" do
  668. article = @paper.articles.create
  669. article.should respond_to(:paper_id)
  670. article.paper_id.should == @paper.id
  671. end
  672. end
  673. end