Is there a way to verify that all controller actions, as defined in config/routes.rb
and exposed by rake routes
, actually correspond to an existing controller action?
For example, suppose we have the following routes file:
Application.routes.draw do
resources :foobar
end
And the following controller:
class FoobarsController < ApplicationController
def index
# ...
end
def show
# ...
end
end
I'd like to have some way of auto-detecting that the create
, new
, edit
, update
and destroy
actions (as implicitly defined by the routes) are not mapped to a valid controller action - so that I can fix the routes.rb
file:
Application.routes.draw do
resources :foobar, only: [:index, :show]
end
An "integrity check" of the routes, if you will.
Such a check wouldn't necessarily need to be perfect; I could easily verify any false positives manually. (Although a "perfect" check would be ideal, as it could be included in the test suite!)
My motivation here is to prevent AbstractController::ActionNotFound
exceptions from being raised by dodgy API requests, as additional routes were inadvertently defined (in a large application).
Huge credit to the other answers - please check them out below. But this is what I've ended up using on multiple projects over the past couple of years, and it's served me well. So I'm self-marking this as the accepted answer for visibility.
I placed the following in 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
Caveats: