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

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

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

J
Jon Leighton 已提交
15 16 17 18
    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 }
19
  end
20

21
  def test_found_items_are_cached
22
    all_posts = Topic.base
23

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

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

34 35 36 37
    new_post = Topic.create!
    assert !all_posts.include?(new_post)
    assert all_posts.reload.include?(new_post)
  end
38

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

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

48 49 50 51 52 53
  def test_method_missing_priority_when_delegating
    klazz = Class.new(ActiveRecord::Base) do
      self.table_name = "topics"
      scope :since, Proc.new { where('written_on >= ?', Time.now - 1.day) }
      scope :to,    Proc.new { where('written_on <= ?', Time.now) }
    end
54
    assert_equal klazz.to.since.to_a, klazz.since.to.to_a
55 56
  end

57
  def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
58
    assert Topic.approved.respond_to?(:limit)
59 60 61 62
    assert Topic.approved.respond_to?(:count)
    assert Topic.approved.respond_to?(:length)
  end

63
  def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
64
    assert !Topic.all.merge!(:where => {:approved => true}).to_a.empty?
65

66
    assert_equal Topic.all.merge!(:where => {:approved => true}).to_a, Topic.approved
J
Jon Leighton 已提交
67
    assert_equal Topic.where(:approved => true).count, Topic.approved.count
68
  end
69

70 71 72 73 74 75
  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

76
  def test_scopes_are_composable
77 78
    assert_equal((approved = Topic.all.merge!(:where => {:approved => true}).to_a), Topic.approved)
    assert_equal((replied = Topic.all.merge!(:where => 'replies_count > 0').to_a), Topic.replied)
79
    assert !(approved == replied)
80
    assert !(approved & replied).empty?
81

82 83 84 85
    assert_equal approved & replied, Topic.approved.replied
  end

  def test_procedural_scopes
86 87
    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)
88
    assert_not_equal topics_written_before_the_second, topics_written_before_the_third
89

90 91 92
    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
93

94
  def test_procedural_scopes_returning_nil
J
Jon Leighton 已提交
95
    all_topics = Topic.all
96 97 98

    assert_equal all_topics, Topic.written_before(nil)
  end
99

100 101 102 103 104 105
  def test_scope_with_object
    objects = Topic.with_object
    assert_operator objects.length, :>, 0
    assert objects.all?(&:approved?), 'all objects should be approved'
  end

106
  def test_has_many_associations_have_access_to_scopes
107 108
    assert_not_equal Post.containing_the_letter_a, authors(:david).posts
    assert !Post.containing_the_letter_a.empty?
109

110 111
    assert_equal authors(:david).posts & Post.containing_the_letter_a, authors(:david).posts.containing_the_letter_a
  end
112

113
  def test_scope_with_STI
114 115 116 117
    assert_equal 3,Post.containing_the_letter_a.count
    assert_equal 1,SpecialPost.containing_the_letter_a.count
  end

118
  def test_has_many_through_associations_have_access_to_scopes
119
    assert_not_equal Comment.containing_the_letter_e, authors(:david).comments
120
    assert !Comment.containing_the_letter_e.empty?
121

122 123
    assert_equal authors(:david).comments & Comment.containing_the_letter_e, authors(:david).comments.containing_the_letter_e
  end
124

125
  def test_scopes_honor_current_scopes_from_when_defined
126 127 128
    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)
129
    assert_not_equal Post.top(5), authors(:david).posts.top(5)
130
    # Oracle sometimes sorts differently if WHERE condition is changed
131
    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)
132
    assert_equal Post.ranked_by_comments.limit_by(5), Post.top(5)
133 134
  end

135
  def test_active_records_have_scope_named__all__
J
Jon Leighton 已提交
136
    assert !Topic.all.empty?
137

J
Jon Leighton 已提交
138
    assert_equal Topic.all.to_a, Topic.base
139
  end
140

141
  def test_active_records_have_scope_named__scoped__
J
Jon Leighton 已提交
142 143
    scope = Topic.where("content LIKE '%Have%'")
    assert !scope.empty?
144

145
    assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'")
146
  end
147

148
  def test_first_and_last_should_allow_integers_for_limit
149
    assert_equal Topic.base.first(2), Topic.base.to_a.first(2)
150
    assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2)
151 152 153 154 155 156 157 158 159 160 161
  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

162 163 164 165 166 167 168 169
  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
170

171 172
  def test_any_should_not_load_results
    topics = Topic.base
173 174 175 176
    assert_queries(2) do
      topics.any?    # use count query
      topics.collect # force load
      topics.any?    # use loaded (no query)
177 178 179 180 181 182 183 184 185 186 187
    end
  end

  def test_any_should_call_proxy_found_if_using_a_block
    topics = Topic.base
    assert_queries(1) do
      topics.expects(:empty?).never
      topics.any? { true }
    end
  end

188
  def test_any_should_not_fire_query_if_scope_loaded
189 190 191 192 193
    topics = Topic.base
    topics.collect # force load
    assert_no_queries { assert topics.any? }
  end

194 195 196 197 198 199
  def test_model_class_should_respond_to_any
    assert Topic.any?
    Topic.delete_all
    assert !Topic.any?
  end

200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
  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
      topics.expects(:size).never
      topics.many? { true }
    end
  end

217
  def test_many_should_not_fire_query_if_scope_loaded
218 219 220 221 222 223
    topics = Topic.base
    topics.collect # force load
    assert_no_queries { assert topics.many? }
  end

  def test_many_should_return_false_if_none_or_one
J
Jon Leighton 已提交
224
    topics = Topic.base.where(:id => 0)
225
    assert !topics.many?
J
Jon Leighton 已提交
226
    topics = Topic.base.where(:id => 1)
227 228 229 230 231 232 233
    assert !topics.many?
  end

  def test_many_should_return_true_if_more_than_one
    assert Topic.base.many?
  end

234 235 236 237 238 239 240 241 242
  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

243
  def test_should_build_on_top_of_scope
244 245 246 247
    topic = Topic.approved.build({})
    assert topic.approved
  end

248
  def test_should_build_new_on_top_of_scope
249 250 251 252
    topic = Topic.approved.new
    assert topic.approved
  end

253
  def test_should_create_on_top_of_scope
254 255 256 257
    topic = Topic.approved.create({})
    assert topic.approved
  end

258
  def test_should_create_with_bang_on_top_of_scope
259 260 261
    topic = Topic.approved.create!({})
    assert topic.approved
  end
262

263
  def test_should_build_on_top_of_chained_scopes
264 265 266 267
    topic = Topic.approved.by_lifo.build({})
    assert topic.approved
    assert_equal 'lifo', topic.author_name
  end
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 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
  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
    ]

    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|
      assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
        klass.class_eval { scope name, ->{ where(approved: true) } }
      end

      assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
        subklass.class_eval { scope name, ->{ where(approved: true) } }
      end
    end

    non_conflicts.each do |name|
      assert_nothing_raised do
317 318 319
        silence_warnings do
          klass.class_eval { scope name, ->{ where(approved: true) } }
        end
320 321 322 323 324 325 326 327
      end

      assert_nothing_raised do
        subklass.class_eval { scope name, ->{ where(approved: true) } }
      end
    end
  end

328 329 330 331 332 333 334 335 336 337 338 339 340
  # 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 '% %'") }
      scope :approved, -> { where(:approved => true) }
    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

341
  def test_find_all_should_behave_like_select
342
    assert_equal Topic.base.to_a.select(&:approved), Topic.base.to_a.find_all(&:approved)
343
  end
344

345
  def test_rand_should_select_a_random_object_from_proxy
346
    assert_kind_of Topic, Topic.approved.sample
347 348
  end

349
  def test_should_use_where_in_query_for_scope
J
Jon Leighton 已提交
350
    assert_equal Developer.where(name: 'Jamis').to_set, Developer.where(id: Developer.jamises).to_set
351
  end
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366

  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
367

368 369 370 371 372
  def test_should_not_duplicates_where_values
    where_values = Topic.where("1=1").scope_with_lambda.where_values
    assert_equal ["1=1"], where_values
  end

373 374 375
  def test_chaining_with_duplicate_joins
    join = "INNER JOIN comments ON comments.post_id = posts.id"
    post = Post.find(1)
J
Jon Leighton 已提交
376
    assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size
377
  end
378

379
  def test_chaining_applies_last_conditions_when_creating
380 381 382 383 384
    post = Topic.rejected.new
    assert !post.approved?

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

386 387 388 389 390
    post = Topic.approved.rejected.new
    assert !post.approved?

    post = Topic.approved.rejected.approved.new
    assert post.approved?
391 392
  end

393
  def test_chaining_combines_conditions_when_searching
394
    # Normal hash conditions
395 396
    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
397 398

    # Nested hash conditions with same keys
399
    assert_equal [], Post.with_special_comments.with_very_special_comments.to_a
400 401

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

405
  def test_scopes_batch_finders
406
    assert_equal 4, Topic.approved.count
407

408
    assert_queries(5) do
409 410 411
      Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? }
    end

412
    assert_queries(3) do
413 414 415 416 417
      Topic.approved.find_in_batches(:batch_size => 2) do |group|
        group.each {|t| assert t.approved? }
      end
    end
  end
418 419 420

  def test_table_names_for_chaining_scopes_with_and_without_table_name_included
    assert_nothing_raised do
421
      Comment.for_first_post.for_first_author.to_a
422 423
    end
  end
424

425
  def test_scopes_on_relations
426
    # Topic.replied
427
    approved_topics = Topic.all.approved.order('id DESC')
428
    assert_equal topics(:fifth), approved_topics.first
429 430 431 432 433

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

434
  def test_index_on_scope
435 436 437 438
    approved = Topic.approved.order('id ASC')
    assert_equal topics(:second), approved[0]
    assert approved.loaded?
  end
439

440
  def test_nested_scopes_queries_size
441
    assert_queries(1) do
442
      Topic.approved.by_lifo.replied.written_before(Time.now).to_a
443 444
    end
  end
445

446 447 448
  # 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.
449
  def test_scopes_are_cached_on_associations
450 451
    post = posts(:welcome)

452
    Post.cache do
453 454
      assert_queries(1) { post.comments.containing_the_letter_e.to_a }
      assert_no_queries { post.comments.containing_the_letter_e.to_a }
455
    end
456
  end
457

458
  def test_scopes_with_arguments_are_cached_on_associations
459 460
    post = posts(:welcome)

461
    Post.cache do
462
      one = assert_queries(1) { post.comments.limit_by(1).to_a }
463
      assert_equal 1, one.size
464

465
      two = assert_queries(1) { post.comments.limit_by(2).to_a }
466
      assert_equal 2, two.size
467

468 469
      assert_no_queries { post.comments.limit_by(1).to_a }
      assert_no_queries { post.comments.limit_by(2).to_a }
470
    end
471 472
  end

A
Arun Agrawal 已提交
473 474 475 476 477 478 479 480
  def test_scopes_to_get_newest
    post = posts(:welcome)
    old_last_comment = post.comments.newest
    new_comment = post.comments.create(:body => "My new comment")
    assert_equal new_comment, post.comments.newest
    assert_not_equal old_last_comment, post.comments.newest
  end

481
  def test_scopes_are_reset_on_association_reload
482 483 484 485
    post = posts(:welcome)

    [:destroy_all, :reset, :delete_all].each do |method|
      before = post.comments.containing_the_letter_e
486
      post.association(:comments).send(method)
487
      assert before.object_id != post.comments.containing_the_letter_e.object_id, "CollectionAssociation##{method} should reset the named scopes cache"
488 489
    end
  end
490

491
  def test_scoped_are_lazy_loaded_if_table_still_does_not_exist
492 493 494 495
    assert_nothing_raised do
      require "models/without_table"
    end
  end
J
Jon Leighton 已提交
496

497
  def test_eager_default_scope_relations_are_remove
J
Jon Leighton 已提交
498 499 500
    klass = Class.new(ActiveRecord::Base)
    klass.table_name = 'posts'

501
    assert_raises(ArgumentError) do
J
Jon Leighton 已提交
502 503 504
      klass.send(:default_scope, klass.where(:id => posts(:welcome).id))
    end
  end
N
Neeraj Singh 已提交
505 506

  def test_subclass_merges_scopes_properly
J
Jon Leighton 已提交
507
    assert_equal 1, SpecialComment.where(body: 'go crazy').created.count
N
Neeraj Singh 已提交
508 509
  end

510
end