require "cases/helper" require "models/developer" require "models/computer" class CallbackDeveloper < ActiveRecord::Base self.table_name = "developers" class << self def callback_string(callback_method) "history << [#{callback_method.to_sym.inspect}, :string]" end def callback_proc(callback_method) Proc.new { |model| model.history << [callback_method, :proc] } end def define_callback_method(callback_method) define_method(callback_method) do history << [callback_method, :method] end send(callback_method, :"#{callback_method}") end def callback_object(callback_method) klass = Class.new klass.send(:define_method, callback_method) do |model| model.history << [callback_method, :object] end klass.new end end ActiveRecord::Callbacks::CALLBACKS.each do |callback_method| next if callback_method.to_s.start_with?("around_") define_callback_method(callback_method) ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method)) } send(callback_method, callback_proc(callback_method)) send(callback_method, callback_object(callback_method)) send(callback_method) { |model| model.history << [callback_method, :block] } end def history @history ||= [] end end class CallbackDeveloperWithFalseValidation < CallbackDeveloper before_validation proc { |model| model.history << [:before_validation, :returning_false]; false } before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } end class CallbackDeveloperWithHaltedValidation < CallbackDeveloper before_validation proc { |model| model.history << [:before_validation, :throwing_abort]; throw(:abort) } before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } end class ParentDeveloper < ActiveRecord::Base self.table_name = "developers" attr_accessor :after_save_called before_validation { |record| record.after_save_called = true } end class ChildDeveloper < ParentDeveloper end class ImmutableDeveloper < ActiveRecord::Base self.table_name = "developers" validates_inclusion_of :salary, in: 50000..200000 before_save :cancel before_destroy :cancel private def cancel false end end class DeveloperWithCanceledCallbacks < ActiveRecord::Base self.table_name = "developers" validates_inclusion_of :salary, in: 50000..200000 before_save :cancel before_destroy :cancel private def cancel throw(:abort) end end class OnCallbacksDeveloper < ActiveRecord::Base self.table_name = "developers" before_validation { history << :before_validation } before_validation(on: :create) { history << :before_validation_on_create } before_validation(on: :update) { history << :before_validation_on_update } validate do history << :validate end after_validation { history << :after_validation } after_validation(on: :create) { history << :after_validation_on_create } after_validation(on: :update) { history << :after_validation_on_update } def history @history ||= [] end end class ContextualCallbacksDeveloper < ActiveRecord::Base self.table_name = "developers" before_validation { history << :before_validation } before_validation :before_validation_on_create_and_update, on: [ :create, :update ] validate do history << :validate end after_validation { history << :after_validation } after_validation :after_validation_on_create_and_update, on: [ :create, :update ] def before_validation_on_create_and_update history << "before_validation_on_#{self.validation_context}".to_sym end def after_validation_on_create_and_update history << "after_validation_on_#{self.validation_context}".to_sym end def history @history ||= [] end end class CallbackCancellationDeveloper < ActiveRecord::Base self.table_name = "developers" attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy before_save { defined?(@cancel_before_save) ? !@cancel_before_save : false } before_create { !@cancel_before_create } before_update { !@cancel_before_update } before_destroy { !@cancel_before_destroy } after_save { @after_save_called = true } after_update { @after_update_called = true } after_create { @after_create_called = true } after_destroy { @after_destroy_called = true } end class CallbackHaltedDeveloper < ActiveRecord::Base self.table_name = "developers" attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy before_save { throw(:abort) if defined?(@cancel_before_save) } before_create { throw(:abort) if @cancel_before_create } before_update { throw(:abort) if @cancel_before_update } before_destroy { throw(:abort) if @cancel_before_destroy } after_save { @after_save_called = true } after_update { @after_update_called = true } after_create { @after_create_called = true } after_destroy { @after_destroy_called = true } end class CallbacksTest < ActiveRecord::TestCase fixtures :developers def test_initialize david = CallbackDeveloper.new assert_equal [ [ :after_initialize, :method ], [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], ], david.history end def test_find david = CallbackDeveloper.find(1) assert_equal [ [ :after_find, :method ], [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], ], david.history end def test_new_valid? david = CallbackDeveloper.new david.valid? assert_equal [ [ :after_initialize, :method ], [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], ], david.history end def test_existing_valid? david = CallbackDeveloper.find(1) david.valid? assert_equal [ [ :after_find, :method ], [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], ], david.history end def test_create david = CallbackDeveloper.create("name" => "David", "salary" => 1000000) assert_equal [ [ :after_initialize, :method ], [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], [ :before_save, :method ], [ :before_save, :string ], [ :before_save, :proc ], [ :before_save, :object ], [ :before_save, :block ], [ :before_create, :method ], [ :before_create, :string ], [ :before_create, :proc ], [ :before_create, :object ], [ :before_create, :block ], [ :after_create, :method ], [ :after_create, :string ], [ :after_create, :proc ], [ :after_create, :object ], [ :after_create, :block ], [ :after_save, :method ], [ :after_save, :string ], [ :after_save, :proc ], [ :after_save, :object ], [ :after_save, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end def test_validate_on_create david = OnCallbacksDeveloper.create("name" => "David", "salary" => 1000000) assert_equal [ :before_validation, :before_validation_on_create, :validate, :after_validation, :after_validation_on_create ], david.history end def test_validate_on_contextual_create david = ContextualCallbacksDeveloper.create("name" => "David", "salary" => 1000000) assert_equal [ :before_validation, :before_validation_on_create, :validate, :after_validation, :after_validation_on_create ], david.history end def test_update david = CallbackDeveloper.find(1) david.save assert_equal [ [ :after_find, :method ], [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], [ :before_save, :method ], [ :before_save, :string ], [ :before_save, :proc ], [ :before_save, :object ], [ :before_save, :block ], [ :before_update, :method ], [ :before_update, :string ], [ :before_update, :proc ], [ :before_update, :object ], [ :before_update, :block ], [ :after_update, :method ], [ :after_update, :string ], [ :after_update, :proc ], [ :after_update, :object ], [ :after_update, :block ], [ :after_save, :method ], [ :after_save, :string ], [ :after_save, :proc ], [ :after_save, :object ], [ :after_save, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end def test_validate_on_update david = OnCallbacksDeveloper.find(1) david.save assert_equal [ :before_validation, :before_validation_on_update, :validate, :after_validation, :after_validation_on_update ], david.history end def test_validate_on_contextual_update david = ContextualCallbacksDeveloper.find(1) david.save assert_equal [ :before_validation, :before_validation_on_update, :validate, :after_validation, :after_validation_on_update ], david.history end def test_destroy david = CallbackDeveloper.find(1) david.destroy assert_equal [ [ :after_find, :method ], [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_destroy, :method ], [ :before_destroy, :string ], [ :before_destroy, :proc ], [ :before_destroy, :object ], [ :before_destroy, :block ], [ :after_destroy, :method ], [ :after_destroy, :string ], [ :after_destroy, :proc ], [ :after_destroy, :object ], [ :after_destroy, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end def test_delete david = CallbackDeveloper.find(1) CallbackDeveloper.delete(david.id) assert_equal [ [ :after_find, :method ], [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], ], david.history end def test_deprecated_before_save_returning_false david = ImmutableDeveloper.find(1) assert_deprecated do assert david.valid? assert !david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } assert_equal david, exc.record assert_equal "Failed to save the record", exc.message end david = ImmutableDeveloper.find(1) david.salary = 10_000_000 assert !david.valid? assert !david.save assert_raise(ActiveRecord::RecordInvalid) { david.save! } someone = CallbackCancellationDeveloper.find(1) someone.cancel_before_save = true assert_deprecated do assert someone.valid? assert !someone.save end assert_save_callbacks_not_called(someone) end def test_deprecated_before_create_returning_false someone = CallbackCancellationDeveloper.new someone.cancel_before_create = true assert_deprecated do assert someone.valid? assert !someone.save end assert_save_callbacks_not_called(someone) end def test_deprecated_before_update_returning_false someone = CallbackCancellationDeveloper.find(1) someone.cancel_before_update = true assert_deprecated do assert someone.valid? assert !someone.save end assert_save_callbacks_not_called(someone) end def test_deprecated_before_destroy_returning_false david = ImmutableDeveloper.find(1) assert_deprecated do assert !david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_equal david, exc.record assert_equal "Failed to destroy the record", exc.message end assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackCancellationDeveloper.find(1) someone.cancel_before_destroy = true assert_deprecated do assert !someone.destroy assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } end assert !someone.after_destroy_called end def assert_save_callbacks_not_called(someone) assert !someone.after_save_called assert !someone.after_create_called assert !someone.after_update_called end private :assert_save_callbacks_not_called def test_before_create_throwing_abort someone = CallbackHaltedDeveloper.new someone.cancel_before_create = true assert someone.valid? assert !someone.save assert_save_callbacks_not_called(someone) end def test_before_save_throwing_abort david = DeveloperWithCanceledCallbacks.find(1) assert david.valid? assert !david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } assert_equal david, exc.record david = DeveloperWithCanceledCallbacks.find(1) david.salary = 10_000_000 assert !david.valid? assert !david.save assert_raise(ActiveRecord::RecordInvalid) { david.save! } someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_save = true assert someone.valid? assert !someone.save assert_save_callbacks_not_called(someone) end def test_before_update_throwing_abort someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_update = true assert someone.valid? assert !someone.save assert_save_callbacks_not_called(someone) end def test_before_destroy_throwing_abort david = DeveloperWithCanceledCallbacks.find(1) assert !david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_equal david, exc.record assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_destroy = true assert !someone.destroy assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } assert !someone.after_destroy_called end def test_callback_returning_false david = CallbackDeveloperWithFalseValidation.find(1) assert_deprecated { david.save } assert_equal [ [ :after_find, :method ], [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :before_validation, :returning_false ], [ :after_rollback, :block ], [ :after_rollback, :object ], [ :after_rollback, :proc ], [ :after_rollback, :string ], [ :after_rollback, :method ], ], david.history end def test_callback_throwing_abort david = CallbackDeveloperWithHaltedValidation.find(1) david.save assert_equal [ [ :after_find, :method ], [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :before_validation, :throwing_abort ], [ :after_rollback, :block ], [ :after_rollback, :object ], [ :after_rollback, :proc ], [ :after_rollback, :string ], [ :after_rollback, :method ], ], david.history end def test_inheritance_of_callbacks parent = ParentDeveloper.new assert !parent.after_save_called parent.save assert parent.after_save_called child = ChildDeveloper.new assert !child.after_save_called child.save assert child.after_save_called end end