diff --git a/test/test_orm_paranoia.rb b/test/test_orm_paranoia.rb index 75c227d..6a5fb8b 100644 --- a/test/test_orm_paranoia.rb +++ b/test/test_orm_paranoia.rb @@ -156,3 +156,191 @@ class OrmParanoiaTest < Minitest::Test widget.really_destroy! + assert_equal false, seen + end + + def test_soft_deleting_predicate_is_false_for_a_non_paranoid_model + seen = nil + plain_class = Class.new(Prism::Orm::Model) do + table_name :plain_things + before_destroy { seen = soft_deleting? } + end + plain_class.store = @store + thing = plain_class.create!(name: 'gizmo') + + thing.destroy + + assert_equal false, seen + end + + def test_restore_undoes_a_soft_delete + widget = Widget.create!(name: 'gizmo') + widget.destroy + refute_includes Widget.all.map(&:id), widget.id + + widget.restore + + refute widget.destroyed? + refute widget.deleted? + assert_includes Widget.all.map(&:id), widget.id + assert_nil Widget.find(widget.id).deleted_at + end + + def test_restore_on_a_record_that_was_never_deleted_is_a_no_op + widget = Widget.create!(name: 'gizmo') + widget.restore + refute widget.deleted? + end + + def test_with_deleted_sees_both_live_and_soft_deleted_records + live = Widget.create!(name: 'live') + dead = Widget.create!(name: 'dead') + dead.destroy + + ids = Widget.with_deleted.map(&:id) + assert_includes ids, live.id + assert_includes ids, dead.id + assert_equal [dead.id], Widget.only_deleted.map(&:id) + end + + def test_really_destroy_bang_removes_the_row_for_real + widget = Widget.create!(name: 'gizmo') + widget.destroy + assert_includes Widget.only_deleted.map(&:id), widget.id + + widget.really_destroy! + + assert_nil @store.find('widgets', widget.id) + refute_includes Widget.with_deleted.map(&:id), widget.id + end + + def test_really_destroy_bang_on_a_non_paranoid_model_behaves_like_plain_destroy + thing = PlainThing.create!(name: 'gizmo') + thing.really_destroy! + assert_raises(Prism::Orm::RecordNotFound) { PlainThing.find(thing.id) } + end + + def test_dependent_destroy_cascades_a_soft_delete_to_paranoid_children + author = Author.create!(name: 'foksiu') + post = Post.create!(title: 'hello', author_id: author.id) + + author.destroy + + refute_nil @store.find('posts', post.id), 'child row must still physically exist' + assert_empty Post.all.to_a + assert_includes Post.only_deleted.map(&:id), post.id + end + + def test_paranoid_destroy_still_respects_optimistic_locking + original = LockedWidget.create!(name: 'a') + stale_copy = LockedWidget.find(original.id) + original.update!(name: 'changed') + + error = assert_raises(Prism::Orm::StaleObjectError) { stale_copy.destroy } + assert_equal :destroy, error.attempted_action + refute stale_copy.destroyed? + refute_nil LockedWidget.find(original.id), 'stale soft-delete must not have stamped the row' + end + + def test_paranoid_column_can_be_customized + @store.create_table(:archivables, %w[id name archived_at]) + archivable_class = Class.new(Prism::Orm::Model) do + table_name :archivables + acts_as_paranoid column: 'archived_at' + end + archivable_class.store = @store + + record = archivable_class.create!(name: 'a') + record.destroy + + assert_equal 'archived_at', archivable_class.paranoid_column + assert_empty archivable_class.all.to_a + refute_nil @store.find('archivables', record.id)['archived_at'] + end + + def test_paranoid_setting_is_inherited_by_subclasses + subclass = Class.new(Widget) { table_name :widgets } + subclass.store = @store + + assert subclass.paranoid? + assert_equal 'deleted_at', subclass.paranoid_column + + record = subclass.create!(name: 'a') + record.destroy + assert_empty subclass.all.to_a + end + + def test_plain_restore_does_not_bring_back_dependent_destroyed_children + blog = Blog.create!(name: 'main', entries_count: 0) + entry = Entry.create!(title: 'first', blog_id: blog.id) + blog.destroy + assert_includes Entry.only_deleted.map(&:id), entry.id + + blog.restore + + refute blog.deleted? + assert_includes Entry.only_deleted.map(&:id), entry.id, 'a non-recursive restore must leave children trashed' + end + + def test_recursive_restore_brings_back_paranoid_dependent_children + blog = Blog.create!(name: 'main', entries_count: 0) + e1 = Entry.create!(title: 'first', blog_id: blog.id) + e2 = Entry.create!(title: 'second', blog_id: blog.id) + blog.destroy + assert_empty Entry.all.to_a + + blog.restore(recursive: true) + + ids = Entry.all.map(&:id) + assert_includes ids, e1.id + assert_includes ids, e2.id + assert_equal 2, blog.reload.entries_count, 'counter_cache must be restored along with each child' + end + + def test_recursive_restore_recurses_into_grandchildren + @store.create_table(:pages, %w[id title entry_id deleted_at]) + page_class = Class.new(Prism::Orm::Model) do + table_name :pages + acts_as_paranoid + belongs_to :entry, class_name: 'Entry', foreign_key: 'entry_id' + end + page_class.store = @store + Object.const_set(:Page, page_class) + Entry.has_many :pages, class_name: 'Page', foreign_key: 'entry_id', dependent: :destroy + + blog = Blog.create!(name: 'main', entries_count: 0) + entry = Entry.create!(title: 'first', blog_id: blog.id) + page = Page.create!(title: 'sub', entry_id: entry.id) + blog.destroy + assert_includes Page.only_deleted.map(&:id), page.id + + blog.restore(recursive: true) + + assert_includes Page.all.map(&:id), page.id + ensure + Object.send(:remove_const, :Page) if Object.const_defined?(:Page) + end + + def test_restore_runs_before_and_after_restore_callbacks_in_order + calls = [] + Widget.before_restore { calls << :before } + Widget.after_restore { calls << :after } + widget = Widget.create!(name: 'gizmo') + widget.destroy + + widget.restore + + assert_equal %i[before after], calls + end + + def test_before_restore_abort_leaves_the_record_soft_deleted + Widget.before_restore { throw :abort } + widget = Widget.create!(name: 'gizmo') + widget.destroy + + widget.restore + + assert widget.deleted? + assert_includes Widget.only_deleted.map(&:id), widget.id + end +end