【问题标题】:How to verify controller actions are defined for all routes in a rails application?如何验证为 Rails 应用程序中的所有路由定义了控制器操作?
【发布时间】:2019-03-24 06:28:36
【问题描述】:

有没有办法验证config/routes.rb 中定义并由rake routes 公开的所有控制器操作实际上对应于现有控制器操作?

例如,假设我们有以下路由文件:

Application.routes.draw do
  resources :foobar
end

还有以下控制器:

class FoobarsController < ApplicationController
  def index
    # ...
  end

  def show
    # ...
  end
end

我想用某种方式自动检测 createneweditupdatedestroy 操作(由路由隐式定义)未映射到有效的控制器操作 - 这样我就可以修复 routes.rb 文件:

Application.routes.draw do
  resources :foobar, only: [:index, :show]
end

如果愿意的话,对路线进行“完整性检查”。

这样的检查不一定是完美的;我可以轻松地手动验证任何误报。 (虽然“完美”检查是理想的,因为它可以包含在测试套件中!)

我的动机是防止不可靠的 API 请求引发 AbstractController::ActionNotFound 异常,因为无意中定义了额外的路由(在大型应用程序中)。

【问题讨论】:

  • 您也许可以创建一个路由测试/规范,遍历Rails.application.routes 并测试每个路由。

标签: ruby-on-rails ruby routes url-routing


【解决方案1】:

我很好奇,以下是我的尝试。它仍然不准确,因为它还没有匹配正确的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"},
# ]

我也有兴趣知道其他答案,或者是否有适当的方法来做到这一点。另外,如果有人知道我的代码有改进,请告诉我。谢谢:)

【讨论】:

  • 可以使用partition 稍微整理一下。这将跳过需要在循环结束时分配三个空白数组中的两个和 if / else 。不错的答案,+1 :)
  • @Pavan 谢谢!!虽然,我确信我的答案仍然是正确的,因为constraintsformatsubdomain(例如),尤其是format,因为respond_to 在控制器动作本身内!我被困在试图找到一种方法,而没有“评估”/运行和测试控制器动作,如果它实际上会正确调用respond_to :someformat。哈哈哈!有些路线也有“lambda”约束,很难正确评估和匹配,但如果我有时间,也许我可以进一步测试一下:)
  • 这是一个有趣的起点,但它当然可以通过一些调整来完成......从我最初的测试运行中,那些puts 的输出没有任何用处,它在遇到不存在的控制器。 (这实际上是一个非常有用的发现!)也许记录 controller_classaction_name 将是最简单/最有用的......
  • @Jay-ArPolidario - 它经常被用作valid_routes, invalid_routes = Rails.application.routes.routes.partition do |route|...。这样,您就可以在返回时获得真假结果。
  • @SRack 啊,你完全正确!我忘了你可以那样做。我会更新我的代码。谢谢! :)
【解决方案2】:

这建立在 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

但是我会质疑它是否真的可用于除了最微不足道的例子之外的任何东西。

【讨论】:

  • “我会质疑它是否真的可用于最微不足道的例子之外的任何东西” - 我将尝试一些复杂的应用程序,看看它对我来说有多可靠……但正如我原来所说,即使有一些误报,这些都可以手动验证! (也许添加到规范中的一些false_positives 列表中?)
  • 我真的不认为您可以以编程方式为任何实际路由生成测试。方法、格式、约束等的可能排列的数量是巨大的。
  • 目前,我只关注控制器动作是否存在。我不是在查看参数、格式、子域等。我怀疑这涵盖了绝大多数额外路线,而无需深入研究极端情况。
【解决方案3】:

非常感谢其他答案 - 请在下面查看。但这是我过去几年在多个项目中最终使用的,它对我很有帮助。因此,我将其自我标记为可见性的公认答案。

我将以下内容放在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 默认行为。但这里的关键要点是:小心删除“死”路线时;您不能立即假设此处的测试失败意味着该路线无效。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-06-19
    • 1970-01-01
    • 2011-05-09
    • 1970-01-01
    • 1970-01-01
    • 2011-12-03
    • 1970-01-01
    • 2012-08-01
    相关资源
    最近更新 更多