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, "#