has_many_through_associations_test.rb 15.6 KB
Newer Older
1 2 3
require "cases/helper"
require 'models/post'
require 'models/person'
4 5
require 'models/reference'
require 'models/job'
6
require 'models/reader'
7
require 'models/comment'
8 9 10
require 'models/tag'
require 'models/tagging'
require 'models/author'
11 12 13
require 'models/owner'
require 'models/pet'
require 'models/toy'
14 15 16
require 'models/contract'
require 'models/company'
require 'models/developer'
17 18 19
require 'models/subscriber'
require 'models/book'
require 'models/subscription'
20 21

class HasManyThroughAssociationsTest < ActiveRecord::TestCase
22 23
  fixtures :posts, :readers, :people, :comments, :authors,
           :owners, :pets, :toys, :jobs, :references, :companies,
J
José Valim 已提交
24
           :subscribers, :books, :subscriptions, :developers
25

26 27 28 29 30
  # 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
31

32 33
  def test_associate_existing
    assert_queries(2) { posts(:thinking); people(:david) }
34

35
    assert_queries(1) do
36
      posts(:thinking).people << people(:david)
37
    end
38

39 40 41
    assert_queries(1) do
      assert posts(:thinking).people.include?(people(:david))
    end
42

43 44 45 46 47 48
    assert posts(:thinking).reload.people(true).include?(people(:david))
  end

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

50
    assert_queries(0) do
51
      new_person = Person.new :first_name => 'bob'
52
    end
53

54 55 56 57 58
    # 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
59

60 61 62
    assert_queries(1) do
      assert posts(:thinking).people.include?(new_person)
    end
63

64 65 66 67 68
    assert posts(:thinking).reload.people(true).include?(new_person)
  end

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

70
    assert_queries(0) do
71 72
      posts(:thinking).people.build(:first_name => "Bob")
      posts(:thinking).people.new(:first_name => "Ted")
73
    end
74

75 76 77 78 79
    # 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
80

81 82 83 84
    # 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
85
      posts(:thinking).body += '-changed'
86 87
      posts(:thinking).save
    end
88

89 90 91 92 93 94
    assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Bob")
    assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Ted")
  end

  def test_delete_association
    assert_queries(2){posts(:welcome);people(:michael); }
95

96 97 98
    assert_queries(1) do
      posts(:welcome).people.delete(people(:michael))
    end
99

100 101 102
    assert_queries(1) do
      assert posts(:welcome).people.empty?
    end
103

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

107
  def test_destroy_association
108
    assert_difference ["Person.count", "Reader.count"], -1 do
109 110 111 112 113 114 115 116
      posts(:welcome).people.destroy(people(:michael))
    end

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

  def test_destroy_all
117
    assert_difference ["Person.count", "Reader.count"], -1 do
118 119 120 121 122 123 124
      posts(:welcome).people.destroy_all
    end

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

125 126 127 128 129 130
  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

131 132
  def test_replace_association
    assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
133

134 135 136 137 138
    # 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
139

140 141 142 143
    assert_queries(0){
      assert posts(:welcome).people.include?(people(:david))
      assert !posts(:welcome).people.include?(people(:michael))
    }
144

145 146 147 148
    assert posts(:welcome).reload.people(true).include?(people(:david))
    assert !posts(:welcome).reload.people(true).include?(people(:michael))
  end

149 150 151
  def test_replace_order_is_preserved
    posts(:welcome).people.clear
    posts(:welcome).people = [people(:david), people(:michael)]
152
    assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order('id').map(&:person_id)
153 154 155 156

    # Test the inverse order in case the first success was a coincidence
    posts(:welcome).people.clear
    posts(:welcome).people = [people(:michael), people(:david)]
157
    assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order('id').map(&:person_id)
158 159 160 161 162
  end

  def test_replace_by_id_order_is_preserved
    posts(:welcome).people.clear
    posts(:welcome).person_ids = [people(:david).id, people(:michael).id]
163
    assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order('id').map(&:person_id)
164 165 166 167

    # 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]
168
    assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order('id').map(&:person_id)
169 170
  end

171 172
  def test_associate_with_create
    assert_queries(1) { posts(:thinking) }
173

174 175 176 177 178
    # 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
179

180 181 182 183
    # *Now* we actually need the collection so it's loaded
    assert_queries(1) do
      assert posts(:thinking).people.collect(&:first_name).include?("Jeb")
    end
184

185 186 187
    assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb")
  end

S
Steven Soroka 已提交
188 189
  def test_associate_with_create_and_no_options
    peeps = posts(:thinking).people.count
190
    posts(:thinking).people.create(:first_name => 'foo')
S
Steven Soroka 已提交
191 192 193
    assert_equal peeps + 1, posts(:thinking).people.count
  end

194 195 196 197 198 199
  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

200
  def test_associate_with_create_exclamation_and_no_options
S
Steven Soroka 已提交
201
    peeps = posts(:thinking).people.count
202
    posts(:thinking).people.create!(:first_name => 'foo')
S
Steven Soroka 已提交
203 204 205
    assert_equal peeps + 1, posts(:thinking).people.count
  end

206 207 208 209 210 211 212
  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

213
  def test_associate_with_create_and_invalid_options
214 215
    firm = companies(:first_firm)
    assert_no_difference('firm.developers.count') { assert_nothing_raised { firm.developers.create(:name => '0') } }
216 217 218
  end

  def test_associate_with_create_and_valid_options
219 220
    firm = companies(:first_firm)
    assert_difference('firm.developers.count', 1) { firm.developers.create(:name => 'developer') }
221 222 223
  end

  def test_associate_with_create_bang_and_invalid_options
224 225
    firm = companies(:first_firm)
    assert_no_difference('firm.developers.count') { assert_raises(ActiveRecord::RecordInvalid) { firm.developers.create!(:name => '0') } }
226 227 228
  end

  def test_associate_with_create_bang_and_valid_options
229 230
    firm = companies(:first_firm)
    assert_difference('firm.developers.count', 1) { firm.developers.create!(:name => 'developer') }
231 232
  end

P
Pratik Naik 已提交
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
  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

251 252
  def test_clear_associations
    assert_queries(2) { posts(:welcome);posts(:welcome).people(true) }
253

254 255 256
    assert_queries(1) do
      posts(:welcome).people.clear
    end
257

258 259 260
    assert_queries(0) do
      assert posts(:welcome).people.empty?
    end
261

262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
    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")]
299
    assert_equal((%w(Ted Bob Sam Lary) * 2).sort, log[-12..-5].collect(&:last).sort)
300 301 302 303 304 305 306 307
    assert_equal [
      [:added, :before, "Julian"],
      [:added, :after, "Julian"],
      [:added, :before, "Roger"],
      [:added, :after, "Roger"]
    ], log.last(4)

    post.people_with_callbacks.clear
308
    assert_equal((%w(Michael David Julian Roger) * 2).sort, log.last(8).collect(&:last).sort)
309
  end
310 311 312 313 314 315

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

  def test_count_with_include_should_alias_join_table
    assert_equal 2, people(:michael).posts.count(:include => :readers)
  end
320

321 322 323 324
  def test_inner_join_with_quoted_table_name
    assert_equal 2, people(:michael).jobs.size
  end

325 326 327 328 329 330
  def test_get_ids_for_belongs_to_source
    assert_sql(/DISTINCT/) { assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort }
  end

  def test_get_ids_for_has_many_source
    assert_equal [comments(:eager_other_comment1).id], authors(:mary).comment_ids
331 332 333 334 335 336 337 338 339 340 341 342 343 344
  end

  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?
345
    assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort
346 347
    assert !person.posts.loaded?
  end
348

349 350 351 352
  def test_association_proxy_transaction_method_starts_transaction_in_association_class
    Tag.expects(:transaction)
    Post.find(:first).tags.transaction do
      # nothing
353 354
    end
  end
355 356 357 358 359 360 361 362 363 364 365 366 367 368

  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
369 370 371 372

  def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
    assert_equal 1, owners(:blackbeard).toys.count
  end
373 374 375 376 377

  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
378 379 380 381 382 383 384 385 386 387 388 389

  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
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

  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)
    dev = Developer.find(:first)

    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)
    ids =  [Developer.find(:first).id, -9999]
    assert_raises(ActiveRecord::RecordNotFound) {company.developer_ids= ids}
  end

423 424 425 426 427 428 429 430 431 432 433 434 435 436
  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
437 438 439 440 441 442 443 444 445 446 447 448 449 450

  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
451 452 453 454 455 456 457

  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
458
end