require 'bundler/setup' require 'active_record' require 'minitest/autorun' require 'paranoia' test_framework = defined?(MiniTest::Test) ? MiniTest::Test : MiniTest::Unit::TestCase def connect! ActiveRecord::Base.establish_connection :adapter => 'sqlite3', database: ':memory:' end def setup! connect! { 'parent_model_with_counter_cache_columns' => 'related_models_count INTEGER DEFAULT 0', 'parent_models' => 'deleted_at DATETIME', 'paranoid_models' => 'parent_model_id INTEGER, deleted_at DATETIME', 'paranoid_model_with_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER', 'paranoid_model_with_build_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_and_build_id INTEGER, name VARCHAR(32)', 'paranoid_model_with_anthor_class_name_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER', 'paranoid_model_with_foreign_key_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, has_one_foreign_key_id INTEGER', 'paranoid_model_with_timestamps' => 'parent_model_id INTEGER, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME', 'not_paranoid_model_with_belongs' => 'parent_model_id INTEGER, paranoid_model_with_has_one_id INTEGER', 'not_paranoid_model_with_belongs_and_assocation_not_soft_destroyed_validator' => 'parent_model_id INTEGER, paranoid_model_with_has_one_id INTEGER', 'paranoid_model_with_has_one_and_builds' => 'parent_model_id INTEGER, color VARCHAR(32), deleted_at DATETIME, has_one_foreign_key_id INTEGER', 'featureful_models' => 'deleted_at DATETIME, name VARCHAR(32)', 'plain_models' => 'deleted_at DATETIME', 'callback_models' => 'deleted_at DATETIME', 'fail_callback_models' => 'deleted_at DATETIME', 'related_models' => 'parent_model_id INTEGER, parent_model_with_counter_cache_column_id INTEGER, deleted_at DATETIME', 'asplode_models' => 'parent_model_id INTEGER, deleted_at DATETIME', 'employers' => 'name VARCHAR(32), deleted_at DATETIME', 'employees' => 'deleted_at DATETIME', 'jobs' => 'employer_id INTEGER NOT NULL, employee_id INTEGER NOT NULL, deleted_at DATETIME', 'custom_column_models' => 'destroyed_at DATETIME', 'custom_sentinel_models' => 'deleted_at DATETIME NOT NULL', 'non_paranoid_models' => 'parent_model_id INTEGER', 'polymorphic_models' => 'parent_id INTEGER, parent_type STRING, deleted_at DATETIME', 'namespaced_paranoid_has_ones' => 'deleted_at DATETIME, paranoid_belongs_tos_id INTEGER', 'namespaced_paranoid_belongs_tos' => 'deleted_at DATETIME, paranoid_has_one_id INTEGER', 'unparanoid_unique_models' => 'name VARCHAR(32), paranoid_with_unparanoids_id INTEGER', 'active_column_models' => 'deleted_at DATETIME, active BOOLEAN', 'active_column_model_with_uniqueness_validations' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN', 'paranoid_model_with_belongs_to_active_column_model_with_has_many_relationships' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN, active_column_model_with_has_many_relationship_id INTEGER', 'active_column_model_with_has_many_relationships' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN', 'without_default_scope_models' => 'deleted_at DATETIME' }.each do |table_name, columns_as_sql_string| ActiveRecord::Base.connection.execute "CREATE TABLE #{table_name} (id INTEGER NOT NULL PRIMARY KEY, #{columns_as_sql_string})" end end class WithDifferentConnection < ActiveRecord::Base establish_connection adapter: 'sqlite3', database: ':memory:' connection.execute 'CREATE TABLE with_different_connections (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' acts_as_paranoid end setup! class ParanoiaTest < test_framework def setup connection = ActiveRecord::Base.connection cleaner = ->(source) { ActiveRecord::Base.connection.execute "DELETE FROM #{source}" } if ActiveRecord::VERSION::MAJOR < 5 connection.tables.each(&cleaner) else connection.data_sources.each(&cleaner) end end def test_plain_model_class_is_not_paranoid assert_equal false, PlainModel.paranoid? end def test_paranoid_model_class_is_paranoid assert_equal true, ParanoidModel.paranoid? end def test_plain_models_are_not_paranoid assert_equal false, PlainModel.new.paranoid? end def test_paranoid_models_are_paranoid assert_equal true, ParanoidModel.new.paranoid? end def test_paranoid_models_to_param model = ParanoidModel.new model.save to_param = model.to_param model.destroy assert model.to_param assert_equal to_param, model.to_param end def test_destroy_behavior_for_plain_models model = PlainModel.new assert_equal 0, model.class.count model.save! assert_equal 1, model.class.count model.destroy assert_equal true, model.deleted_at.nil? assert_equal 0, model.class.count assert_equal 0, model.class.unscoped.count end # Anti-regression test for #81, which would've introduced a bug to break this test. def test_destroy_behavior_for_plain_models_callbacks model = CallbackModel.new model.save model.remove_called_variables # clear called callback flags model.destroy assert_nil model.instance_variable_get(:@update_callback_called) assert_nil model.instance_variable_get(:@save_callback_called) assert_nil model.instance_variable_get(:@validate_called) assert model.instance_variable_get(:@destroy_callback_called) assert model.instance_variable_get(:@after_destroy_callback_called) assert model.instance_variable_get(:@after_commit_callback_called) end def test_delete_behavior_for_plain_models_callbacks model = CallbackModel.new model.save model.remove_called_variables # clear called callback flags model.delete assert_nil model.instance_variable_get(:@update_callback_called) assert_nil model.instance_variable_get(:@save_callback_called) assert_nil model.instance_variable_get(:@validate_called) assert_nil model.instance_variable_get(:@destroy_callback_called) assert_nil model.instance_variable_get(:@after_destroy_callback_called) assert_nil model.instance_variable_get(:@after_commit_callback_called) end def test_delete_in_transaction_behavior_for_plain_models_callbacks model = CallbackModel.new model.save model.remove_called_variables # clear called callback flags CallbackModel.transaction do model.delete end assert_nil model.instance_variable_get(:@update_callback_called) assert_nil model.instance_variable_get(:@save_callback_called) assert_nil model.instance_variable_get(:@validate_called) assert_nil model.instance_variable_get(:@destroy_callback_called) assert_nil model.instance_variable_get(:@after_destroy_callback_called) assert model.instance_variable_get(:@after_commit_callback_called) end def test_destroy_behavior_for_paranoid_models model = ParanoidModel.new assert_equal 0, model.class.count model.save! assert_equal 1, model.class.count model.destroy assert_equal false, model.deleted_at.nil? assert_equal 0, model.class.count assert_equal 1, model.class.unscoped.count end def test_update_columns_on_paranoia_destroyed record = ParentModel.create record.destroy assert record.update_columns deleted_at: Time.now end def test_scoping_behavior_for_paranoid_models parent1 = ParentModel.create parent2 = ParentModel.create p1 = ParanoidModel.create(:parent_model => parent1) p2 = ParanoidModel.create(:parent_model => parent2) p1.destroy p2.destroy assert_equal 0, parent1.paranoid_models.count assert_equal 1, parent1.paranoid_models.only_deleted.count assert_equal 2, ParanoidModel.only_deleted.joins(:parent_model).count assert_equal 1, parent1.paranoid_models.deleted.count assert_equal 0, parent1.paranoid_models.without_deleted.count p3 = ParanoidModel.create(:parent_model => parent1) assert_equal 2, parent1.paranoid_models.with_deleted.count assert_equal 1, parent1.paranoid_models.without_deleted.count assert_equal [p1,p3], parent1.paranoid_models.with_deleted end def test_only_deleted_with_joins c1 = ActiveColumnModelWithHasManyRelationship.create(name: 'Jacky') c2 = ActiveColumnModelWithHasManyRelationship.create(name: 'Thomas') p1 = ParanoidModelWithBelongsToActiveColumnModelWithHasManyRelationship.create(name: 'Hello', active_column_model_with_has_many_relationship: c1) c1.destroy assert_equal 1, ActiveColumnModelWithHasManyRelationship.count assert_equal 1, ActiveColumnModelWithHasManyRelationship.only_deleted.count assert_equal 1, ActiveColumnModelWithHasManyRelationship.only_deleted.joins(:paranoid_model_with_belongs_to_active_column_model_with_has_many_relationships).count end def test_destroy_behavior_for_custom_column_models model = CustomColumnModel.new assert_equal 0, model.class.count model.save! assert_nil model.destroyed_at assert_equal 1, model.class.count model.destroy assert_equal false, model.destroyed_at.nil? assert model.paranoia_destroyed? assert_equal 0, model.class.count assert_equal 1, model.class.unscoped.count assert_equal 1, model.class.only_deleted.count assert_equal 1, model.class.deleted.count end def test_default_sentinel_value assert_nil ParanoidModel.paranoia_sentinel_value end def test_without_default_scope_option model = WithoutDefaultScopeModel.create model.destroy assert_equal 1, model.class.count assert_equal 1, model.class.only_deleted.count assert_equal 0, model.class.where(deleted_at: nil).count end def test_active_column_model model = ActiveColumnModel.new assert_equal 0, model.class.count model.save! assert_nil model.deleted_at assert_equal true, model.active assert_equal 1, model.class.count model.destroy assert_equal false, model.deleted_at.nil? assert_nil model.active assert model.paranoia_destroyed? assert_equal 0, model.class.count assert_equal 1, model.class.unscoped.count assert_equal 1, model.class.only_deleted.count assert_equal 1, model.class.deleted.count end def test_active_column_model_with_uniqueness_validation_only_checks_non_deleted_records a = ActiveColumnModelWithUniquenessValidation.create!(name: "A") a.destroy b = ActiveColumnModelWithUniquenessValidation.new(name: "A") assert b.valid? end def test_active_column_model_with_uniqueness_validation_still_works_on_non_deleted_records a = ActiveColumnModelWithUniquenessValidation.create!(name: "A") b = ActiveColumnModelWithUniquenessValidation.new(name: "A") refute b.valid? end def test_sentinel_value_for_custom_sentinel_models model = CustomSentinelModel.new assert_equal 0, model.class.count model.save! assert_equal DateTime.new(0), model.deleted_at assert_equal 1, model.class.count model.destroy assert DateTime.new(0) != model.deleted_at assert model.paranoia_destroyed? assert_equal 0, model.class.count assert_equal 1, model.class.unscoped.count assert_equal 1, model.class.only_deleted.count assert_equal 1, model.class.deleted.count model.restore assert_equal DateTime.new(0), model.deleted_at assert !model.destroyed? assert_equal 1, model.class.count assert_equal 1, model.class.unscoped.count assert_equal 0, model.class.only_deleted.count assert_equal 0, model.class.deleted.count end def test_destroy_behavior_for_featureful_paranoid_models model = get_featureful_model assert_equal 0, model.class.count model.save! assert_equal 1, model.class.count model.destroy assert_equal false, model.deleted_at.nil? assert_equal 0, model.class.count assert_equal 1, model.class.unscoped.count end def test_destroy_behavior_for_has_one_with_build_and_validation_error model = ParanoidModelWithHasOneAndBuild.create model.destroy end # Regression test for #24 def test_chaining_for_paranoid_models scope = FeaturefulModel.where(:name => "foo").only_deleted assert_equal({'name' => "foo"}, scope.where_values_hash) end def test_only_destroyed_scope_for_paranoid_models model = ParanoidModel.new model.save model.destroy model2 = ParanoidModel.new model2.save assert_equal model, ParanoidModel.only_deleted.last assert_equal false, ParanoidModel.only_deleted.include?(model2) end def test_default_scope_for_has_many_relationships parent = ParentModel.create assert_equal 0, parent.related_models.count child = parent.related_models.create assert_equal 1, parent.related_models.count child.destroy assert_equal false, child.deleted_at.nil? assert_equal 0, parent.related_models.count assert_equal 1, parent.related_models.unscoped.count end def test_default_scope_for_has_many_through_relationships employer = Employer.create employee = Employee.create assert_equal 0, employer.jobs.count assert_equal 0, employer.employees.count assert_equal 0, employee.jobs.count assert_equal 0, employee.employers.count job = Job.create :employer => employer, :employee => employee assert_equal 1, employer.jobs.count assert_equal 1, employer.employees.count assert_equal 1, employee.jobs.count assert_equal 1, employee.employers.count employee2 = Employee.create job2 = Job.create :employer => employer, :employee => employee2 employee2.destroy assert_equal 2, employer.jobs.count assert_equal 1, employer.employees.count job.destroy assert_equal 1, employer.jobs.count assert_equal 0, employer.employees.count assert_equal 0, employee.jobs.count assert_equal 0, employee.employers.count end def test_delete_behavior_for_callbacks model = CallbackModel.new model.save model.delete assert_nil model.instance_variable_get(:@destroy_callback_called) end def test_destroy_behavior_for_callbacks model = CallbackModel.new model.save model.destroy assert model.instance_variable_get(:@destroy_callback_called) end def test_destroy_on_readonly_record # Just to demonstrate the AR behaviour model = NonParanoidModel.create! model.readonly! assert_raises ActiveRecord::ReadOnlyRecord do model.destroy end # Mirrors behaviour above model = ParanoidModel.create! model.readonly! assert_raises ActiveRecord::ReadOnlyRecord do model.destroy end end def test_destroy_on_really_destroyed_record model = ParanoidModel.create! model.really_destroy! assert model.really_destroyed? assert model.paranoia_destroyed? model.destroy assert model.really_destroyed? assert model.paranoia_destroyed? end def test_destroy_on_unsaved_record # Just to demonstrate the AR behaviour model = NonParanoidModel.new model.destroy! assert model.destroyed? model.destroy! assert model.destroyed? # Mirrors behaviour above model = ParanoidModel.new model.destroy! assert model.paranoia_destroyed? model.destroy! assert model.paranoia_destroyed? end def test_restore model = ParanoidModel.new model.save id = model.id model.destroy assert model.paranoia_destroyed? model = ParanoidModel.only_deleted.find(id) model.restore! model.reload assert_equal false, model.paranoia_destroyed? end def test_restore_on_object_return_self model = ParanoidModel.create model.destroy assert_equal model.class, model.restore.class end # Regression test for #92 def test_destroy_twice model = ParanoidModel.new model.save model.destroy model.destroy assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count end # Regression test for #92 def test_destroy_bang_twice model = ParanoidModel.new model.save! model.destroy! model.destroy! assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count end def test_destroy_return_value_on_success model = ParanoidModel.create return_value = model.destroy assert_equal(return_value, model) end def test_destroy_return_value_on_failure model = FailCallbackModel.create return_value = model.destroy assert_equal(return_value, false) end def test_restore_behavior_for_callbacks model = CallbackModel.new model.save id = model.id model.destroy assert model.paranoia_destroyed? model = CallbackModel.only_deleted.find(id) model.restore! model.reload assert model.instance_variable_get(:@restore_callback_called) end def test_really_destroy model = ParanoidModel.new model.save model.really_destroy! refute ParanoidModel.unscoped.exists?(model.id) end def test_real_destroy_dependent_destroy parent = ParentModel.create child1 = parent.very_related_models.create child2 = parent.non_paranoid_models.create child3 = parent.create_non_paranoid_model parent.really_destroy! refute RelatedModel.unscoped.exists?(child1.id) refute NonParanoidModel.unscoped.exists?(child2.id) refute NonParanoidModel.unscoped.exists?(child3.id) end def test_real_destroy_dependent_destroy_after_normal_destroy parent = ParentModel.create child = parent.very_related_models.create parent.destroy parent.really_destroy! refute RelatedModel.unscoped.exists?(child.id) end def test_real_destroy_dependent_destroy_after_normal_destroy_does_not_delete_other_children parent_1 = ParentModel.create child_1 = parent_1.very_related_models.create parent_2 = ParentModel.create child_2 = parent_2.very_related_models.create parent_1.destroy parent_1.really_destroy! assert RelatedModel.unscoped.exists?(child_2.id) end def test_really_destroy_behavior_for_callbacks model = CallbackModel.new model.save model.really_destroy! assert model.instance_variable_get(:@real_destroy_callback_called) end def test_really_delete model = ParanoidModel.new model.save model.really_delete refute ParanoidModel.unscoped.exists?(model.id) end def test_multiple_restore a = ParanoidModel.new a.save a_id = a.id a.destroy b = ParanoidModel.new b.save b_id = b.id b.destroy c = ParanoidModel.new c.save c_id = c.id c.destroy ParanoidModel.restore([a_id, c_id]) a.reload b.reload c.reload refute a.paranoia_destroyed? assert b.paranoia_destroyed? refute c.paranoia_destroyed? end def test_restore_with_associations_using_recovery_window parent = ParentModel.create first_child = parent.very_related_models.create second_child = parent.very_related_models.create parent.destroy second_child.update(deleted_at: parent.deleted_at + 11.minutes) parent.restore!(:recursive => true) assert_equal true, parent.deleted_at.nil? assert_equal true, first_child.reload.deleted_at.nil? assert_equal true, second_child.reload.deleted_at.nil? parent.destroy second_child.update(deleted_at: parent.deleted_at + 11.minutes) parent.restore(:recursive => true, :recovery_window => 10.minutes) assert_equal true, parent.deleted_at.nil? assert_equal true, first_child.reload.deleted_at.nil? assert_equal false, second_child.reload.deleted_at.nil? second_child.restore parent.destroy first_child.update(deleted_at: parent.deleted_at - 11.minutes) second_child.update(deleted_at: parent.deleted_at - 9.minutes) ParentModel.restore(parent.id, :recursive => true, :recovery_window => 10.minutes) assert_equal true, parent.reload.deleted_at.nil? assert_equal false, first_child.reload.deleted_at.nil? assert_equal true, second_child.reload.deleted_at.nil? end def test_restore_with_associations parent = ParentModel.create first_child = parent.very_related_models.create second_child = parent.non_paranoid_models.create parent.destroy assert_equal false, parent.deleted_at.nil? assert_equal false, first_child.reload.deleted_at.nil? assert_equal true, second_child.destroyed? parent.restore! assert_equal true, parent.deleted_at.nil? assert_equal false, first_child.reload.deleted_at.nil? assert_equal true, second_child.destroyed? parent.destroy parent.restore(:recursive => true) assert_equal true, parent.deleted_at.nil? assert_equal true, first_child.reload.deleted_at.nil? assert_equal true, second_child.destroyed? parent.destroy ParentModel.restore(parent.id, :recursive => true) assert_equal true, parent.reload.deleted_at.nil? assert_equal true, first_child.reload.deleted_at.nil? assert_equal true, second_child.destroyed? end # regression tests for #118 def test_restore_with_has_one_association # setup and destroy test objects hasOne = ParanoidModelWithHasOne.create belongsTo = ParanoidModelWithBelong.create anthorClassName = ParanoidModelWithAnthorClassNameBelong.create foreignKey = ParanoidModelWithForeignKeyBelong.create notParanoidModel = NotParanoidModelWithBelong.create hasOne.paranoid_model_with_belong = belongsTo hasOne.class_name_belong = anthorClassName hasOne.paranoid_model_with_foreign_key_belong = foreignKey hasOne.not_paranoid_model_with_belong = notParanoidModel hasOne.save! hasOne.destroy assert_equal false, hasOne.deleted_at.nil? assert_equal false, belongsTo.deleted_at.nil? # Does it restore has_one associations? hasOne.restore(:recursive => true) hasOne.save! assert_equal true, hasOne.reload.deleted_at.nil? assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" assert_equal true, notParanoidModel.destroyed? assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" end def test_new_restore_with_has_one_association # setup and destroy test objects hasOne = ParanoidModelWithHasOne.create belongsTo = ParanoidModelWithBelong.create anthorClassName = ParanoidModelWithAnthorClassNameBelong.create foreignKey = ParanoidModelWithForeignKeyBelong.create notParanoidModel = NotParanoidModelWithBelong.create hasOne.paranoid_model_with_belong = belongsTo hasOne.class_name_belong = anthorClassName hasOne.paranoid_model_with_foreign_key_belong = foreignKey hasOne.not_paranoid_model_with_belong = notParanoidModel hasOne.save! hasOne.destroy assert_equal false, hasOne.deleted_at.nil? assert_equal false, belongsTo.deleted_at.nil? # Does it restore has_one associations? newHasOne = ParanoidModelWithHasOne.with_deleted.find(hasOne.id) newHasOne.restore(:recursive => true) newHasOne.save! assert_equal true, hasOne.reload.deleted_at.nil? assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" assert_equal true, notParanoidModel.destroyed? assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" end def test_model_restore_with_has_one_association # setup and destroy test objects hasOne = ParanoidModelWithHasOne.create belongsTo = ParanoidModelWithBelong.create anthorClassName = ParanoidModelWithAnthorClassNameBelong.create foreignKey = ParanoidModelWithForeignKeyBelong.create notParanoidModel = NotParanoidModelWithBelong.create hasOne.paranoid_model_with_belong = belongsTo hasOne.class_name_belong = anthorClassName hasOne.paranoid_model_with_foreign_key_belong = foreignKey hasOne.not_paranoid_model_with_belong = notParanoidModel hasOne.save! hasOne.destroy assert_equal false, hasOne.deleted_at.nil? assert_equal false, belongsTo.deleted_at.nil? # Does it restore has_one associations? ParanoidModelWithHasOne.restore(hasOne.id, :recursive => true) hasOne.save! assert_equal true, hasOne.reload.deleted_at.nil? assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" assert_equal true, notParanoidModel.destroyed? assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" end def test_restore_with_nil_has_one_association # setup and destroy test object hasOne = ParanoidModelWithHasOne.create hasOne.destroy assert_equal false, hasOne.reload.deleted_at.nil? # Does it raise NoMethodException on restore of nil hasOne.restore(:recursive => true) assert hasOne.reload.deleted_at.nil? end def test_restore_with_module_scoped_has_one_association # setup and destroy test object hasOne = Namespaced::ParanoidHasOne.create hasOne.destroy assert_equal false, hasOne.reload.deleted_at.nil? # Does it raise "uninitialized constant ParanoidBelongsTo" # on restore of ParanoidHasOne? hasOne.restore(:recursive => true) assert hasOne.reload.deleted_at.nil? end # covers #185 def test_restoring_recursive_has_one_restores_correct_object hasOnes = 2.times.map { ParanoidModelWithHasOne.create } belongsTos = 2.times.map { ParanoidModelWithBelong.create } hasOnes[0].update paranoid_model_with_belong: belongsTos[0] hasOnes[1].update paranoid_model_with_belong: belongsTos[1] hasOnes.each(&:destroy) ParanoidModelWithHasOne.restore(hasOnes[1].id, :recursive => true) hasOnes.each(&:reload) belongsTos.each(&:reload) # without #185, belongsTos[0] will be restored instead of belongsTos[1] refute_nil hasOnes[0].deleted_at refute_nil belongsTos[0].deleted_at assert_nil hasOnes[1].deleted_at assert_nil belongsTos[1].deleted_at end # covers #131 def test_has_one_really_destroy_with_nil model = ParanoidModelWithHasOne.create model.really_destroy! refute ParanoidModelWithBelong.unscoped.exists?(model.id) end def test_has_one_really_destroy_with_record model = ParanoidModelWithHasOne.create { |record| record.build_paranoid_model_with_belong } model.really_destroy! refute ParanoidModelWithBelong.unscoped.exists?(model.id) end def test_observers_notified a = ParanoidModelWithObservers.create a.destroy a.restore! assert a.observers_notified.select {|args| args == [:before_restore, a]} assert a.observers_notified.select {|args| args == [:after_restore, a]} end def test_observers_not_notified_if_not_supported a = ParanoidModelWithObservers.create a.destroy a.restore! # essentially, we're just ensuring that this doesn't crash end def test_validates_uniqueness_only_checks_non_deleted_records a = Employer.create!(name: "A") a.destroy b = Employer.new(name: "A") assert b.valid? end def test_validates_uniqueness_still_works_on_non_deleted_records a = Employer.create!(name: "A") b = Employer.new(name: "A") refute b.valid? end def test_updated_at_modification_on_destroy paranoid_model = ParanoidModelWithTimestamp.create(:parent_model => ParentModel.create, :updated_at => 1.day.ago) assert paranoid_model.updated_at < 10.minutes.ago paranoid_model.destroy assert paranoid_model.updated_at > 10.minutes.ago end def test_updated_at_modification_on_restore parent1 = ParentModel.create pt1 = ParanoidModelWithTimestamp.create(:parent_model => parent1) ParanoidModelWithTimestamp.record_timestamps = false pt1.update_columns(created_at: 20.years.ago, updated_at: 10.years.ago, deleted_at: 10.years.ago) ParanoidModelWithTimestamp.record_timestamps = true assert pt1.updated_at < 10.minutes.ago refute pt1.deleted_at.nil? pt1.restore! assert pt1.deleted_at.nil? assert pt1.updated_at > 10.minutes.ago end def test_i_am_the_destroyer expected = %Q{ Sharon: "There should be a method called I_AM_THE_DESTROYER!" Ryan: "What should this method do?" Sharon: "It should fix all the spelling errors on the page!" } assert_output expected do ParanoidModel.I_AM_THE_DESTROYER! end end def test_destroy_fails_if_callback_raises_exception parent = AsplodeModel.create assert_raises(StandardError) { parent.destroy } #transaction should be rolled back, so parent NOT deleted refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception' end def test_destroy_fails_if_association_callback_raises_exception parent = ParentModel.create children = [] 3.times { children << parent.asplode_models.create } assert_raises(StandardError) { parent.destroy } #transaction should be rolled back, so parent and children NOT deleted refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception' refute children.any?(&:destroyed?), 'Child record was destroyed, even though AR callback threw exception' end def test_restore_model_with_different_connection ActiveRecord::Base.remove_connection # Disconnect the main connection a = WithDifferentConnection.create a.destroy! a.restore! # This test passes if no exception is raised ensure setup! # Reconnect the main connection end def test_restore_clear_association_cache_if_associations_present parent = ParentModel.create 3.times { parent.very_related_models.create } parent.destroy assert_equal 0, parent.very_related_models.count assert_equal 0, parent.very_related_models.size parent.restore(recursive: true) assert_equal 3, parent.very_related_models.count assert_equal 3, parent.very_related_models.size end def test_model_without_db_connection ActiveRecord::Base.remove_connection NoConnectionModel.class_eval{ acts_as_paranoid } ensure setup! end def test_restore_recursive_on_polymorphic_has_one_association parent = ParentModel.create polymorphic = PolymorphicModel.create(parent: parent) parent.destroy assert_equal 0, polymorphic.class.count parent.restore(recursive: true) assert_equal 1, polymorphic.class.count end # Ensure that we're checking parent_type when restoring def test_missing_restore_recursive_on_polymorphic_has_one_association parent = ParentModel.create polymorphic = PolymorphicModel.create(parent_id: parent.id, parent_type: 'ParanoidModel') parent.destroy polymorphic.destroy assert_equal 0, polymorphic.class.count parent.restore(recursive: true) assert_equal 0, polymorphic.class.count end def test_counter_cache_column_update_on_destroy#_and_restore_and_really_destroy parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count related_model.destroy assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count end def test_callbacks_for_counter_cache_column_update_on_destroy parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create assert_nil related_model.instance_variable_get(:@after_destroy_callback_called) assert_nil related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) related_model.destroy assert related_model.instance_variable_get(:@after_destroy_callback_called) # assert related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) end def test_uniqueness_for_unparanoid_associated parent_model = ParanoidWithUnparanoids.create related = parent_model.unparanoid_unique_models.create # will raise exception if model is not checked for paranoia related.valid? end def test_assocation_not_soft_destroyed_validator notParanoidModel = NotParanoidModelWithBelongsAndAssocationNotSoftDestroyedValidator.create parentModel = ParentModel.create assert notParanoidModel.valid? notParanoidModel.parent_model = parentModel assert notParanoidModel.valid? parentModel.destroy assert !notParanoidModel.valid? assert notParanoidModel.errors.full_messages.include? "Parent model has been soft-deleted" end # TODO: find a fix for Rails 4.1 if ActiveRecord::VERSION::STRING !~ /\A4\.1/ def test_counter_cache_column_update_on_really_destroy parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count related_model.really_destroy! assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count end end # TODO: find a fix for Rails 4.0 and 4.1 if ActiveRecord::VERSION::STRING >= '4.2' def test_callbacks_for_counter_cache_column_update_on_really_destroy! parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create assert_nil related_model.instance_variable_get(:@after_destroy_callback_called) assert_nil related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) related_model.really_destroy! assert related_model.instance_variable_get(:@after_destroy_callback_called) assert related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) end def test_counter_cache_column_on_double_destroy parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create related_model.destroy related_model.destroy assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count end def test_counter_cache_column_on_double_restore parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create related_model.destroy related_model.restore related_model.restore assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count end def test_counter_cache_column_on_destroy_and_really_destroy parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create related_model.destroy related_model.really_destroy! assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count end def test_counter_cache_column_on_restore parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create related_model = parent_model_with_counter_cache_column.related_models.create related_model.destroy assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count related_model.restore assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count end end private def get_featureful_model FeaturefulModel.new(:name => "not empty") end end # Helper classes class ParanoidModel < ActiveRecord::Base belongs_to :parent_model acts_as_paranoid end class ParanoidWithUnparanoids < ActiveRecord::Base self.table_name = 'plain_models' has_many :unparanoid_unique_models end class UnparanoidUniqueModel < ActiveRecord::Base belongs_to :paranoid_with_unparanoids validates :name, :uniqueness => true end class FailCallbackModel < ActiveRecord::Base belongs_to :parent_model acts_as_paranoid before_destroy { |_| if ActiveRecord::VERSION::MAJOR < 5 false else throw :abort end } end class FeaturefulModel < ActiveRecord::Base acts_as_paranoid validates :name, :presence => true, :uniqueness => true end class NonParanoidChildModel < ActiveRecord::Base validates :name, :presence => true, :uniqueness => true end class PlainModel < ActiveRecord::Base end class CallbackModel < ActiveRecord::Base acts_as_paranoid before_destroy { |model| model.instance_variable_set :@destroy_callback_called, true } before_restore { |model| model.instance_variable_set :@restore_callback_called, true } before_update { |model| model.instance_variable_set :@update_callback_called, true } before_save { |model| model.instance_variable_set :@save_callback_called, true} before_real_destroy { |model| model.instance_variable_set :@real_destroy_callback_called, true } after_destroy { |model| model.instance_variable_set :@after_destroy_callback_called, true } after_commit { |model| model.instance_variable_set :@after_commit_callback_called, true } validate { |model| model.instance_variable_set :@validate_called, true } def remove_called_variables instance_variables.each {|name| (name.to_s.end_with?('_called')) ? remove_instance_variable(name) : nil} end end class ParentModel < ActiveRecord::Base acts_as_paranoid has_many :paranoid_models has_many :related_models has_many :very_related_models, :class_name => 'RelatedModel', dependent: :destroy has_many :non_paranoid_models, dependent: :destroy has_one :non_paranoid_model, dependent: :destroy has_many :asplode_models, dependent: :destroy has_one :polymorphic_model, as: :parent, dependent: :destroy end class ParentModelWithCounterCacheColumn < ActiveRecord::Base has_many :related_models end class RelatedModel < ActiveRecord::Base acts_as_paranoid belongs_to :parent_model belongs_to :parent_model_with_counter_cache_column, counter_cache: true after_destroy do |model| if parent_model_with_counter_cache_column && parent_model_with_counter_cache_column.reload.related_models_count == 0 model.instance_variable_set :@after_destroy_callback_called, true end end after_commit :set_after_commit_on_destroy_callback_called, on: :destroy def set_after_commit_on_destroy_callback_called if parent_model_with_counter_cache_column && parent_model_with_counter_cache_column.reload.related_models_count == 0 self.instance_variable_set :@after_commit_on_destroy_callback_called, true end end end class Employer < ActiveRecord::Base acts_as_paranoid validates_uniqueness_of :name has_many :jobs has_many :employees, :through => :jobs end class Employee < ActiveRecord::Base acts_as_paranoid has_many :jobs has_many :employers, :through => :jobs end class Job < ActiveRecord::Base acts_as_paranoid belongs_to :employer belongs_to :employee end class CustomColumnModel < ActiveRecord::Base acts_as_paranoid column: :destroyed_at end class CustomSentinelModel < ActiveRecord::Base acts_as_paranoid sentinel_value: DateTime.new(0) end class WithoutDefaultScopeModel < ActiveRecord::Base acts_as_paranoid without_default_scope: true end class ActiveColumnModel < ActiveRecord::Base acts_as_paranoid column: :active, sentinel_value: true def paranoia_restore_attributes { deleted_at: nil, active: true } end def paranoia_destroy_attributes { deleted_at: current_time_from_proper_timezone, active: nil } end end class ActiveColumnModelWithUniquenessValidation < ActiveRecord::Base validates :name, :uniqueness => true acts_as_paranoid column: :active, sentinel_value: true def paranoia_restore_attributes { deleted_at: nil, active: true } end def paranoia_destroy_attributes { deleted_at: current_time_from_proper_timezone, active: nil } end end class ActiveColumnModelWithHasManyRelationship < ActiveRecord::Base has_many :paranoid_model_with_belongs_to_active_column_model_with_has_many_relationships acts_as_paranoid column: :active, sentinel_value: true def paranoia_restore_attributes { deleted_at: nil, active: true } end def paranoia_destroy_attributes { deleted_at: current_time_from_proper_timezone, active: nil } end end class ParanoidModelWithBelongsToActiveColumnModelWithHasManyRelationship < ActiveRecord::Base belongs_to :active_column_model_with_has_many_relationship acts_as_paranoid column: :active, sentinel_value: true def paranoia_restore_attributes { deleted_at: nil, active: true } end def paranoia_destroy_attributes { deleted_at: current_time_from_proper_timezone, active: nil } end end class NonParanoidModel < ActiveRecord::Base end class ParanoidModelWithObservers < ParanoidModel def observers_notified @observers_notified ||= [] end def self.notify_observer(*args) observers_notified << args end end class ParanoidModelWithoutObservers < ParanoidModel self.class.send(remove_method :notify_observers) if method_defined?(:notify_observers) end # refer back to regression test for #118 class ParanoidModelWithHasOne < ParanoidModel has_one :paranoid_model_with_belong, :dependent => :destroy has_one :class_name_belong, :dependent => :destroy, :class_name => "ParanoidModelWithAnthorClassNameBelong" has_one :paranoid_model_with_foreign_key_belong, :dependent => :destroy, :foreign_key => "has_one_foreign_key_id" has_one :not_paranoid_model_with_belong, :dependent => :destroy end class ParanoidModelWithHasOneAndBuild < ActiveRecord::Base has_one :paranoid_model_with_build_belong, :dependent => :destroy validates :color, :presence => true after_validation :build_paranoid_model_with_build_belong, on: :create private def build_paranoid_model_with_build_belong super.tap { |child| child.name = "foo" } end end class ParanoidModelWithBuildBelong < ActiveRecord::Base acts_as_paranoid validates :name, :presence => true belongs_to :paranoid_model_with_has_one_and_build end class ParanoidModelWithBelong < ActiveRecord::Base acts_as_paranoid belongs_to :paranoid_model_with_has_one end class ParanoidModelWithAnthorClassNameBelong < ActiveRecord::Base acts_as_paranoid belongs_to :paranoid_model_with_has_one end class ParanoidModelWithForeignKeyBelong < ActiveRecord::Base acts_as_paranoid belongs_to :paranoid_model_with_has_one end class ParanoidModelWithTimestamp < ActiveRecord::Base belongs_to :parent_model acts_as_paranoid end class NotParanoidModelWithBelong < ActiveRecord::Base belongs_to :paranoid_model_with_has_one end class NotParanoidModelWithBelongsAndAssocationNotSoftDestroyedValidator < NotParanoidModelWithBelong belongs_to :parent_model validates :parent_model, association_not_soft_destroyed: true end class FlaggedModel < PlainModel acts_as_paranoid :flag_column => :is_deleted end class FlaggedModelWithCustomIndex < PlainModel acts_as_paranoid :flag_column => :is_deleted, :indexed_column => :is_deleted end class AsplodeModel < ActiveRecord::Base acts_as_paranoid before_destroy do |r| raise StandardError, 'ASPLODE!' end end class NoConnectionModel < ActiveRecord::Base end class PolymorphicModel < ActiveRecord::Base acts_as_paranoid belongs_to :parent, polymorphic: true end module Namespaced def self.table_name_prefix "namespaced_" end class ParanoidHasOne < ActiveRecord::Base acts_as_paranoid has_one :paranoid_belongs_to, dependent: :destroy end class ParanoidBelongsTo < ActiveRecord::Base acts_as_paranoid belongs_to :paranoid_has_one end end