named_scoping_test.rb 17.2 KB
Newer Older
1
require "cases/helper"
2 3 4 5 6 7 8
require "models/post"
require "models/topic"
require "models/comment"
require "models/reply"
require "models/author"
require "models/developer"
require "models/computer"
9

10
class NamedScopingTest < ActiveRecord::TestCase
11
  fixtures :posts, :authors, :topics, :comments, :author_addresses
12

13
  def test_implements_enumerable
J
Jon Leighton 已提交
14
    assert !Topic.all.empty?
15

J
Jon Leighton 已提交
16 17 18 19
    assert_equal Topic.all.to_a, Topic.base
    assert_equal Topic.all.to_a, Topic.base.to_a
    assert_equal Topic.first,    Topic.base.first
    assert_equal Topic.all.to_a, Topic.base.map { |i| i }
20
  end
21

22
  def test_found_items_are_cached
23
    all_posts = Topic.base
24

25 26 27 28 29
    assert_queries(1) do
      all_posts.collect
      all_posts.collect
    end
  end
30

31
  def test_reload_expires_cache_of_found_items
32
    all_posts = Topic.base
33
    all_posts.to_a
34

35
    new_post = Topic.create!
36 37
    assert_not_includes all_posts, new_post
    assert_includes all_posts.reload, new_post
38
  end
39

40
  def test_delegates_finds_and_calculations_to_the_base_class
J
Jon Leighton 已提交
41
    assert !Topic.all.empty?
42

J
Jon Leighton 已提交
43 44 45
    assert_equal Topic.all.to_a,                Topic.base.to_a
    assert_equal Topic.first,                   Topic.base.first
    assert_equal Topic.count,                   Topic.base.count
46
    assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count)
47
  end
48

49 50 51 52 53 54 55
  def test_calling_merge_at_first_in_scope
    Topic.class_eval do
      scope :calling_merge_at_first_in_scope, Proc.new { merge(Topic.replied) }
    end
    assert_equal Topic.calling_merge_at_first_in_scope.to_a, Topic.replied.to_a
  end

56 57 58
  def test_method_missing_priority_when_delegating
    klazz = Class.new(ActiveRecord::Base) do
      self.table_name = "topics"
59 60
      scope :since, Proc.new { where("written_on >= ?", Time.now - 1.day) }
      scope :to,    Proc.new { where("written_on <= ?", Time.now) }
61
    end
62
    assert_equal klazz.to.since.to_a, klazz.since.to.to_a
63 64
  end

65
  def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
66
    assert Topic.approved.respond_to?(:limit)
67 68 69 70
    assert Topic.approved.respond_to?(:count)
    assert Topic.approved.respond_to?(:length)
  end

71
  def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
72
    assert !Topic.all.merge!(where: { approved: true }).to_a.empty?
73

74
    assert_equal Topic.all.merge!(where: { approved: true }).to_a, Topic.approved
75
    assert_equal Topic.where(approved: true).count, Topic.approved.count
76
  end
77

78 79 80 81 82 83
  def test_scopes_with_string_name_can_be_composed
    # NOTE that scopes defined with a string as a name worked on their own
    # but when called on another scope the other scope was completely replaced
    assert_equal Topic.replied.approved, Topic.replied.approved_as_string
  end

84
  def test_scopes_are_composable
85
    assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved)
86
    assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied)
87
    assert !(approved == replied)
88
    assert !(approved & replied).empty?
89

90 91 92 93
    assert_equal approved & replied, Topic.approved.replied
  end

  def test_procedural_scopes
94 95
    topics_written_before_the_third = Topic.where("written_on < ?", topics(:third).written_on)
    topics_written_before_the_second = Topic.where("written_on < ?", topics(:second).written_on)
96
    assert_not_equal topics_written_before_the_second, topics_written_before_the_third
97

98 99 100
    assert_equal topics_written_before_the_third, Topic.written_before(topics(:third).written_on)
    assert_equal topics_written_before_the_second, Topic.written_before(topics(:second).written_on)
  end
101

102
  def test_procedural_scopes_returning_nil
J
Jon Leighton 已提交
103
    all_topics = Topic.all
104 105 106

    assert_equal all_topics, Topic.written_before(nil)
  end
107

108 109 110
  def test_scope_with_object
    objects = Topic.with_object
    assert_operator objects.length, :>, 0
111
    assert objects.all?(&:approved?), "all objects should be approved"
112 113
  end

114
  def test_has_many_associations_have_access_to_scopes
115 116
    assert_not_equal Post.containing_the_letter_a, authors(:david).posts
    assert !Post.containing_the_letter_a.empty?
117

118 119
    assert_equal authors(:david).posts & Post.containing_the_letter_a, authors(:david).posts.containing_the_letter_a
  end
120

121
  def test_scope_with_STI
122 123 124 125
    assert_equal 3,Post.containing_the_letter_a.count
    assert_equal 1,SpecialPost.containing_the_letter_a.count
  end

126
  def test_has_many_through_associations_have_access_to_scopes
127
    assert_not_equal Comment.containing_the_letter_e, authors(:david).comments
128
    assert !Comment.containing_the_letter_e.empty?
129

130 131
    assert_equal authors(:david).comments & Comment.containing_the_letter_e, authors(:david).comments.containing_the_letter_e
  end
132

133
  def test_scopes_honor_current_scopes_from_when_defined
134 135 136
    assert !Post.ranked_by_comments.limit_by(5).empty?
    assert !authors(:david).posts.ranked_by_comments.limit_by(5).empty?
    assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5)
137
    assert_not_equal Post.top(5), authors(:david).posts.top(5)
138
    # Oracle sometimes sorts differently if WHERE condition is changed
139
    assert_equal authors(:david).posts.ranked_by_comments.limit_by(5).to_a.sort_by(&:id), authors(:david).posts.top(5).to_a.sort_by(&:id)
140
    assert_equal Post.ranked_by_comments.limit_by(5), Post.top(5)
141 142
  end

143
  def test_scopes_body_is_a_callable
144 145
    e = assert_raises ArgumentError do
      Class.new(Post).class_eval { scope :containing_the_letter_z, where("body LIKE '%z%'") }
146
    end
147
    assert_equal "The scope body needs to be callable.", e.message
148 149
  end

150
  def test_active_records_have_scope_named__all__
J
Jon Leighton 已提交
151
    assert !Topic.all.empty?
152

J
Jon Leighton 已提交
153
    assert_equal Topic.all.to_a, Topic.base
154
  end
155

156
  def test_active_records_have_scope_named__scoped__
J
Jon Leighton 已提交
157 158
    scope = Topic.where("content LIKE '%Have%'")
    assert !scope.empty?
159

160
    assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'")
161
  end
162

163
  def test_first_and_last_should_allow_integers_for_limit
164
    assert_equal Topic.base.first(2), Topic.base.to_a.first(2)
165
    assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2)
166 167 168 169 170 171 172 173 174 175 176
  end

  def test_first_and_last_should_not_use_query_when_results_are_loaded
    topics = Topic.base
    topics.reload # force load
    assert_no_queries do
      topics.first
      topics.last
    end
  end

177 178 179 180 181 182 183 184
  def test_empty_should_not_load_results
    topics = Topic.base
    assert_queries(2) do
      topics.empty?  # use count query
      topics.collect # force load
      topics.empty?  # use loaded (no query)
    end
  end
185

186 187
  def test_any_should_not_load_results
    topics = Topic.base
188 189 190 191
    assert_queries(2) do
      topics.any?    # use count query
      topics.collect # force load
      topics.any?    # use loaded (no query)
192 193 194 195 196 197
    end
  end

  def test_any_should_call_proxy_found_if_using_a_block
    topics = Topic.base
    assert_queries(1) do
198 199 200
      assert_not_called(topics, :empty?) do
        topics.any? { true }
      end
201 202 203
    end
  end

204
  def test_any_should_not_fire_query_if_scope_loaded
205 206 207 208 209
    topics = Topic.base
    topics.collect # force load
    assert_no_queries { assert topics.any? }
  end

210 211 212 213 214 215
  def test_model_class_should_respond_to_any
    assert Topic.any?
    Topic.delete_all
    assert !Topic.any?
  end

216 217 218 219 220 221 222 223 224 225 226 227
  def test_many_should_not_load_results
    topics = Topic.base
    assert_queries(2) do
      topics.many?   # use count query
      topics.collect # force load
      topics.many?   # use loaded (no query)
    end
  end

  def test_many_should_call_proxy_found_if_using_a_block
    topics = Topic.base
    assert_queries(1) do
228 229 230
      assert_not_called(topics, :size) do
        topics.many? { true }
      end
231 232 233
    end
  end

234
  def test_many_should_not_fire_query_if_scope_loaded
235 236 237 238 239 240
    topics = Topic.base
    topics.collect # force load
    assert_no_queries { assert topics.many? }
  end

  def test_many_should_return_false_if_none_or_one
241
    topics = Topic.base.where(id: 0)
242
    assert !topics.many?
243
    topics = Topic.base.where(id: 1)
244 245 246 247 248 249 250
    assert !topics.many?
  end

  def test_many_should_return_true_if_more_than_one
    assert Topic.base.many?
  end

251 252 253 254 255 256 257 258 259
  def test_model_class_should_respond_to_many
    Topic.delete_all
    assert !Topic.many?
    Topic.create!
    assert !Topic.many?
    Topic.create!
    assert Topic.many?
  end

260
  def test_should_build_on_top_of_scope
261 262 263 264
    topic = Topic.approved.build({})
    assert topic.approved
  end

265
  def test_should_build_new_on_top_of_scope
266 267 268 269
    topic = Topic.approved.new
    assert topic.approved
  end

270
  def test_should_create_on_top_of_scope
271 272 273 274
    topic = Topic.approved.create({})
    assert topic.approved
  end

275
  def test_should_create_with_bang_on_top_of_scope
276 277 278
    topic = Topic.approved.create!({})
    assert topic.approved
  end
279

280
  def test_should_build_on_top_of_chained_scopes
281 282
    topic = Topic.approved.by_lifo.build({})
    assert topic.approved
283
    assert_equal "lifo", topic.author_name
284
  end
285

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
  def test_reserved_scope_names
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "topics"

      scope :approved, -> { where(approved: true) }

      class << self
        public
          def pub; end

        private
          def pri; end

        protected
          def pro; end
      end
    end

    subklass = Class.new(klass)

    conflicts = [
      :create,        # public class method on AR::Base
      :relation,      # private class method on AR::Base
      :new,           # redefined class method on AR::Base
      :all,           # a default scope
J
Joe Rafaniello 已提交
311
      :public,        # some important methods on Module and Class
312
      :protected,
313 314 315 316
      :private,
      :name,
      :parent,
      :superclass
317 318 319 320 321 322 323 324 325 326 327 328
    ]

    non_conflicts = [
      :find_by_title, # dynamic finder method
      :approved,      # existing scope
      :pub,           # existing public class method
      :pri,           # existing private class method
      :pro,           # existing protected class method
      :open,          # a ::Kernel method
    ]

    conflicts.each do |name|
F
Franky W 已提交
329
      e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
330
        klass.class_eval { scope name, -> { where(approved: true) } }
331
      end
F
Franky W 已提交
332
      assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
333

F
Franky W 已提交
334
      e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
335
        subklass.class_eval { scope name, -> { where(approved: true) } }
336
      end
F
Franky W 已提交
337
      assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
338 339 340 341
    end

    non_conflicts.each do |name|
      assert_nothing_raised do
342
        silence_warnings do
343
          klass.class_eval { scope name, -> { where(approved: true) } }
344
        end
345 346 347
      end

      assert_nothing_raised do
348
        subklass.class_eval { scope name, -> { where(approved: true) } }
349 350 351 352
      end
    end
  end

353 354 355 356 357 358 359
  # Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/
  # has been done by evaluating a string with a plain def statement. For scope
  # names which contain spaces this approach doesn't work.
  def test_spaces_in_scope_names
    klass = Class.new(ActiveRecord::Base) do
      self.table_name = "topics"
      scope :"title containing space", -> { where("title LIKE '% %'") }
360
      scope :approved, -> { where(approved: true) }
361 362 363 364 365
    end
    assert_equal klass.send(:"title containing space"), klass.where("title LIKE '% %'")
    assert_equal klass.approved.send(:"title containing space"), klass.approved.where("title LIKE '% %'")
  end

366
  def test_find_all_should_behave_like_select
367
    assert_equal Topic.base.to_a.select(&:approved), Topic.base.to_a.find_all(&:approved)
368
  end
369

370
  def test_rand_should_select_a_random_object_from_proxy
371
    assert_kind_of Topic, Topic.approved.sample
372 373
  end

374
  def test_should_use_where_in_query_for_scope
375
    assert_equal Developer.where(name: "Jamis").to_set, Developer.where(id: Developer.jamises).to_set
376
  end
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391

  def test_size_should_use_count_when_results_are_not_loaded
    topics = Topic.base
    assert_queries(1) do
      assert_sql(/COUNT/i) { topics.size }
    end
  end

  def test_size_should_use_length_when_results_are_loaded
    topics = Topic.base
    topics.reload # force load
    assert_no_queries do
      topics.size # use loaded (no query)
    end
  end
392

393
  def test_should_not_duplicates_where_values
394 395
    relation = Topic.where("1=1")
    assert_equal relation.where_clause, relation.scope_with_lambda.where_clause
396 397
  end

398 399 400
  def test_chaining_with_duplicate_joins
    join = "INNER JOIN comments ON comments.post_id = posts.id"
    post = Post.find(1)
J
Jon Leighton 已提交
401
    assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size
402
  end
403

404
  def test_chaining_applies_last_conditions_when_creating
405 406 407 408 409
    post = Topic.rejected.new
    assert !post.approved?

    post = Topic.rejected.approved.new
    assert post.approved?
410

411 412 413 414 415
    post = Topic.approved.rejected.new
    assert !post.approved?

    post = Topic.approved.rejected.approved.new
    assert post.approved?
416 417
  end

418
  def test_chaining_combines_conditions_when_searching
419
    # Normal hash conditions
420 421
    assert_equal Topic.where(approved: false).where(approved: true).to_a, Topic.rejected.approved.to_a
    assert_equal Topic.where(approved: true).where(approved: false).to_a, Topic.approved.rejected.to_a
422 423

    # Nested hash conditions with same keys
424
    assert_equal [], Post.with_special_comments.with_very_special_comments.to_a
425 426

    # Nested hash conditions with different keys
427
    assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq
428
  end
429

430
  def test_scopes_batch_finders
431
    assert_equal 4, Topic.approved.count
432

433
    assert_queries(5) do
434
      Topic.approved.find_each(batch_size: 1) { |t| assert t.approved? }
435 436
    end

437
    assert_queries(3) do
438
      Topic.approved.find_in_batches(batch_size: 2) do |group|
439
        group.each { |t| assert t.approved? }
440 441 442
      end
    end
  end
443 444 445

  def test_table_names_for_chaining_scopes_with_and_without_table_name_included
    assert_nothing_raised do
446
      Comment.for_first_post.for_first_author.to_a
447 448
    end
  end
449

450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
  def test_scopes_with_reserved_names
    class << Topic
      def public_method; end
      public :public_method

      def protected_method; end
      protected :protected_method

      def private_method; end
      private :private_method
    end

    [:public_method, :protected_method, :private_method].each do |reserved_method|
      assert Topic.respond_to?(reserved_method, true)
      ActiveRecord::Base.logger.expects(:warn)
465
      silence_warnings { Topic.scope(reserved_method, -> {}) }
466 467 468
    end
  end

469
  def test_scopes_on_relations
470
    # Topic.replied
471
    approved_topics = Topic.all.approved.order("id DESC")
472
    assert_equal topics(:fifth), approved_topics.first
473 474 475 476 477

    replied_approved_topics = approved_topics.replied
    assert_equal topics(:third), replied_approved_topics.first
  end

478
  def test_index_on_scope
479
    approved = Topic.approved.order("id ASC")
480 481 482
    assert_equal topics(:second), approved[0]
    assert approved.loaded?
  end
483

484
  def test_nested_scopes_queries_size
485
    assert_queries(1) do
486
      Topic.approved.by_lifo.replied.written_before(Time.now).to_a
487 488
    end
  end
489

490 491 492
  # Note: these next two are kinda odd because they are essentially just testing that the
  # query cache works as it should, but they are here for legacy reasons as they was previously
  # a separate cache on association proxies, and these show that that is not necessary.
493
  def test_scopes_are_cached_on_associations
494 495
    post = posts(:welcome)

496
    Post.cache do
497 498
      assert_queries(1) { post.comments.containing_the_letter_e.to_a }
      assert_no_queries { post.comments.containing_the_letter_e.to_a }
499
    end
500
  end
501

502
  def test_scopes_with_arguments_are_cached_on_associations
503 504
    post = posts(:welcome)

505
    Post.cache do
506
      one = assert_queries(1) { post.comments.limit_by(1).to_a }
507
      assert_equal 1, one.size
508

509
      two = assert_queries(1) { post.comments.limit_by(2).to_a }
510
      assert_equal 2, two.size
511

512 513
      assert_no_queries { post.comments.limit_by(1).to_a }
      assert_no_queries { post.comments.limit_by(2).to_a }
514
    end
515 516
  end

A
Arun Agrawal 已提交
517 518 519
  def test_scopes_to_get_newest
    post = posts(:welcome)
    old_last_comment = post.comments.newest
520
    new_comment = post.comments.create(body: "My new comment")
A
Arun Agrawal 已提交
521 522 523 524
    assert_equal new_comment, post.comments.newest
    assert_not_equal old_last_comment, post.comments.newest
  end

525
  def test_scopes_are_reset_on_association_reload
526 527 528 529
    post = posts(:welcome)

    [:destroy_all, :reset, :delete_all].each do |method|
      before = post.comments.containing_the_letter_e
530
      post.association(:comments).send(method)
531
      assert before.object_id != post.comments.containing_the_letter_e.object_id, "CollectionAssociation##{method} should reset the named scopes cache"
532 533
    end
  end
534

535
  def test_scoped_are_lazy_loaded_if_table_still_does_not_exist
536 537 538 539
    assert_nothing_raised do
      require "models/without_table"
    end
  end
J
Jon Leighton 已提交
540

541
  def test_eager_default_scope_relations_are_remove
J
Jon Leighton 已提交
542
    klass = Class.new(ActiveRecord::Base)
543
    klass.table_name = "posts"
J
Jon Leighton 已提交
544

545
    assert_raises(ArgumentError) do
546
      klass.send(:default_scope, klass.where(id: posts(:welcome).id))
J
Jon Leighton 已提交
547 548
    end
  end
N
Neeraj Singh 已提交
549 550

  def test_subclass_merges_scopes_properly
551
    assert_equal 1, SpecialComment.where(body: "go crazy").created.count
N
Neeraj Singh 已提交
552 553
  end

554 555 556 557 558 559 560 561 562 563 564 565 566
  def test_model_class_should_respond_to_none
    assert !Topic.none?
    Topic.delete_all
    assert Topic.none?
  end

  def test_model_class_should_respond_to_one
    assert !Topic.one?
    Topic.delete_all
    assert !Topic.one?
    Topic.create!
    assert Topic.one?
  end
567
end