has_many_through_associations_test.rb 37.7 KB
Newer Older
1
require "cases/helper"
2 3
require 'models/post'
require 'models/person'
4 5
require 'models/reference'
require 'models/job'
6
require 'models/reader'
7
require 'models/comment'
8
require 'models/rating'
9 10 11
require 'models/tag'
require 'models/tagging'
require 'models/author'
12 13 14
require 'models/owner'
require 'models/pet'
require 'models/toy'
15 16 17
require 'models/contract'
require 'models/company'
require 'models/developer'
18 19 20
require 'models/subscriber'
require 'models/book'
require 'models/subscription'
21 22
require 'models/essay'
require 'models/category'
23
require 'models/categorization'
24 25 26
require 'models/member'
require 'models/membership'
require 'models/club'
27 28

class HasManyThroughAssociationsTest < ActiveRecord::TestCase
29 30
  fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
           :owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses,
A
Aaron Patterson 已提交
31
           :subscribers, :books, :subscriptions, :developers, :categorizations, :essays,
32
           :categories_posts, :clubs, :memberships
33

34 35 36 37 38
  # Dummies to force column loads so query counts are clean.
  def setup
    Person.create :first_name => 'gummy'
    Reader.create :person_id => 0, :post_id => 0
  end
39

40 41 42 43 44 45 46
  def test_preload_sti_rhs_class
    developers = Developer.includes(:firms).all.to_a
    assert_no_queries do
      developers.each { |d| d.firms }
    end
  end

47 48 49 50 51 52 53 54 55
  def test_preload_sti_middle_relation
    club = Club.create!(name: 'Aaron cool banana club')
    member1 = Member.create!(name: 'Aaron')
    member2 = Member.create!(name: 'Cat')

    SuperMembership.create! club: club, member: member1
    CurrentMembership.create! club: club, member: member2

    club1 = Club.includes(:members).find_by_id club.id
56 57
    assert_equal [member1, member2].sort_by(&:id),
                 club1.members.sort_by(&:id)
58 59
  end

60 61 62 63
  def make_model(name)
    Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
  end

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
  def test_ordered_habtm
    person_prime = Class.new(ActiveRecord::Base) do
      def self.name; 'Person'; end

      has_many :readers
      has_many :posts, -> { order('posts.id DESC') }, :through => :readers
    end
    posts = person_prime.includes(:posts).first.posts

    assert_operator posts.length, :>, 1
    posts.each_cons(2) do |left,right|
      assert_operator left.id, :>, right.id
    end
  end

79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
  def test_singleton_has_many_through
    book         = make_model "Book"
    subscription = make_model "Subscription"
    subscriber   = make_model "Subscriber"

    subscriber.primary_key = 'nick'
    subscription.belongs_to :book,       class: book
    subscription.belongs_to :subscriber, class: subscriber

    book.has_many :subscriptions, class: subscription
    book.has_many :subscribers, through: :subscriptions, class: subscriber

    anonbook = book.first
    namebook = Book.find anonbook.id

    assert_operator anonbook.subscribers.count, :>, 0
    anonbook.subscribers.each do |s|
      assert_instance_of subscriber, s
    end
    assert_equal namebook.subscribers.map(&:id).sort,
                 anonbook.subscribers.map(&:id).sort
  end

102
  def test_no_pk_join_table_append
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
    lesson, _, student = make_no_pk_hm_t

    sicp = lesson.new(:name => "SICP")
    ben = student.new(:name => "Ben Bitdiddle")
    sicp.students << ben
    assert sicp.save!
  end

  def test_no_pk_join_table_delete
    lesson, lesson_student, student = make_no_pk_hm_t

    sicp = lesson.new(:name => "SICP")
    ben = student.new(:name => "Ben Bitdiddle")
    louis = student.new(:name => "Louis Reasoner")
    sicp.students << ben
    sicp.students << louis
    assert sicp.save!

    sicp.students.reload
    assert_operator lesson_student.count, :>=, 2
    assert_no_difference('student.count') do
      assert_difference('lesson_student.count', -2) do
        sicp.students.destroy(*student.all.to_a)
      end
    end
  end

  def test_no_pk_join_model_callbacks
    lesson, lesson_student, student = make_no_pk_hm_t

    after_destroy_called = false
    lesson_student.after_destroy do
      after_destroy_called = true
    end

    sicp = lesson.new(:name => "SICP")
    ben = student.new(:name => "Ben Bitdiddle")
    sicp.students << ben
    assert sicp.save!

    sicp.students.reload
    sicp.students.destroy(*student.all.to_a)
    assert after_destroy_called, "after destroy should be called"
  end

  def make_no_pk_hm_t
149 150 151 152 153 154 155 156 157 158
    lesson = make_model 'Lesson'
    student = make_model 'Student'

    lesson_student = make_model 'LessonStudent'
    lesson_student.table_name = 'lessons_students'

    lesson_student.belongs_to :lesson, :class => lesson
    lesson_student.belongs_to :student, :class => student
    lesson.has_many :lesson_students, :class => lesson_student
    lesson.has_many :students, :through => :lesson_students, :class => student
159
    [lesson, lesson_student, student]
160 161
  end

162 163 164 165
  def test_pk_is_not_required_for_join
    post  = Post.includes(:scategories).first
    post2 = Post.includes(:categories).first

166
    assert_operator post.categories.length, :>, 0
167 168 169
    assert_equal post2.categories, post.categories
  end

170 171 172 173 174 175 176
  def test_include?
    person = Person.new
    post = Post.new
    person.posts << post
    assert person.posts.include?(post)
  end

177
  def test_associate_existing
178 179
    post   = posts(:thinking)
    person = people(:david)
180

181
    assert_queries(1) do
182
      post.people << person
183
    end
184

185
    assert_queries(1) do
186
      assert post.people.include?(person)
187
    end
188

189 190 191
    assert post.reload.people(true).include?(person)
  end

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
  def test_delete_all_for_with_dependent_option_destroy
    person = people(:david)
    assert_equal 1, person.jobs_with_dependent_destroy.count

    assert_no_difference 'Job.count' do
      assert_difference 'Reference.count', -1 do
        person.reload.jobs_with_dependent_destroy.delete_all
      end
    end
  end

  def test_delete_all_for_with_dependent_option_nullify
    person = people(:david)
    assert_equal 1, person.jobs_with_dependent_nullify.count

    assert_no_difference 'Job.count' do
      assert_no_difference 'Reference.count' do
        person.reload.jobs_with_dependent_nullify.delete_all
      end
    end
  end

  def test_delete_all_for_with_dependent_option_delete_all
    person = people(:david)
    assert_equal 1, person.jobs_with_dependent_delete_all.count

    assert_no_difference 'Job.count' do
      assert_difference 'Reference.count', -1 do
        person.reload.jobs_with_dependent_delete_all.delete_all
      end
    end
  end

225 226 227 228 229 230 231
  def test_concat
    person = people(:david)
    post   = posts(:thinking)
    post.people.concat [person]
    assert_equal 1, post.people.size
    assert_equal 1, post.people(true).size
  end
232

233 234 235 236 237 238 239 240 241 242
  def test_associate_existing_record_twice_should_add_to_target_twice
    post   = posts(:thinking)
    person = people(:david)

    assert_difference 'post.people.to_a.count', 2 do
      post.people << person
      post.people << person
    end
  end

243 244 245 246 247 248 249 250 251 252
  def test_associate_existing_record_twice_should_add_records_twice
    post   = posts(:thinking)
    person = people(:david)

    assert_difference 'post.people.count', 2 do
      post.people << person
      post.people << person
    end
  end

253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
  def test_add_two_instance_and_then_deleting
    post   = posts(:thinking)
    person = people(:david)

    post.people << person
    post.people << person

    counts = ['post.people.count', 'post.people.to_a.count', 'post.readers.count', 'post.readers.to_a.count']
    assert_difference counts, -2 do
      post.people.delete(person)
    end

    assert !post.people.reload.include?(person)
  end

268 269 270
  def test_associating_new
    assert_queries(1) { posts(:thinking) }
    new_person = nil # so block binding catches it
271

272
    assert_queries(0) do
273
      new_person = Person.new :first_name => 'bob'
274
    end
275

276 277 278 279 280
    # Associating new records always saves them
    # Thus, 1 query for the new person record, 1 query for the new join table record
    assert_queries(2) do
      posts(:thinking).people << new_person
    end
281

282 283 284
    assert_queries(1) do
      assert posts(:thinking).people.include?(new_person)
    end
285

286 287 288 289 290
    assert posts(:thinking).reload.people(true).include?(new_person)
  end

  def test_associate_new_by_building
    assert_queries(1) { posts(:thinking) }
291

292
    assert_queries(0) do
293 294
      posts(:thinking).people.build(:first_name => "Bob")
      posts(:thinking).people.new(:first_name => "Ted")
295
    end
296

297 298 299 300 301
    # Should only need to load the association once
    assert_queries(1) do
      assert posts(:thinking).people.collect(&:first_name).include?("Bob")
      assert posts(:thinking).people.collect(&:first_name).include?("Ted")
    end
302

303 304 305 306
    # 2 queries for each new record (1 to save the record itself, 1 for the join model)
    #    * 2 new records = 4
    # + 1 query to save the actual post = 5
    assert_queries(5) do
307
      posts(:thinking).body += '-changed'
308 309
      posts(:thinking).save
    end
310

311 312 313 314
    assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Bob")
    assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Ted")
  end

315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
  def test_build_then_save_with_has_many_inverse
    post   = posts(:thinking)
    person = post.people.build(:first_name => "Bob")
    person.save
    post.reload

    assert post.people.include?(person)
  end

  def test_build_then_save_with_has_one_inverse
    post   = posts(:thinking)
    person = post.single_people.build(:first_name => "Bob")
    person.save
    post.reload

    assert post.single_people.include?(person)
  end

333 334 335 336 337 338 339 340 341 342 343 344 345
  def test_both_parent_ids_set_when_saving_new
    post = Post.new(title: 'Hello', body: 'world')
    person = Person.new(first_name: 'Sean')

    post.people = [person]
    post.save

    assert post.id
    assert person.id
    assert_equal post.id, post.readers.first.post_id
    assert_equal person.id, post.readers.first.person_id
  end

346 347
  def test_delete_association
    assert_queries(2){posts(:welcome);people(:michael); }
348

349 350 351
    assert_queries(1) do
      posts(:welcome).people.delete(people(:michael))
    end
352

353 354 355
    assert_queries(1) do
      assert posts(:welcome).people.empty?
    end
356

357 358 359
    assert posts(:welcome).reload.people(true).empty?
  end

360
  def test_destroy_association
361 362 363 364
    assert_no_difference "Person.count" do
      assert_difference "Reader.count", -1 do
        posts(:welcome).people.destroy(people(:michael))
      end
365 366 367 368 369 370 371
    end

    assert posts(:welcome).reload.people.empty?
    assert posts(:welcome).people(true).empty?
  end

  def test_destroy_all
372 373 374 375
    assert_no_difference "Person.count" do
      assert_difference "Reader.count", -1 do
        posts(:welcome).people.destroy_all
      end
376 377 378 379 380 381
    end

    assert posts(:welcome).reload.people.empty?
    assert posts(:welcome).people(true).empty?
  end

382 383 384 385 386 387
  def test_should_raise_exception_for_destroying_mismatching_records
    assert_no_difference ["Person.count", "Reader.count"] do
      assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:welcome).people.destroy(posts(:thinking)) }
    end
  end

388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
  def test_delete_through_belongs_to_with_dependent_nullify
    Reference.make_comments = true

    person    = people(:michael)
    job       = jobs(:magician)
    reference = Reference.where(:job_id => job.id, :person_id => person.id).first

    assert_no_difference ['Job.count', 'Reference.count'] do
      assert_difference 'person.jobs.count', -1 do
        person.jobs_with_dependent_nullify.delete(job)
      end
    end

    assert_equal nil, reference.reload.job_id
  ensure
    Reference.make_comments = false
  end

  def test_delete_through_belongs_to_with_dependent_delete_all
    Reference.make_comments = true

    person = people(:michael)
    job    = jobs(:magician)

    # Make sure we're not deleting everything
    assert person.jobs.count >= 2

    assert_no_difference 'Job.count' do
      assert_difference ['person.jobs.count', 'Reference.count'], -1 do
        person.jobs_with_dependent_delete_all.delete(job)
      end
    end

    # Check that the destroy callback on Reference did not run
    assert_equal nil, person.reload.comments
  ensure
    Reference.make_comments = false
  end

  def test_delete_through_belongs_to_with_dependent_destroy
    Reference.make_comments = true

    person = people(:michael)
    job    = jobs(:magician)

    # Make sure we're not deleting everything
    assert person.jobs.count >= 2

    assert_no_difference 'Job.count' do
      assert_difference ['person.jobs.count', 'Reference.count'], -1 do
        person.jobs_with_dependent_destroy.delete(job)
      end
    end

    # Check that the destroy callback on Reference ran
    assert_equal "Reference destroyed", person.reload.comments
  ensure
    Reference.make_comments = false
  end

  def test_belongs_to_with_dependent_destroy
    person = PersonWithDependentDestroyJobs.find(1)

    # Create a reference which is not linked to a job. This should not be destroyed.
    person.references.create!

    assert_no_difference 'Job.count' do
      assert_difference 'Reference.count', -person.jobs.count do
        person.destroy
      end
    end
  end

  def test_belongs_to_with_dependent_delete_all
    person = PersonWithDependentDeleteAllJobs.find(1)

    # Create a reference which is not linked to a job. This should not be destroyed.
    person.references.create!

    assert_no_difference 'Job.count' do
      assert_difference 'Reference.count', -person.jobs.count do
        person.destroy
      end
    end
  end

  def test_belongs_to_with_dependent_nullify
    person = PersonWithDependentNullifyJobs.find(1)

    references = person.references.to_a

    assert_no_difference ['Reference.count', 'Job.count'] do
      person.destroy
    end

    references.each do |reference|
      assert_equal nil, reference.reload.job_id
    end
  end

488 489 490 491 492 493 494 495 496 497 498 499
  def test_update_counter_caches_on_delete
    post = posts(:welcome)
    tag  = post.tags.create!(:name => 'doomed')

    assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do
      posts(:welcome).tags.delete(tag)
    end
  end

  def test_update_counter_caches_on_delete_with_dependent_destroy
    post = posts(:welcome)
    tag  = post.tags.create!(:name => 'doomed')
500
    post.update_columns(tags_with_destroy_count: post.tags.count)
501 502 503 504 505 506 507 508 509

    assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do
      posts(:welcome).tags_with_destroy.delete(tag)
    end
  end

  def test_update_counter_caches_on_delete_with_dependent_nullify
    post = posts(:welcome)
    tag  = post.tags.create!(:name => 'doomed')
510
    post.update_columns(tags_with_nullify_count: post.tags.count)
511 512 513 514 515 516 517 518

    assert_no_difference 'post.reload.taggings_count' do
      assert_difference 'post.reload.tags_with_nullify_count', -1 do
        posts(:welcome).tags_with_nullify.delete(tag)
      end
    end
  end

519 520 521 522 523 524 525 526 527 528 529
  def test_update_counter_caches_on_replace_association
    post = posts(:welcome)
    tag  = post.tags.create!(:name => 'doomed')
    tag.tagged_posts << posts(:thinking)

    tag.tagged_posts = []
    post.reload

    assert_equal(post.taggings.count, post.taggings_count)
  end

530 531 532 533 534 535 536 537 538
  def test_update_counter_caches_on_destroy
    post = posts(:welcome)
    tag  = post.tags.create!(name: 'doomed')

    assert_difference 'post.reload.taggings_count', -1 do
      tag.tagged_posts.destroy(post)
    end
  end

539 540
  def test_replace_association
    assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
541

542 543 544 545 546
    # 1 query to delete the existing reader (michael)
    # 1 query to associate the new reader (david)
    assert_queries(2) do
      posts(:welcome).people = [people(:david)]
    end
547

548 549 550 551
    assert_queries(0){
      assert posts(:welcome).people.include?(people(:david))
      assert !posts(:welcome).people.include?(people(:michael))
    }
552

553 554 555 556
    assert posts(:welcome).reload.people(true).include?(people(:david))
    assert !posts(:welcome).reload.people(true).include?(people(:michael))
  end

557 558 559
  def test_replace_order_is_preserved
    posts(:welcome).people.clear
    posts(:welcome).people = [people(:david), people(:michael)]
560
    assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order('id').map(&:person_id)
561 562 563 564

    # Test the inverse order in case the first success was a coincidence
    posts(:welcome).people.clear
    posts(:welcome).people = [people(:michael), people(:david)]
565
    assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order('id').map(&:person_id)
566 567 568 569 570
  end

  def test_replace_by_id_order_is_preserved
    posts(:welcome).people.clear
    posts(:welcome).person_ids = [people(:david).id, people(:michael).id]
571
    assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order('id').map(&:person_id)
572 573 574 575

    # Test the inverse order in case the first success was a coincidence
    posts(:welcome).people.clear
    posts(:welcome).person_ids = [people(:michael).id, people(:david).id]
576
    assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order('id').map(&:person_id)
577 578
  end

579 580
  def test_associate_with_create
    assert_queries(1) { posts(:thinking) }
581

582 583 584 585 586
    # 1 query for the new record, 1 for the join table record
    # No need to update the actual collection yet!
    assert_queries(2) do
      posts(:thinking).people.create(:first_name=>"Jeb")
    end
587

588 589 590 591
    # *Now* we actually need the collection so it's loaded
    assert_queries(1) do
      assert posts(:thinking).people.collect(&:first_name).include?("Jeb")
    end
592

593 594 595
    assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb")
  end

S
Steven Soroka 已提交
596 597
  def test_associate_with_create_and_no_options
    peeps = posts(:thinking).people.count
598
    posts(:thinking).people.create(:first_name => 'foo')
S
Steven Soroka 已提交
599 600 601
    assert_equal peeps + 1, posts(:thinking).people.count
  end

602 603 604 605 606 607
  def test_associate_with_create_with_through_having_conditions
    impatient_people = posts(:thinking).impatient_people.count
    posts(:thinking).impatient_people.create!(:first_name => 'foo')
    assert_equal impatient_people + 1, posts(:thinking).impatient_people.count
  end

608
  def test_associate_with_create_exclamation_and_no_options
S
Steven Soroka 已提交
609
    peeps = posts(:thinking).people.count
610
    posts(:thinking).people.create!(:first_name => 'foo')
S
Steven Soroka 已提交
611 612 613
    assert_equal peeps + 1, posts(:thinking).people.count
  end

614 615 616 617 618 619 620
  def test_create_on_new_record
    p = Post.new

    assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") }
    assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") }
  end

621
  def test_associate_with_create_and_invalid_options
622 623
    firm = companies(:first_firm)
    assert_no_difference('firm.developers.count') { assert_nothing_raised { firm.developers.create(:name => '0') } }
624 625 626
  end

  def test_associate_with_create_and_valid_options
627 628
    firm = companies(:first_firm)
    assert_difference('firm.developers.count', 1) { firm.developers.create(:name => 'developer') }
629 630 631
  end

  def test_associate_with_create_bang_and_invalid_options
632 633
    firm = companies(:first_firm)
    assert_no_difference('firm.developers.count') { assert_raises(ActiveRecord::RecordInvalid) { firm.developers.create!(:name => '0') } }
634 635 636
  end

  def test_associate_with_create_bang_and_valid_options
637 638
    firm = companies(:first_firm)
    assert_difference('firm.developers.count', 1) { firm.developers.create!(:name => 'developer') }
639 640
  end

P
Pratik Naik 已提交
641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658
  def test_push_with_invalid_record
    firm = companies(:first_firm)
    assert_raises(ActiveRecord::RecordInvalid) { firm.developers << Developer.new(:name => '0') }
  end

  def test_push_with_invalid_join_record
    repair_validations(Contract) do
      Contract.validate {|r| r.errors[:base] << 'Invalid Contract' }

      firm = companies(:first_firm)
      lifo = Developer.new(:name => 'lifo')
      assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }

      lifo = Developer.create!(:name => 'lifo')
      assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
    end
  end

659 660
  def test_clear_associations
    assert_queries(2) { posts(:welcome);posts(:welcome).people(true) }
661

662 663 664
    assert_queries(1) do
      posts(:welcome).people.clear
    end
665

666 667 668
    assert_queries(0) do
      assert posts(:welcome).people.empty?
    end
669

670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706
    assert posts(:welcome).reload.people(true).empty?
  end

  def test_association_callback_ordering
    Post.reset_log
    log = Post.log
    post = posts(:thinking)

    post.people_with_callbacks << people(:michael)
    assert_equal [
      [:added, :before, "Michael"],
      [:added, :after, "Michael"]
    ], log.last(2)

    post.people_with_callbacks.push(people(:david), Person.create!(:first_name => "Bob"), Person.new(:first_name => "Lary"))
    assert_equal [
      [:added, :before, "David"],
      [:added, :after, "David"],
      [:added, :before, "Bob"],
      [:added, :after, "Bob"],
      [:added, :before, "Lary"],
      [:added, :after, "Lary"]
    ],log.last(6)

    post.people_with_callbacks.build(:first_name => "Ted")
    assert_equal [
      [:added, :before, "Ted"],
      [:added, :after, "Ted"]
    ], log.last(2)

    post.people_with_callbacks.create(:first_name => "Sam")
    assert_equal [
      [:added, :before, "Sam"],
      [:added, :after, "Sam"]
    ], log.last(2)

    post.people_with_callbacks = [people(:michael),people(:david), Person.new(:first_name => "Julian"), Person.create!(:first_name => "Roger")]
707
    assert_equal((%w(Ted Bob Sam Lary) * 2).sort, log[-12..-5].collect(&:last).sort)
708 709 710 711 712 713 714
    assert_equal [
      [:added, :before, "Julian"],
      [:added, :after, "Julian"],
      [:added, :before, "Roger"],
      [:added, :after, "Roger"]
    ], log.last(4)
  end
715 716 717 718

  def test_dynamic_find_should_respect_association_include
    # SQL error in sort clause if :include is not included
    # due to Unknown column 'comments.id'
719
    assert Person.find(1).posts_with_comments_sorted_by_comment_id.find_by_title('Welcome to the weblog')
720
  end
721 722

  def test_count_with_include_should_alias_join_table
J
Jon Leighton 已提交
723
    assert_equal 2, people(:michael).posts.includes(:readers).count
724
  end
725

726 727 728 729
  def test_inner_join_with_quoted_table_name
    assert_equal 2, people(:michael).jobs.size
  end

730 731
  def test_get_ids
    assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
732 733
  end

S
Sergey Nartimov 已提交
734 735 736 737 738 739
  def test_get_ids_for_has_many_through_with_conditions_should_not_preload
    Tagging.create!(:taggable_type => 'Post', :taggable_id => posts(:welcome).id, :tag => tags(:misc))
    ActiveRecord::Associations::Preloader.expects(:new).never
    posts(:welcome).misc_tag_ids
  end

740 741 742 743 744 745 746 747 748 749 750 751
  def test_get_ids_for_loaded_associations
    person = people(:michael)
    person.posts(true)
    assert_queries(0) do
      person.post_ids
      person.post_ids
    end
  end

  def test_get_ids_for_unloaded_associations_does_not_load_them
    person = people(:michael)
    assert !person.posts.loaded?
752
    assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort
753 754
    assert !person.posts.loaded?
  end
755

756 757
  def test_association_proxy_transaction_method_starts_transaction_in_association_class
    Tag.expects(:transaction)
758
    Post.first.tags.transaction do
759
      # nothing
760 761
    end
  end
762 763 764 765 766 767 768 769 770 771 772 773 774 775

  def test_has_many_association_through_a_belongs_to_association_where_the_association_doesnt_exist
    post = Post.create!(:title => "TITLE", :body => "BODY")
    assert_equal [], post.author_favorites
  end

  def test_has_many_association_through_a_belongs_to_association
    author = authors(:mary)
    post = Post.create!(:author => author, :title => "TITLE", :body => "BODY")
    author.author_favorites.create(:favorite_author_id => 1)
    author.author_favorites.create(:favorite_author_id => 2)
    author.author_favorites.create(:favorite_author_id => 3)
    assert_equal post.author.author_favorites, post.author_favorites
  end
776

777 778 779 780 781
  def test_merge_join_association_with_has_many_through_association_proxy
    author = authors(:mary)
    assert_nothing_raised { author.comments.ratings.to_sql }
  end

782
  def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
783
    assert_equal 2, owners(:blackbeard).toys.count
784
  end
785 786 787 788 789

  def test_find_on_has_many_association_collection_with_include_and_conditions
    post_with_no_comments = people(:michael).posts_with_no_comments.first
    assert_equal post_with_no_comments, posts(:authorless)
  end
790 791 792 793 794 795 796 797 798 799 800 801

  def test_has_many_through_has_one_reflection
    assert_equal [comments(:eager_sti_on_associations_vs_comment)], authors(:david).very_special_comments
  end

  def test_modifying_has_many_through_has_one_reflection_should_raise
    [
      lambda { authors(:david).very_special_comments = [VerySpecialComment.create!(:body => "Gorp!", :post_id => 1011), VerySpecialComment.create!(:body => "Eep!", :post_id => 1012)] },
      lambda { authors(:david).very_special_comments << VerySpecialComment.create!(:body => "Hoohah!", :post_id => 1013) },
      lambda { authors(:david).very_special_comments.delete(authors(:david).very_special_comments.first) },
    ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
  end
802

803 804 805 806
  def test_has_many_association_through_a_has_many_association_to_self
    sarah = Person.create!(:first_name => 'Sarah', :primary_contact_id => people(:susan).id, :gender => 'F', :number1_fan_id => 1)
    john = Person.create!(:first_name => 'John', :primary_contact_id => sarah.id, :gender => 'M', :number1_fan_id => 1)
    assert_equal sarah.agents, [john]
807
    assert_equal people(:susan).agents.flat_map(&:agents), people(:susan).agents_of_agents
808 809
  end

810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
  def test_associate_existing_with_nonstandard_primary_key_on_belongs_to
    Categorization.create(:author => authors(:mary), :named_category_name => categories(:general).name)
    assert_equal categories(:general), authors(:mary).named_categories.first
  end

  def test_collection_build_with_nonstandard_primary_key_on_belongs_to
    author   = authors(:mary)
    category = author.named_categories.build(:name => "Primary")
    author.save
    assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name)
    assert author.named_categories(true).include?(category)
  end

  def test_collection_create_with_nonstandard_primary_key_on_belongs_to
    author   = authors(:mary)
    category = author.named_categories.create(:name => "Primary")
    assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name)
    assert author.named_categories(true).include?(category)
  end

830 831 832 833 834 835 836
  def test_collection_exists
    author   = authors(:mary)
    category = Category.create!(author_ids: [author.id], name: "Primary")
    assert category.authors.exists?(id: author.id)
    assert category.reload.authors.exists?(id: author.id)
  end

837 838 839 840 841 842 843 844
  def test_collection_delete_with_nonstandard_primary_key_on_belongs_to
    author   = authors(:mary)
    category = author.named_categories.create(:name => "Primary")
    author.named_categories.delete(category)
    assert !Categorization.exists?(:author_id => author.id, :named_category_name => category.name)
    assert author.named_categories(true).empty?
  end

845 846 847 848 849 850 851 852
  def test_collection_singular_ids_getter_with_string_primary_keys
    book = books(:awdr)
    assert_equal 2, book.subscriber_ids.size
    assert_equal [subscribers(:first).nick, subscribers(:second).nick].sort, book.subscriber_ids.sort
  end

  def test_collection_singular_ids_setter
    company = companies(:rails_core)
853
    dev = Developer.first
854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872

    company.developer_ids = [dev.id]
    assert_equal [dev], company.developers
  end

  def test_collection_singular_ids_setter_with_string_primary_keys
    assert_nothing_raised do
      book = books(:awdr)
      book.subscriber_ids = [subscribers(:second).nick]
      assert_equal [subscribers(:second)], book.subscribers(true)

      book.subscriber_ids = []
      assert_equal [], book.subscribers(true)
    end

  end

  def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set
    company = companies(:rails_core)
873
    ids =  [Developer.first.id, -9999]
874 875 876
    assert_raises(ActiveRecord::RecordNotFound) {company.developer_ids= ids}
  end

877 878 879 880 881 882 883 884 885 886 887 888 889 890
  def test_build_a_model_from_hm_through_association_with_where_clause
    assert_nothing_raised { books(:awdr).subscribers.where(:nick => "marklazz").build }
  end

  def test_attributes_are_being_set_when_initialized_from_hm_through_association_with_where_clause
    new_subscriber = books(:awdr).subscribers.where(:nick => "marklazz").build
    assert_equal new_subscriber.nick, "marklazz"
  end

  def test_attributes_are_being_set_when_initialized_from_hm_through_association_with_multiple_where_clauses
    new_subscriber = books(:awdr).subscribers.where(:nick => "marklazz").where(:name => 'Marcelo Giorgi').build
    assert_equal new_subscriber.nick, "marklazz"
    assert_equal new_subscriber.name, "Marcelo Giorgi"
  end
891 892 893 894 895 896 897 898 899 900 901 902 903 904

  def test_include_method_in_association_through_should_return_true_for_instance_added_with_build
    person = Person.new
    reference = person.references.build
    job = reference.build_job
    assert person.jobs.include?(job)
  end

  def test_include_method_in_association_through_should_return_true_for_instance_added_with_nested_builds
    author = Author.new
    post = author.posts.build
    comment = post.comments.build
    assert author.comments.include?(comment)
  end
905

906 907
  def test_through_association_readonly_should_be_false
    assert !people(:michael).posts.first.readonly?
908
    assert !people(:michael).posts.to_a.first.readonly?
909 910 911 912
  end

  def test_can_update_through_association
    assert_nothing_raised do
913
      people(:michael).posts.first.update!(title: "Can write")
914 915 916
    end
  end

917
  def test_has_many_through_polymorphic_with_primary_key_option
918
    assert_equal [categories(:general)], authors(:david).essay_categories
J
Jon Leighton 已提交
919

920 921
    authors = Author.joins(:essay_categories).where('categories.id' => categories(:general).id)
    assert_equal authors(:david), authors.first
J
Jon Leighton 已提交
922

923
    assert_equal [owners(:blackbeard)], authors(:david).essay_owners
J
Jon Leighton 已提交
924

925 926
    authors = Author.joins(:essay_owners).where("owners.name = 'blackbeard'")
    assert_equal authors(:david), authors.first
927
  end
J
Jon Leighton 已提交
928

929
  def test_has_many_through_with_primary_key_option
930
    assert_equal [categories(:general)], authors(:david).essay_categories_2
J
Jon Leighton 已提交
931

932 933 934
    authors = Author.joins(:essay_categories_2).where('categories.id' => categories(:general).id)
    assert_equal authors(:david), authors.first
  end
935

936 937 938 939 940 941
  def test_size_of_through_association_should_increase_correctly_when_has_many_association_is_added
    post = posts(:thinking)
    readers = post.readers.size
    post.people << people(:michael)
    assert_equal readers + 1, post.readers.size
  end
942 943

  def test_has_many_through_with_default_scope_on_join_model
944
    assert_equal posts(:welcome).comments.order('id').to_a, authors(:david).comments_on_first_posts
945
  end
946 947 948 949 950

  def test_create_has_many_through_with_default_scope_on_join_model
    category = authors(:david).special_categories.create(:name => "Foo")
    assert_equal 1, category.categorizations.where(:special => true).count
  end
951 952 953 954 955 956

  def test_joining_has_many_through_with_uniq
    mary = Author.joins(:unique_categorized_posts).where(:id => authors(:mary).id).first
    assert_equal 1, mary.unique_categorized_posts.length
    assert_equal 1, mary.unique_categorized_post_ids.length
  end
957 958

  def test_joining_has_many_through_belongs_to
959
    posts = Post.joins(:author_categorizations).order('posts.id').
960 961
                 where('categorizations.id' => categorizations(:mary_thinking_sti).id)

962
    assert_equal [posts(:eager_other), posts(:misc_by_mary), posts(:other_by_mary)], posts
963
  end
964 965 966

  def test_select_chosen_fields_only
    author = authors(:david)
967
    assert_equal ['body', 'id'].sort, author.comments.select('comments.body').first.attributes.keys.sort
968
  end
969 970 971 972

  def test_get_has_many_through_belongs_to_ids_with_conditions
    assert_equal [categories(:general).id], authors(:mary).categories_like_general_ids
  end
973

974 975 976 977 978
  def test_get_collection_singular_ids_on_has_many_through_with_conditions_and_include
    person = Person.first
    assert_equal person.posts_with_no_comment_ids, person.posts_with_no_comments.map(&:id)
  end

979 980 981 982
  def test_count_has_many_through_with_named_scope
    assert_equal 2, authors(:mary).categories.count
    assert_equal 1, authors(:mary).categories.general.count
  end
983 984 985 986 987 988 989 990 991 992 993 994 995 996 997

  def test_has_many_through_belongs_to_should_update_when_the_through_foreign_key_changes
    post = posts(:eager_other)

    post.author_categorizations
    proxy = post.send(:association_instance_get, :author_categorizations)

    assert !proxy.stale_target?
    assert_equal authors(:mary).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id)

    post.author_id = authors(:david).id

    assert proxy.stale_target?
    assert_equal authors(:david).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id)
  end
998 999 1000 1001 1002 1003 1004

  def test_create_with_conditions_hash_on_through_association
    member = members(:groucho)
    club   = member.clubs.create!

    assert_equal true, club.reload.membership.favourite
  end
1005 1006 1007 1008 1009 1010 1011 1012 1013

  def test_deleting_from_has_many_through_a_belongs_to_should_not_try_to_update_counter
    post    = posts(:welcome)
    address = author_addresses(:david_address)

    assert post.author_addresses.include?(address)
    post.author_addresses.delete(address)
    assert post[:author_count].nil?
  end
1014

1015 1016 1017
  def test_primary_key_option_on_source
    post     = posts(:welcome)
    category = categories(:general)
1018
    Categorization.create!(:post_id => post.id, :named_category_name => category.name)
1019 1020 1021 1022 1023

    assert_equal [category], post.named_categories
    assert_equal [category.name], post.named_category_ids # checks when target loaded
    assert_equal [category.name], post.reload.named_category_ids # checks when target no loaded
  end
1024 1025 1026 1027

  def test_create_should_not_raise_exception_when_join_record_has_errors
    repair_validations(Categorization) do
      Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
1028
      Category.create(:name => 'Fishing', :authors => [Author.first])
1029 1030
    end
  end
1031

1032 1033 1034 1035 1036 1037 1038 1039
  def test_save_should_not_raise_exception_when_join_record_has_errors
    repair_validations(Categorization) do
      Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
      c = Category.create(:name => 'Fishing', :authors => [Author.first])
      c.save
    end
  end

1040 1041 1042 1043 1044
  def test_assign_array_to_new_record_builds_join_records
    c = Category.new(:name => 'Fishing', :authors => [Author.first])
    assert_equal 1, c.categorizations.size
  end

1045 1046 1047 1048 1049 1050 1051 1052
  def test_create_bang_should_raise_exception_when_join_record_has_errors
    repair_validations(Categorization) do
      Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
      assert_raises(ActiveRecord::RecordInvalid) do
        Category.create!(:name => 'Fishing', :authors => [Author.first])
      end
    end
  end
1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070

  def test_save_bang_should_raise_exception_when_join_record_has_errors
    repair_validations(Categorization) do
      Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
      c = Category.new(:name => 'Fishing', :authors => [Author.first])
      assert_raises(ActiveRecord::RecordInvalid) do
        c.save!
      end
    end
  end

  def test_create_bang_returns_falsy_when_join_record_has_errors
    repair_validations(Categorization) do
      Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' }
      c = Category.new(:name => 'Fishing', :authors => [Author.first])
      assert !c.save
    end
  end
1071 1072 1073

  def test_preloading_empty_through_association_via_joins
    person = Person.create!(:first_name => "Gaga")
1074
    person = Person.where(:id => person.id).where('readers.id = 1 or 1=1').references(:readers).includes(:posts).to_a.first
1075 1076 1077 1078

    assert person.posts.loaded?, 'person.posts should be loaded'
    assert_equal [], person.posts
  end
1079 1080 1081 1082

  def test_explicitly_joining_join_table
    assert_equal owners(:blackbeard).toys, owners(:blackbeard).toys.with_pet
  end
J
Jon Leighton 已提交
1083 1084 1085 1086 1087

  def test_has_many_through_with_polymorphic_source
    post = tags(:general).tagged_posts.create! :title => "foo", :body => "bar"
    assert_equal [tags(:general)], post.reload.tags
  end
V
Vipul A M 已提交
1088

1089 1090 1091 1092 1093 1094
  def test_has_many_through_obeys_order_on_through_association
    owner = owners(:blackbeard)
    assert owner.toys.to_sql.include?("pets.name desc")
    assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name }
  end

1095
  def test_has_many_through_associations_on_new_records_use_null_relations
V
Vipul A M 已提交
1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106
    person = Person.new

    assert_no_queries do
      assert_equal [], person.posts
      assert_equal [], person.posts.where(body: 'omg')
      assert_equal [], person.posts.pluck(:body)
      assert_equal 0,  person.posts.sum(:tags_count)
      assert_equal 0,  person.posts.count
    end
  end

1107
  def test_has_many_through_with_default_scope_on_the_target
1108 1109 1110 1111 1112 1113
    person = people(:michael)
    assert_equal [posts(:thinking)], person.first_posts

    readers(:michael_authorless).update(first_post_id: 1)
    assert_equal [posts(:thinking)], person.reload.first_posts
  end
1114

1115
  def test_has_many_through_with_includes_in_through_association_scope
1116
    assert_not_empty posts(:welcome).author_address_extra_with_address
1117
  end
1118

1119
  def test_insert_records_via_has_many_through_association_with_scope
1120 1121 1122 1123 1124 1125 1126 1127 1128
    club = Club.create!
    member = Member.create!
    Membership.create!(club: club, member: member)

    club.favourites << member
    assert_equal [member], club.favourites

    club.reload
    assert_equal [member], club.favourites
R
Rafael Mendonça França 已提交
1129
  end
1130

1131 1132 1133 1134
  def test_has_many_through_unscope_default_scope
    post = Post.create!(:title => 'Beaches', :body => "I like beaches!")
    Reader.create! :person => people(:david), :post => post
    LazyReader.create! :person => people(:susan), :post => post
1135

1136 1137
    assert_equal 2, post.people.to_a.size
    assert_equal 1, post.lazy_people.to_a.size
1138

1139 1140
    assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size
    assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size
1141
  end
S
Sean Griffin 已提交
1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160

  class ClubWithCallbacks < ActiveRecord::Base
    self.table_name = 'clubs'
    after_create :add_a_member

    has_many :memberships, inverse_of: :club, foreign_key: :club_id
    has_many :members, through: :memberships

    def add_a_member
      members << Member.last
    end
  end

  def test_has_many_with_callback_before_association
    Member.create!
    club = ClubWithCallbacks.create!

    assert_equal 1, club.reload.memberships.count
  end
1161
end