Skip to content

Instantly share code, notes, and snippets.

@fractaledmind
Created October 31, 2024 11:26
Show Gist options
  • Save fractaledmind/410e519ccd51445cc10c3408b5f24d77 to your computer and use it in GitHub Desktop.
Save fractaledmind/410e519ccd51445cc10c3408b5f24d77 to your computer and use it in GitHub Desktop.
Test that ensures you have no controllers with public methods that do not have a corresponding route defined.
class RoutingTest < ActionDispatch::IntegrationTest
IGNORED_CONTROLLERS = Set[
"Rails::MailersController"
]
test "no unrouted actions (public controller methods)" do
actions_by_controller.each do |controller_path, actions|
controller_name = "#{controller_path.camelize}Controller"
next if IGNORED_CONTROLLERS.include?(controller_name)
controller = Object.const_get(controller_name)
public_methods = controller.public_instance_methods(_include_super = false).map(&:to_s)
unrouted_actions = public_methods - actions
assert_empty(
unrouted_actions,
"#{controller_name} has unrouted actions (public methods). These should probably be private"
)
end
end
private
def actions_by_controller
{}.tap do |controllers|
@routes.routes.each do |route|
controller = route.requirements[:controller]
action = route.requirements[:action]
next unless controller && action
(controllers[controller] ||= []) << action
end
end
end
end
@yukideluxe
Copy link

yukideluxe commented Oct 31, 2024

As soon as the assertion for one controller fails the test ends! I made a small tweak to get a result for all controllers in one go. Thank you, I have a little cleanup to do 😳 🙏

require "test_helper"

class RoutingTest < ActionDispatch::IntegrationTest
  IGNORED_CONTROLLERS = Set[
    "Rails::MailersController"
  ]

  test "no unrouted actions (public controller methods)" do
    unrouted_actions_by_controller = {}

    actions_by_controller.each do |controller_path, actions|
      controller_name = "#{controller_path.camelize}Controller"
      next if IGNORED_CONTROLLERS.include?(controller_name)

      controller = Object.const_get(controller_name)
      public_methods = controller.public_instance_methods(_include_super = false).map(&:to_s)

      unrouted_actions = public_methods - actions

      if unrouted_actions.present?
        unrouted_actions_by_controller[controller_path] = unrouted_actions
      end
    end

    assert_empty(
      unrouted_actions_by_controller,
      "there are unrouted actions (public methods) for these controllers. These should probably be private",
    )
  end

  private

  def actions_by_controller
    {}.tap do |controllers|
      @routes.routes.each do |route|
        controller = route.requirements[:controller]
        action = route.requirements[:action]

        next unless controller && action

        (controllers[controller] ||= []) << action
      end
    end
  end
end

@dennispaagman
Copy link

dennispaagman commented Oct 31, 2024

One nice addition is to list the actual methods in the message:

edit: not necessary with minitest, see https://bsky.app/profile/fractaledmind.bsky.social/post/3l7susvs6rk2v

      assert_empty(
        unrouted_actions, 
-       "#{controller_name} has unrouted actions (public methods). These should probably be private"
+       "#{controller_name} has unrouted actions (public methods): #{unrouted_actions.map(&:to_sym)}. These should probably be private"
      )

@dennispaagman
Copy link

And here's an RSpec version if anyone is looking for one (with my improvement from above and :aggregate_failures to find all cases immediately).

RSpec.describe "Routing" do
  IGNORED_CONTROLLERS = Set[
    "Rails::MailersController"
  ]

  it "has no unrouted actions (public controller methods)", :aggregate_failures do
    actions_by_controller.each do |controller_path, actions|
      controller_name = "#{controller_path.camelize}Controller"
      next if IGNORED_CONTROLLERS.include?(controller_name)

      controller = Object.const_get(controller_name)
      public_methods = controller.public_instance_methods(_include_super = false).map(&:to_s)
      unrouted_actions = public_methods - actions

      expect(unrouted_actions).to be_empty,
        "#{controller_name} has unrouted actions (public methods): #{unrouted_actions.map(&:to_sym)}. These should probably be private"
    end
  end

private

  def actions_by_controller
    {}.tap do |controllers|
      Rails.application.routes.routes.each do |route|
        controller = route.requirements[:controller]
        action = route.requirements[:action]
        next unless controller && action

        (controllers[controller] ||= []) << action
      end
    end
  end
end

@hmaddocks
Copy link

There is a gem that does this. It all so does the inverse, checks for defined routes that don’t have corresponding methods.

https://github.com/amatsuda/traceroute

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment