Search code examples
ruby-on-railsrubyroutesurl-routing

How to verify controller actions are defined for all routes in a rails application?


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).


Solution

  • 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:

    • This is only a basic check for "does the controller action exist?". It does not consider parameters, format, subdomains, or any other route constraints. However, in my experience, that's good enough for the vast majority of scenarios.
    • This test only ensures that defined routes map to valid controller actions, not the converse. So it's still possible to have "dead code" in controllers, without a test failing. I haven't attempted to address that problem here.
    • It's possible for a route to have no controller action and still be valid!! This test can fail in such scenarios! As a workaround, you could - for example - define empty methods in the controller, instead of relying on the "magic" rails default behaviour. But the key takeaway here is: be careful when removing "dead" routes; you cannot immediately assume that a failing test here means the route is invalid.