如何验证为 rails 应用程序中的所有路由定义了控制器操作?
How to verify controller actions are defined for all routes in a rails application?
有没有办法验证在 config/routes.rb
中定义并由 rake routes
公开的所有控制器操作是否确实对应于现有的控制器操作?
例如,假设我们有以下路由文件:
Application.routes.draw do
resources :foobar
end
以及以下控制器:
class FoobarsController < ApplicationController
def index
# ...
end
def show
# ...
end
end
我想要一些方法来自动检测 create
、new
、edit
、update
和 destroy
操作(由路由隐式定义)未映射到有效的控制器操作 - 因此我可以修复 routes.rb
文件:
Application.routes.draw do
resources :foobar, only: [:index, :show]
end
一条 "integrity check" 路线,如果你愿意的话。
这样的检查不一定是完美的;我可以轻松地手动验证任何误报。 (虽然 "perfect" 检查是理想的,因为它可以包含在测试套件中!)
我的动机是防止 AbstractController::ActionNotFound
不可靠的 API 请求引发异常,因为无意中定义了额外的路由(在大型应用程序中)。
我很好奇,下面是我的尝试。它仍然不准确,因为它还没有匹配正确的 format
。此外,某些路线有限制;我的代码还没有考虑。
rails console
:
todo_skipped_routes = []
valid_routes = []
invalid_routes = []
Rails.application.routes.routes.each do |route|
controller_route_name = route.defaults[:controller]
action_route_name = route.defaults[:action]
if controller_route_name.blank? || action_route_name.blank?
todo_skipped_routes << route
next
end
# TODO: maybe Rails already has a "proper" way / method to constantize this
# copied over @max answer, because I forgot to consider namespacing
controller_class = "#{controller_route_name.sub('\/', '::')}_controller".camelcase.safe_constantize
is_route_valid = !controller_class.nil? && controller_class.instance_methods(false).include?(action_route_name.to_sym)
# TODO: check also if "format" matches / gonna be "responded to" properly by the controller-action
# check also "lambda" constraints, and `request.SOMEMETHOD` constraints (i.e. `subdomain`, `remote_ip`, `host`, ...)
if is_route_valid
valid_routes << route
else
invalid_routes << route
end
end
puts valid_routes
puts invalid_routes
# puts "friendlier" version
pp invalid_routes.map(&:defaults)
# => [
# {:controller=>"sessions", :action=>"somenonexistingaction"},
# {:controller=>"posts", :action=>"criate"},
# {:controller=>"yoosers", :action=>"create"},
# ]
我也有兴趣知道其他答案,或者是否有适当的方法来做到这一点。另外,如果有人知道我的代码有什么改进之处,请告诉我。谢谢:)
这基于 Jay-Ar Polidario 的回答:
require 'test_helper'
class RoutesTest < ActionDispatch::IntegrationTest
Rails.application.routes.routes.each do |route|
controller, action = route.defaults.slice(:controller, :action).values
# Some routes may have the controller assigned as a dynamic segment
# We need to skip them since we can't really test them in this way
next if controller.nil?
# Skip the built in Rails 5 active_storage routes
next if 'active_storage' == controller.split('/').first
# Naive attempt to resolve the controller constant from the name
# Replacing / with :: is for namespaces
ctrl_name = "#{controller.sub('\/', '::')}_controller".camelcase
ctrl = ctrl_name.safe_constantize
# tagging SecureRandom.uuid on the end is a hack to ensure that each
# test name is unique
test "#{ctrl_name} controller exists - #{SecureRandom.uuid}" do
assert ctrl, "Controller #{ctrl_name} is not defined for #{route.name}"
end
test "#{controller} has the action #{action} - #{SecureRandom.uuid}" do
assert ctrl.respond_to?(action),
"#{ctrl_name} does not have the action '#{action}' - #{route.name}"
end if ctrl
end
end
但是我会质疑它是否真的可以用于最微不足道的例子之外的任何事情。
其他答案的巨大功劳 - 请在下面查看它们。但这是我过去几年在多个项目中最终使用的,它对我很有帮助。所以我self-marking这是公认的可见性答案。
我在 spec/routes/integrity_check_spec.rb
中放置了以下内容:
require 'rails_helper'
RSpec.describe 'Integrity Check of Routes', order: :defined do # rubocop:disable RSpec/DescribeClass
Rails.application.routes.routes.sort_by { |r| r.defaults[:controller].to_s }.each do |route|
controller, action = route.defaults.slice(:controller, :action).values
# Some routes may have the controller assigned as a dynamic segment
# We need to skip them since we can't really test them in this way
next if controller.nil?
# Skip the built in Rails 5 active_storage routes
next if controller.split('/').first == 'active_storage'
# Skip built in Rails 6 action_mailbox routes
next if controller == 'rails/conductor/action_mailbox/inbound_emails'
ctrl_name = "#{controller.sub('\/', '::')}_controller".camelcase
ctrl_klass = ctrl_name.safe_constantize
it "#{ctrl_name} is defined and has corresponding action: #{action}, for #{route.name || '(no route name)'}" do
expect(ctrl_klass).to be_present
expect(ctrl_klass.new).to respond_to(action)
end
end
end
注意事项:
- 这只是对“控制器操作是否存在?”的基本检查。它不考虑参数、格式、子域或任何其他路由约束。但是,根据我的经验,这对于绝大多数情况来说已经足够了。
- 此测试仅确保定义的路由映射到有效的控制器操作,而非相反。因此,在没有测试失败的情况下,控制器中仍有可能存在“死代码”。我没有尝试在这里解决这个问题。
- 可能一条路线有no controller action and still be valid!!在这种情况下,此测试可能会失败!作为一种解决方法,您可以 - 例如 - 在控制器中定义空方法,而不是依赖于“魔术” rails 默认行为。但这里的关键要点是:删除“死”路由时要小心;您不能立即假设此处的失败测试意味着路由无效。
有没有办法验证在 config/routes.rb
中定义并由 rake routes
公开的所有控制器操作是否确实对应于现有的控制器操作?
例如,假设我们有以下路由文件:
Application.routes.draw do
resources :foobar
end
以及以下控制器:
class FoobarsController < ApplicationController
def index
# ...
end
def show
# ...
end
end
我想要一些方法来自动检测 create
、new
、edit
、update
和 destroy
操作(由路由隐式定义)未映射到有效的控制器操作 - 因此我可以修复 routes.rb
文件:
Application.routes.draw do
resources :foobar, only: [:index, :show]
end
一条 "integrity check" 路线,如果你愿意的话。
这样的检查不一定是完美的;我可以轻松地手动验证任何误报。 (虽然 "perfect" 检查是理想的,因为它可以包含在测试套件中!)
我的动机是防止 AbstractController::ActionNotFound
不可靠的 API 请求引发异常,因为无意中定义了额外的路由(在大型应用程序中)。
我很好奇,下面是我的尝试。它仍然不准确,因为它还没有匹配正确的 format
。此外,某些路线有限制;我的代码还没有考虑。
rails console
:
todo_skipped_routes = []
valid_routes = []
invalid_routes = []
Rails.application.routes.routes.each do |route|
controller_route_name = route.defaults[:controller]
action_route_name = route.defaults[:action]
if controller_route_name.blank? || action_route_name.blank?
todo_skipped_routes << route
next
end
# TODO: maybe Rails already has a "proper" way / method to constantize this
# copied over @max answer, because I forgot to consider namespacing
controller_class = "#{controller_route_name.sub('\/', '::')}_controller".camelcase.safe_constantize
is_route_valid = !controller_class.nil? && controller_class.instance_methods(false).include?(action_route_name.to_sym)
# TODO: check also if "format" matches / gonna be "responded to" properly by the controller-action
# check also "lambda" constraints, and `request.SOMEMETHOD` constraints (i.e. `subdomain`, `remote_ip`, `host`, ...)
if is_route_valid
valid_routes << route
else
invalid_routes << route
end
end
puts valid_routes
puts invalid_routes
# puts "friendlier" version
pp invalid_routes.map(&:defaults)
# => [
# {:controller=>"sessions", :action=>"somenonexistingaction"},
# {:controller=>"posts", :action=>"criate"},
# {:controller=>"yoosers", :action=>"create"},
# ]
我也有兴趣知道其他答案,或者是否有适当的方法来做到这一点。另外,如果有人知道我的代码有什么改进之处,请告诉我。谢谢:)
这基于 Jay-Ar Polidario 的回答:
require 'test_helper'
class RoutesTest < ActionDispatch::IntegrationTest
Rails.application.routes.routes.each do |route|
controller, action = route.defaults.slice(:controller, :action).values
# Some routes may have the controller assigned as a dynamic segment
# We need to skip them since we can't really test them in this way
next if controller.nil?
# Skip the built in Rails 5 active_storage routes
next if 'active_storage' == controller.split('/').first
# Naive attempt to resolve the controller constant from the name
# Replacing / with :: is for namespaces
ctrl_name = "#{controller.sub('\/', '::')}_controller".camelcase
ctrl = ctrl_name.safe_constantize
# tagging SecureRandom.uuid on the end is a hack to ensure that each
# test name is unique
test "#{ctrl_name} controller exists - #{SecureRandom.uuid}" do
assert ctrl, "Controller #{ctrl_name} is not defined for #{route.name}"
end
test "#{controller} has the action #{action} - #{SecureRandom.uuid}" do
assert ctrl.respond_to?(action),
"#{ctrl_name} does not have the action '#{action}' - #{route.name}"
end if ctrl
end
end
但是我会质疑它是否真的可以用于最微不足道的例子之外的任何事情。
其他答案的巨大功劳 - 请在下面查看它们。但这是我过去几年在多个项目中最终使用的,它对我很有帮助。所以我self-marking这是公认的可见性答案。
我在 spec/routes/integrity_check_spec.rb
中放置了以下内容:
require 'rails_helper'
RSpec.describe 'Integrity Check of Routes', order: :defined do # rubocop:disable RSpec/DescribeClass
Rails.application.routes.routes.sort_by { |r| r.defaults[:controller].to_s }.each do |route|
controller, action = route.defaults.slice(:controller, :action).values
# Some routes may have the controller assigned as a dynamic segment
# We need to skip them since we can't really test them in this way
next if controller.nil?
# Skip the built in Rails 5 active_storage routes
next if controller.split('/').first == 'active_storage'
# Skip built in Rails 6 action_mailbox routes
next if controller == 'rails/conductor/action_mailbox/inbound_emails'
ctrl_name = "#{controller.sub('\/', '::')}_controller".camelcase
ctrl_klass = ctrl_name.safe_constantize
it "#{ctrl_name} is defined and has corresponding action: #{action}, for #{route.name || '(no route name)'}" do
expect(ctrl_klass).to be_present
expect(ctrl_klass.new).to respond_to(action)
end
end
end
注意事项:
- 这只是对“控制器操作是否存在?”的基本检查。它不考虑参数、格式、子域或任何其他路由约束。但是,根据我的经验,这对于绝大多数情况来说已经足够了。
- 此测试仅确保定义的路由映射到有效的控制器操作,而非相反。因此,在没有测试失败的情况下,控制器中仍有可能存在“死代码”。我没有尝试在这里解决这个问题。
- 可能一条路线有no controller action and still be valid!!在这种情况下,此测试可能会失败!作为一种解决方法,您可以 - 例如 - 在控制器中定义空方法,而不是依赖于“魔术” rails 默认行为。但这里的关键要点是:删除“死”路由时要小心;您不能立即假设此处的失败测试意味着路由无效。