diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 6ada987bdc..e16e5cded3 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -33,14 +33,26 @@ module AbstractController define_callbacks :process_action, terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? }, skip_after_callbacks_if_terminated: true + mattr_accessor :raise_on_missing_callback_actions, default: false end class ActionFilter # :nodoc: - def initialize(actions) + def initialize(filters, conditional_key, actions) + @filters = filters.to_a + @conditional_key = conditional_key @actions = Array(actions).map(&:to_s).to_set end def match?(controller) + if controller.raise_on_missing_callback_actions + missing_action = @actions.find { |action| !controller.available_action?(action) } + if missing_action + filter_names = @filters.length == 1 ? @filters.first.inspect : @filters.inspect + message = "The #{missing_action} action could not be found for the #{filter_names} callback on #{controller.class.name}, but it is listed in its #{@conditional_key.inspect} option" + raise ActionNotFound.new(message, controller, missing_action) + end + end + @actions.include?(controller.action_name) end @@ -75,9 +87,10 @@ module AbstractController end def _normalize_callback_option(options, from, to) # :nodoc: - if from = options.delete(from) - from = ActionFilter.new(from) - options[to] = Array(options[to]).unshift(from) + if from_value = options.delete(from) + filters = options[:filters] + from_value = ActionFilter.new(filters, from, from_value) + options[to] = Array(options[to]).unshift(from_value) end end @@ -95,8 +108,10 @@ module AbstractController # * options - A hash of options to be used when adding the callback. def _insert_callbacks(callbacks, block = nil) options = callbacks.extract_options! - _normalize_callback_options(options) callbacks.push(block) if block + options[:filters] = callbacks + _normalize_callback_options(options) + options.delete(:filters) callbacks.each do |callback| yield callback, options end diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index e64f580fb2..88f333cf8a 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -308,5 +308,190 @@ module AbstractController assert_equal "Hello world Howdy!", controller.response_body end end + + class TestCallbacksWithMissingConditions < ActiveSupport::TestCase + class CallbacksWithMissingOnly < ControllerWithCallbacks + before_action :callback, only: :showw + + def index + end + + def show + end + + private + def callback + end + end + + test "callbacks raise exception when their 'only' condition is a missing action" do + with_raise_on_missing_callback_actions do + controller = CallbacksWithMissingOnly.new + assert_raises(AbstractController::ActionNotFound) do + controller.process(:index) + end + end + end + + class CallbacksWithMissingOnlyInArray < ControllerWithCallbacks + before_action :callback, only: [:index, :showw] + + def index + end + + def show + end + + private + def callback + end + end + + test "callbacks raise exception when their 'only' array condition contains a missing action" do + with_raise_on_missing_callback_actions do + controller = CallbacksWithMissingOnlyInArray.new + assert_raises(AbstractController::ActionNotFound) do + controller.process(:index) + end + end + end + + class CallbacksWithMissingExcept < ControllerWithCallbacks + before_action :callback, except: :showw + + def index + end + + def show + end + + private + def callback + end + end + + test "callbacks raise exception when their 'except' condition is a missing action" do + with_raise_on_missing_callback_actions do + controller = CallbacksWithMissingExcept.new + assert_raises(AbstractController::ActionNotFound) do + controller.process(:index) + end + end + end + + class CallbacksWithMissingExceptInArray < ControllerWithCallbacks + before_action :callback, except: [:index, :showw] + + def index + end + + def show + end + + private + def callback + end + end + + test "callbacks raise exception when their 'except' array condition contains a missing action" do + with_raise_on_missing_callback_actions do + controller = CallbacksWithMissingExceptInArray.new + assert_raises(AbstractController::ActionNotFound) do + controller.process(:index) + end + end + end + + class MultipleCallbacksWithMissingOnly < ControllerWithCallbacks + before_action :callback1, :callback2, ->() { }, only: :showw + + def index + end + + def show + end + + private + def callback1 + end + + def callback2 + end + end + + test "raised exception message includes the names of callback actions and missing conditional action" do + with_raise_on_missing_callback_actions do + controller = MultipleCallbacksWithMissingOnly.new + error = assert_raises(AbstractController::ActionNotFound) do + controller.process(:index) + end + + assert_includes error.message, ":callback1" + assert_includes error.message, ":callback2" + assert_includes error.message, "#