test: cover OrmCounterCacheCascadeDestroyTest, teardown
This commit is contained in:
1 parent
659271b3ec
commit
995eac6bea
2 files changed
+162
No files matched your search
@@ -0,0 +1,91 @@
|
|||||||
|
require 'minitest/autorun'
|
||||||
|
require 'prism'
|
||||||
|
require 'tmpdir'
|
||||||
|
require 'fileutils'
|
||||||
|
|
||||||
|
# Regression test for a real bug found through examples/blog integration
|
||||||
|
# (blog_demo.rb liking a post AND one of its comments, then trashing and
|
||||||
|
# restoring the post): a three-level paranoid dependent: :destroy cascade
|
||||||
|
# where the MIDDLE record (Comment) is both a counter_cache CHILD (of Post)
|
||||||
|
# and a counter_cache OWNER (of its own Like) used to lose counter updates.
|
||||||
|
#
|
||||||
|
# Model#save/Paranoia#soft_delete_row used to write the record's FULL
|
||||||
|
# @attributes hash on every update, including the soft-delete stamp. When
|
||||||
|
# Comment's own dependent: :destroy cascade (its Like) bumped Comment's
|
||||||
|
# likes_count via Store#increment_counter (a raw row mutation bypassing
|
||||||
|
# Comment's in-memory @attributes) and THEN Comment's own soft_delete_row
|
||||||
|
# wrote its stale in-memory @attributes back over the row, the fresh
|
||||||
|
# counter mutation was clobbered. The mirror bug existed on Paranoia#restore
|
||||||
|
# too, since it calls the ordinary #save (full-attribute overwrite) BEFORE
|
||||||
|
# cascade_restore_dependent_associations increments the counter again,
|
||||||
|
# corrupting the count from the other direction (double count instead of
|
||||||
|
# lost count). Both are fixed by only ever writing changed attributes
|
||||||
|
# (Model#dirty_attributes) instead of the full @attributes hash.
|
||||||
|
class OrmCounterCacheCascadeDestroyTest < Minitest::Test
|
||||||
|
def setup
|
||||||
|
@dir = Dir.mktmpdir('prism-counter-cache-cascade-store')
|
||||||
|
@store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore'))
|
||||||
|
@store.create_table(:posts, %w[id title comments_count likes_count deleted_at])
|
||||||
|
@store.create_table(:comments, %w[id post_id body likes_count deleted_at])
|
||||||
|
@store.create_table(:likes, %w[id likeable_id likeable_type deleted_at])
|
||||||
|
|
||||||
|
post_class = Class.new(Prism::Orm::Model) do
|
||||||
|
table_name :posts
|
||||||
|
has_many :comments, foreign_key: 'post_id', dependent: :destroy
|
||||||
|
has_many :likes, as: :likeable, dependent: :destroy
|
||||||
|
acts_as_paranoid
|
||||||
|
end
|
||||||
|
comment_class = Class.new(Prism::Orm::Model) do
|
||||||
|
table_name :comments
|
||||||
|
belongs_to :post, class_name: 'Post', foreign_key: 'post_id', counter_cache: true
|
||||||
|
has_many :likes, as: :likeable, dependent: :destroy
|
||||||
|
acts_as_paranoid
|
||||||
|
end
|
||||||
|
like_class = Class.new(Prism::Orm::Model) do
|
||||||
|
table_name :likes
|
||||||
|
belongs_to :likeable, polymorphic: true, counter_cache: true
|
||||||
|
acts_as_paranoid
|
||||||
|
end
|
||||||
|
[post_class, comment_class, like_class].each { |k| k.store = @store }
|
||||||
|
Object.const_set(:Post, post_class)
|
||||||
|
Object.const_set(:Comment, comment_class)
|
||||||
|
Object.const_set(:Like, like_class)
|
||||||
|
|
||||||
|
@post = Post.create!(title: 'hi')
|
||||||
|
@comment = Comment.create!(post_id: @post.id, body: 'yo')
|
||||||
|
@like_on_post = Like.create!(likeable_id: @post.id, likeable_type: 'Post')
|
||||||
|
@like_on_comment = Like.create!(likeable_id: @comment.id, likeable_type: 'Comment')
|
||||||
|
end
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
FileUtils.remove_entry(@dir)
|
||||||
|
%i[Post Comment Like].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_counters_start_correct
|
||||||
|
assert_equal 1, @post.reload.comments_count
|
||||||
|
assert_equal 1, @post.reload.likes_count
|
||||||
|
assert_equal 1, @comment.reload.likes_count
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cascading_soft_destroy_does_not_clobber_a_counter_bumped_by_its_own_nested_cascade
|
||||||
|
@post.destroy
|
||||||
|
|
||||||
|
# Comment's own Like was destroyed as part of Comment's own cascade
|
||||||
|
# (triggered by Post's cascade destroying Comment), decrementing
|
||||||
|
# Comment#likes_count to 0 - and Comment's subsequent soft-delete stamp
|
||||||
|
# must not silently revert that back to the stale in-memory value (1).
|
||||||
|
assert_equal 0, @store.find(:comments, @comment.id)['likes_count']
|
||||||
|
assert_equal 0, @store.find(:posts, @post.id)['comments_count']
|
||||||
|
assert_equal 0, @store.find(:posts, @post.id)['likes_count']
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_recursive_restore_brings_every_counter_back_exactly_once
|
||||||
|
@post.destroy
|
||||||
|
@post.restore(recursive: true)
|
||||||
|
|
||||||
|
assert_equal 1, @post.reload.comments_count
|
||||||
|
assert_equal 1, @post.reload.likes_count
|
||||||
|
assert_equal 1, Comment.find(@comment.id).likes_count
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
require 'minitest/autorun'
|
||||||
|
require 'prism'
|
||||||
|
require 'tmpdir'
|
||||||
|
|
||||||
|
class OrmDependentDestroyTest < Minitest::Test
|
||||||
|
def setup
|
||||||
|
@dir = Dir.mktmpdir('prism-dependent-store')
|
||||||
|
@store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore'))
|
||||||
|
@store.create_table(:authors, %w[id name])
|
||||||
|
@store.create_table(:posts, %w[id title author_id])
|
||||||
|
@store.create_table(:profiles, %w[id author_id bio])
|
||||||
|
|
||||||
|
author_class = Class.new(Prism::Orm::Model) do
|
||||||
|
table_name :authors
|
||||||
|
has_many :posts, class_name: 'Post', foreign_key: 'author_id', dependent: :destroy
|
||||||
|
has_one :profile, class_name: 'Profile', foreign_key: 'author_id', dependent: :nullify
|
||||||
|
end
|
||||||
|
post_class = Class.new(Prism::Orm::Model) do
|
||||||
|
table_name :posts
|
||||||
|
belongs_to :author, class_name: 'Author', foreign_key: 'author_id'
|
||||||
|
end
|
||||||
|
profile_class = Class.new(Prism::Orm::Model) do
|
||||||
|
table_name :profiles
|
||||||
|
belongs_to :author, class_name: 'Author', foreign_key: 'author_id'
|
||||||
|
end
|
||||||
|
[author_class, post_class, profile_class].each { |klass| klass.store = @store }
|
||||||
|
Object.const_set(:Author, author_class)
|
||||||
|
Object.const_set(:Post, post_class)
|
||||||
|
Object.const_set(:Profile, profile_class)
|
||||||
|
end
|
||||||
|
|
||||||
|
def teardown
|
||||||
|
FileUtils.remove_entry(@dir)
|
||||||
|
%i[Author Post Profile].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_dependent_destroy_removes_has_many_children
|
||||||
|
author = Author.create!(name: 'foksiu')
|
||||||
|
p1 = Post.create!(title: 'a', author_id: author.id)
|
||||||
|
p2 = Post.create!(title: 'b', author_id: author.id)
|
||||||
|
|
||||||
|
author.destroy
|
||||||
|
|
||||||
|
assert_raises(Prism::Orm::RecordNotFound) { Post.find(p1.id) }
|
||||||
|
assert_raises(Prism::Orm::RecordNotFound) { Post.find(p2.id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_dependent_nullify_clears_foreign_key_on_has_one
|
||||||
|
author = Author.create!(name: 'foksiu')
|
||||||
|
profile = Profile.create!(bio: 'hello', author_id: author.id)
|
||||||
|
|
||||||
|
author.destroy
|
||||||
|
|
||||||
|
reloaded = Profile.find(profile.id)
|
||||||
|
assert_nil reloaded.author_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_destroying_childless_author_does_not_raise
|
||||||
|
author = Author.create!(name: 'lonely')
|
||||||
|
assert author.destroy.destroyed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unsupported_dependent_option_raises_at_declaration_time
|
||||||
|
assert_raises(ArgumentError) do
|
||||||
|
Class.new(Prism::Orm::Model) do
|
||||||
|
table_name :posts
|
||||||
|
has_many :whatever, class_name: 'Post', foreign_key: 'author_id', dependent: :delete_all
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user