Skip to content

Commit b4ea44f

Browse files
authored
Merge pull request rails#51464 from joshuay03/raise-error-on-invalid-resources-on-or-except-args
[Fix rails#51463] Raise an error when invalid `:on` or `:except` options are given to `#resource` or `#resources`
2 parents 4f68f3e + c7a547f commit b4ea44f

File tree

4 files changed

+106
-20
lines changed

4 files changed

+106
-20
lines changed

actionpack/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Raise an `ArgumentError` when invalid `:on` or `:except` options are passed into `#resource` and `#resources`.
2+
3+
*Joshua Young*
4+
15
## Rails 8.0.0.beta1 (September 26, 2024) ##
26

37
* Fix non-GET requests not updating cookies in `ActionController::TestCase`.

actionpack/lib/action_dispatch/routing/mapper.rb

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,13 +1162,28 @@ module Resources
11621162
CANONICAL_ACTIONS = %w(index create new show update destroy)
11631163

11641164
class Resource # :nodoc:
1165+
class << self
1166+
def default_actions(api_only)
1167+
if api_only
1168+
[:index, :create, :show, :update, :destroy]
1169+
else
1170+
[:index, :create, :new, :show, :update, :destroy, :edit]
1171+
end
1172+
end
1173+
end
1174+
11651175
attr_reader :controller, :path, :param
11661176

11671177
def initialize(entities, api_only, shallow, options = {})
11681178
if options[:param].to_s.include?(":")
11691179
raise ArgumentError, ":param option can't contain colons"
11701180
end
11711181

1182+
valid_actions = self.class.default_actions(false) # ignore api_only for this validation
1183+
if invalid_actions = invalid_only_except_options(options, valid_actions).presence
1184+
raise ArgumentError, ":only and :except must include only #{valid_actions}, but also included #{invalid_actions}"
1185+
end
1186+
11721187
@name = entities.to_s
11731188
@path = (options[:path] || @name).to_s
11741189
@controller = (options[:controller] || @name).to_s
@@ -1182,11 +1197,7 @@ def initialize(entities, api_only, shallow, options = {})
11821197
end
11831198

11841199
def default_actions
1185-
if @api_only
1186-
[:index, :create, :show, :update, :destroy]
1187-
else
1188-
[:index, :create, :new, :show, :update, :destroy, :edit]
1189-
end
1200+
self.class.default_actions(@api_only)
11901201
end
11911202

11921203
def actions
@@ -1254,9 +1265,24 @@ def shallow?
12541265
end
12551266

12561267
def singleton?; false; end
1268+
1269+
private
1270+
def invalid_only_except_options(options, valid_actions)
1271+
options.values_at(:only, :except).flatten.compact.uniq - valid_actions
1272+
end
12571273
end
12581274

12591275
class SingletonResource < Resource # :nodoc:
1276+
class << self
1277+
def default_actions(api_only)
1278+
if api_only
1279+
[:show, :create, :update, :destroy]
1280+
else
1281+
[:show, :create, :update, :destroy, :new, :edit]
1282+
end
1283+
end
1284+
end
1285+
12601286
def initialize(entities, api_only, shallow, options)
12611287
super
12621288
@as = nil
@@ -1265,11 +1291,7 @@ def initialize(entities, api_only, shallow, options)
12651291
end
12661292

12671293
def default_actions
1268-
if @api_only
1269-
[:show, :create, :update, :destroy]
1270-
else
1271-
[:show, :create, :update, :destroy, :new, :edit]
1272-
end
1294+
self.class.default_actions(@api_only)
12731295
end
12741296

12751297
def plural
@@ -1330,7 +1352,7 @@ def resource(*resources, &block)
13301352
end
13311353

13321354
with_scope_level(:resource) do
1333-
options = apply_action_options options
1355+
options = apply_action_options :resource, options
13341356
resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
13351357
yield if block_given?
13361358

@@ -1500,7 +1522,7 @@ def resources(*resources, &block)
15001522
end
15011523

15021524
with_scope_level(:resources) do
1503-
options = apply_action_options options
1525+
options = apply_action_options :resources, options
15041526
resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
15051527
yield if block_given?
15061528

@@ -1769,17 +1791,32 @@ def apply_common_behavior_for(method, resources, options, &block)
17691791
false
17701792
end
17711793

1772-
def apply_action_options(options)
1794+
def apply_action_options(method, options)
17731795
return options if action_options? options
1774-
options.merge scope_action_options
1796+
options.merge scope_action_options(method)
17751797
end
17761798

17771799
def action_options?(options)
17781800
options[:only] || options[:except]
17791801
end
17801802

1781-
def scope_action_options
1782-
@scope[:action_options] || {}
1803+
def scope_action_options(method)
1804+
return {} unless @scope[:action_options]
1805+
1806+
actions = applicable_actions_for(method)
1807+
@scope[:action_options].dup.tap do |options|
1808+
(options[:only] = Array(options[:only]) & actions) if options[:only]
1809+
(options[:except] = Array(options[:except]) & actions) if options[:except]
1810+
end
1811+
end
1812+
1813+
def applicable_actions_for(method)
1814+
case method
1815+
when :resource
1816+
SingletonResource.default_actions(api_only?)
1817+
when :resources
1818+
Resource.default_actions(api_only?)
1819+
end
17831820
end
17841821

17851822
def resource_scope?

actionpack/lib/action_dispatch/testing/assertions/routing.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ def setup # :nodoc:
118118
# assert_equal "/users", users_path
119119
# end
120120
#
121-
def with_routing(&block)
121+
def with_routing(config = nil, &block)
122122
old_routes, old_controller = @routes, @controller
123-
create_routes(&block)
123+
create_routes(config, &block)
124124
ensure
125125
reset_routes(old_routes, old_controller)
126126
end
@@ -267,8 +267,8 @@ def method_missing(selector, ...)
267267
end
268268

269269
private
270-
def create_routes
271-
@routes = ActionDispatch::Routing::RouteSet.new
270+
def create_routes(config = nil)
271+
@routes = ActionDispatch::Routing::RouteSet.new(config || ActionDispatch::Routing::RouteSet::DEFAULT_CONFIG)
272272
if @controller
273273
@controller = @controller.clone
274274
_routes = @routes

actionpack/test/controller/resources_test.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,51 @@ def test_singleton_resource_name_is_not_singularized
11081108
end
11091109
end
11101110

1111+
def test_invalid_only_option_for_resources
1112+
expected_message = ":only and :except must include only [:index, :create, :new, :show, :update, :destroy, :edit], but also included [:foo, :bar]"
1113+
assert_raise(ArgumentError, match: expected_message) do
1114+
with_routing do |set|
1115+
set.draw do
1116+
resources :products, only: [:foo, :bar]
1117+
end
1118+
end
1119+
end
1120+
end
1121+
1122+
def test_invalid_only_option_for_singleton_resource
1123+
expected_message = ":only and :except must include only [:show, :create, :update, :destroy, :new, :edit], but also included [:foo, :bar]"
1124+
assert_raise(ArgumentError, match: expected_message) do
1125+
with_routing do |set|
1126+
set.draw do
1127+
resource :products, only: [:foo, :bar]
1128+
end
1129+
end
1130+
end
1131+
end
1132+
1133+
def test_invalid_except_option_for_resources
1134+
expected_message = ":only and :except must include only [:index, :create, :new, :show, :update, :destroy, :edit], but also included [:foo]"
1135+
1136+
assert_raise(ArgumentError, match: expected_message) do
1137+
with_routing do |set|
1138+
set.draw do
1139+
resources :products, except: :foo
1140+
end
1141+
end
1142+
end
1143+
end
1144+
1145+
def test_invalid_except_option_for_singleton_resource
1146+
expected_message = ":only and :except must include only [:show, :create, :update, :destroy, :new, :edit], but also included [:foo]"
1147+
assert_raise(ArgumentError, match: expected_message) do
1148+
with_routing do |set|
1149+
set.draw do
1150+
resource :products, except: :foo
1151+
end
1152+
end
1153+
end
1154+
end
1155+
11111156
private
11121157
def with_restful_routing(*args)
11131158
options = args.extract_options!

0 commit comments

Comments
 (0)