named_scoping_test.rb 18.7 KB
Newer Older
1 2
# frozen_string_literal: true

3
require "cases/helper"
4 5 6 7 8 9 10
require "models/post"
require "models/topic"
require "models/comment"
require "models/reply"
require "models/author"
require "models/developer"
require "models/computer"
11

12
class NamedScopingTest < ActiveRecord::TestCase
13
  fixtures :posts, :authors, :topics, :comments, :author_addresses
14

15
  def test_implements_enumerable
16
    assert_not_empty Topic.all
17

J
Jon Leighton 已提交
18 19 20 21
    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 }
22
  end
23

24
  def test_found_items_are_cached
25
    all_posts = Topic.base
26

27
    assert_queries(1) do
28 29
      all_posts.collect { true }
      all_posts.collect { true }
30 31
    end
  end
32

33
  def test_reload_expires_cache_of_found_items
34
    all_posts = Topic.base
35
    all_posts.to_a
36

37
    new_post = Topic.create!
38 39
    assert_not_includes all_posts, new_post
    assert_includes all_posts.reload, new_post
40
  end
41

42
  def test_delegates_finds_and_calculations_to_the_base_class
43
    assert_not_empty Topic.all
44

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

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

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

67
  def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
D
Daniel Colson 已提交
68 69 70
    assert_respond_to Topic.approved, :limit
    assert_respond_to Topic.approved, :count
    assert_respond_to Topic.approved, :length
71 72
  end

73
  def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
74
    assert_not_empty Topic.all.merge!(where: { approved: true }).to_a
75

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

80 81 82 83 84 85
  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

86
  def test_scopes_are_composable
87
    assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved)
88
    assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied)
89
    assert_not (approved == replied)
90
    assert_not_empty (approved & replied)
91

92 93 94 95
    assert_equal approved & replied, Topic.approved.replied
  end

  def test_procedural_scopes
96 97
    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)
98
    assert_not_equal topics_written_before_the_second, topics_written_before_the_third
99

100 101 102
    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
103

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

    assert_equal all_topics, Topic.written_before(nil)
  end
109

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

116
  def test_has_many_associations_have_access_to_scopes
117
    assert_not_equal Post.containing_the_letter_a, authors(:david).posts
118
    assert_not_empty Post.containing_the_letter_a
119

120 121
    expected = authors(:david).posts & Post.containing_the_letter_a
    assert_equal expected.sort_by(&:id), authors(:david).posts.containing_the_letter_a.sort_by(&:id)
122
  end
123

124
  def test_scope_with_STI
125 126
    assert_equal 3, Post.containing_the_letter_a.count
    assert_equal 1, SpecialPost.containing_the_letter_a.count
127 128
  end

129
  def test_has_many_through_associations_have_access_to_scopes
130
    assert_not_equal Comment.containing_the_letter_e, authors(:david).comments
131
    assert_not_empty Comment.containing_the_letter_e
132

133 134
    expected = authors(:david).comments & Comment.containing_the_letter_e
    assert_equal expected.sort_by(&:id), authors(:david).comments.containing_the_letter_e.sort_by(&:id)
135
  end
136

137
  def test_scopes_honor_current_scopes_from_when_defined
138 139
    assert_not_empty Post.ranked_by_comments.limit_by(5)
    assert_not_empty authors(:david).posts.ranked_by_comments.limit_by(5)
140
    assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5)
141
    assert_not_equal Post.top(5), authors(:david).posts.top(5)
142
    # Oracle sometimes sorts differently if WHERE condition is changed
143
    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)
144
    assert_equal Post.ranked_by_comments.limit_by(5), Post.top(5)
145 146
  end

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

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
  def test_scopes_name_is_relation_method
    conflicts = [
      :records,
      :to_ary,
      :to_sql,
      :explain
    ]

    conflicts.each do |name|
      e = assert_raises ArgumentError do
        Class.new(Post).class_eval { scope name, -> { where(approved: true) } }
      end
      assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
    end
  end

170
  def test_active_records_have_scope_named__all__
171
    assert_not_empty Topic.all
172

J
Jon Leighton 已提交
173
    assert_equal Topic.all.to_a, Topic.base
174
  end
175

176
  def test_active_records_have_scope_named__scoped__
J
Jon Leighton 已提交
177
    scope = Topic.where("content LIKE '%Have%'")
178
    assert_not_empty scope
179

180
    assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'")
181
  end
182

183
  def test_first_and_last_should_allow_integers_for_limit
184
    assert_equal Topic.base.first(2), Topic.base.order("id").to_a.first(2)
185
    assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2)
186 187 188 189
  end

  def test_first_and_last_should_not_use_query_when_results_are_loaded
    topics = Topic.base
190
    topics.load # force load
191 192 193 194 195 196
    assert_no_queries do
      topics.first
      topics.last
    end
  end

197 198 199 200
  def test_empty_should_not_load_results
    topics = Topic.base
    assert_queries(2) do
      topics.empty?  # use count query
201
      topics.load    # force load
202 203 204
      topics.empty?  # use loaded (no query)
    end
  end
205

206 207
  def test_any_should_not_load_results
    topics = Topic.base
208 209
    assert_queries(2) do
      topics.any?    # use count query
210
      topics.load    # force load
211
      topics.any?    # use loaded (no query)
212 213 214 215 216 217
    end
  end

  def test_any_should_call_proxy_found_if_using_a_block
    topics = Topic.base
    assert_queries(1) do
218 219 220
      assert_not_called(topics, :empty?) do
        topics.any? { true }
      end
221 222 223
    end
  end

224
  def test_any_should_not_fire_query_if_scope_loaded
225
    topics = Topic.base
226
    topics.load # force load
227 228 229
    assert_no_queries { assert topics.any? }
  end

230
  def test_model_class_should_respond_to_any
231
    assert_predicate Topic, :any?
232
    Topic.delete_all
233
    assert_not_predicate Topic, :any?
234 235
  end

236 237 238 239
  def test_many_should_not_load_results
    topics = Topic.base
    assert_queries(2) do
      topics.many?   # use count query
240
      topics.load    # force load
241 242 243 244 245 246 247
      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
248 249 250
      assert_not_called(topics, :size) do
        topics.many? { true }
      end
251 252 253
    end
  end

254
  def test_many_should_not_fire_query_if_scope_loaded
255
    topics = Topic.base
256
    topics.load # force load
257 258 259 260
    assert_no_queries { assert topics.many? }
  end

  def test_many_should_return_false_if_none_or_one
261
    topics = Topic.base.where(id: 0)
262
    assert_not_predicate topics, :many?
263
    topics = Topic.base.where(id: 1)
264
    assert_not_predicate topics, :many?
265 266 267
  end

  def test_many_should_return_true_if_more_than_one
268
    assert_predicate Topic.base, :many?
269 270
  end

271 272
  def test_model_class_should_respond_to_many
    Topic.delete_all
273
    assert_not_predicate Topic, :many?
274
    Topic.create!
275
    assert_not_predicate Topic, :many?
276
    Topic.create!
277
    assert_predicate Topic, :many?
278 279
  end

280
  def test_should_build_on_top_of_scope
281 282 283 284
    topic = Topic.approved.build({})
    assert topic.approved
  end

285
  def test_should_build_new_on_top_of_scope
286 287 288 289
    topic = Topic.approved.new
    assert topic.approved
  end

290
  def test_should_create_on_top_of_scope
291 292 293 294
    topic = Topic.approved.create({})
    assert topic.approved
  end

295
  def test_should_create_with_bang_on_top_of_scope
296 297 298
    topic = Topic.approved.create!({})
    assert topic.approved
  end
299

300
  def test_should_build_on_top_of_chained_scopes
301 302
    topic = Topic.approved.by_lifo.build({})
    assert topic.approved
303
    assert_equal "lifo", topic.author_name
304
  end
305

306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
  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 已提交
331
      :public,        # some important methods on Module and Class
332
      :protected,
333 334 335 336
      :private,
      :name,
      :parent,
      :superclass
337 338 339 340 341 342 343 344 345 346 347 348
    ]

    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 已提交
349
      e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
350
        klass.class_eval { scope name, -> { where(approved: true) } }
351
      end
F
Franky W 已提交
352
      assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
353

F
Franky W 已提交
354
      e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
355
        subklass.class_eval { scope name, -> { where(approved: true) } }
356
      end
F
Franky W 已提交
357
      assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
358 359 360 361
    end

    non_conflicts.each do |name|
      assert_nothing_raised do
362
        silence_warnings do
363
          klass.class_eval { scope name, -> { where(approved: true) } }
364
        end
365 366 367
      end

      assert_nothing_raised do
368
        subklass.class_eval { scope name, -> { where(approved: true) } }
369 370 371 372
      end
    end
  end

373 374 375 376 377 378 379
  # 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 '% %'") }
380
      scope :approved, -> { where(approved: true) }
381 382 383 384 385
    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

386
  def test_find_all_should_behave_like_select
387
    assert_equal Topic.base.to_a.select(&:approved), Topic.base.to_a.find_all(&:approved)
388
  end
389

390
  def test_rand_should_select_a_random_object_from_proxy
391
    assert_kind_of Topic, Topic.approved.sample
392 393
  end

394
  def test_should_use_where_in_query_for_scope
395
    assert_equal Developer.where(name: "Jamis").to_set, Developer.where(id: Developer.jamises).to_set
396
  end
397 398 399 400 401 402 403 404 405 406

  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
407
    topics.load # force load
408 409 410 411
    assert_no_queries do
      topics.size # use loaded (no query)
    end
  end
412

413
  def test_should_not_duplicates_where_values
414 415
    relation = Topic.where("1=1")
    assert_equal relation.where_clause, relation.scope_with_lambda.where_clause
416 417
  end

418 419 420
  def test_chaining_with_duplicate_joins
    join = "INNER JOIN comments ON comments.post_id = posts.id"
    post = Post.find(1)
J
Jon Leighton 已提交
421
    assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size
422
  end
423

424
  def test_chaining_applies_last_conditions_when_creating
425
    post = Topic.rejected.new
426
    assert_not_predicate post, :approved?
427 428

    post = Topic.rejected.approved.new
429
    assert_predicate post, :approved?
430

431
    post = Topic.approved.rejected.new
432
    assert_not_predicate post, :approved?
433 434

    post = Topic.approved.rejected.approved.new
435
    assert_predicate post, :approved?
436 437
  end

438
  def test_chaining_combines_conditions_when_searching
439
    # Normal hash conditions
440 441
    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
442 443

    # Nested hash conditions with same keys
444
    assert_equal [], Post.with_special_comments.with_very_special_comments.to_a
445 446

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

450
  def test_class_method_in_scope
451 452 453
    assert_deprecated do
      assert_equal [topics(:second)], topics(:first).approved_replies.ordered
    end
454 455 456 457 458 459 460
  end

  def test_nested_scoping
    expected = Reply.approved
    assert_equal expected.to_a, Topic.rejected.nested_scoping(expected)
  end

461
  def test_scopes_batch_finders
462
    assert_equal 4, Topic.approved.count
463

464
    assert_queries(5) do
465
      Topic.approved.find_each(batch_size: 1) { |t| assert t.approved? }
466 467
    end

468
    assert_queries(3) do
469
      Topic.approved.find_in_batches(batch_size: 2) do |group|
470
        group.each { |t| assert t.approved? }
471 472 473
      end
    end
  end
474 475 476

  def test_table_names_for_chaining_scopes_with_and_without_table_name_included
    assert_nothing_raised do
477
      Comment.for_first_post.for_first_author.to_a
478 479
    end
  end
480

481 482 483 484 485 486 487 488 489 490 491 492 493 494
  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)
U
utilum 已提交
495
      assert_called(ActiveRecord::Base.logger, :warn) do
496
        silence_warnings { Topic.scope(reserved_method, -> { }) }
U
utilum 已提交
497
      end
498 499 500
    end
  end

501
  def test_scopes_on_relations
502
    # Topic.replied
503
    approved_topics = Topic.all.approved.order("id DESC")
504
    assert_equal topics(:fifth), approved_topics.first
505 506 507 508 509

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

510
  def test_index_on_scope
511
    approved = Topic.approved.order("id ASC")
512
    assert_equal topics(:second), approved[0]
513
    assert_predicate approved, :loaded?
514
  end
515

516
  def test_nested_scopes_queries_size
517
    assert_queries(1) do
518
      Topic.approved.by_lifo.replied.written_before(Time.now).to_a
519 520
    end
  end
521

522 523 524
  # 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.
525
  def test_scopes_are_cached_on_associations
526 527
    post = posts(:welcome)

528
    Post.cache do
529 530
      assert_queries(1) { post.comments.containing_the_letter_e.to_a }
      assert_no_queries { post.comments.containing_the_letter_e.to_a }
531
    end
532
  end
533

534
  def test_scopes_with_arguments_are_cached_on_associations
535 536
    post = posts(:welcome)

537
    Post.cache do
538
      one = assert_queries(1) { post.comments.limit_by(1).to_a }
539
      assert_equal 1, one.size
540

541
      two = assert_queries(1) { post.comments.limit_by(2).to_a }
542
      assert_equal 2, two.size
543

544 545
      assert_no_queries { post.comments.limit_by(1).to_a }
      assert_no_queries { post.comments.limit_by(2).to_a }
546
    end
547 548
  end

A
Arun Agrawal 已提交
549 550 551
  def test_scopes_to_get_newest
    post = posts(:welcome)
    old_last_comment = post.comments.newest
552
    new_comment = post.comments.create(body: "My new comment")
A
Arun Agrawal 已提交
553 554 555 556
    assert_equal new_comment, post.comments.newest
    assert_not_equal old_last_comment, post.comments.newest
  end

557
  def test_scopes_are_reset_on_association_reload
558 559 560 561
    post = posts(:welcome)

    [:destroy_all, :reset, :delete_all].each do |method|
      before = post.comments.containing_the_letter_e
562
      post.association(:comments).send(method)
563
      assert before.object_id != post.comments.containing_the_letter_e.object_id, "CollectionAssociation##{method} should reset the named scopes cache"
564 565
    end
  end
566

567
  def test_scoped_are_lazy_loaded_if_table_still_does_not_exist
568 569 570 571
    assert_nothing_raised do
      require "models/without_table"
    end
  end
J
Jon Leighton 已提交
572

573
  def test_eager_default_scope_relations_are_remove
J
Jon Leighton 已提交
574
    klass = Class.new(ActiveRecord::Base)
575
    klass.table_name = "posts"
J
Jon Leighton 已提交
576

577
    assert_raises(ArgumentError) do
578
      klass.send(:default_scope, klass.where(id: posts(:welcome).id))
J
Jon Leighton 已提交
579 580
    end
  end
N
Neeraj Singh 已提交
581 582

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

586 587 588 589 590 591
  def test_model_class_should_respond_to_extending
    assert_raises OopsError do
      Comment.unscoped.oops_comments.destroy_all
    end
  end

592
  def test_model_class_should_respond_to_none
593
    assert_not_predicate Topic, :none?
594
    Topic.delete_all
595
    assert_predicate Topic, :none?
596 597 598
  end

  def test_model_class_should_respond_to_one
599
    assert_not_predicate Topic, :one?
600
    Topic.delete_all
601
    assert_not_predicate Topic, :one?
602
    Topic.create!
603
    assert_predicate Topic, :one?
604
  end
605 606 607 608 609 610 611 612 613 614

  def test_scope_with_annotation
    Topic.class_eval do
      scope :including_annotate_in_scope, Proc.new { annotate("from-scope") }
    end

    assert_sql(%r{/\* from-scope \*/}) do
      assert Topic.including_annotate_in_scope.to_a, Topic.all.to_a
    end
  end
615
end