module Shoulda module Matchers module ActionController # The `route` matcher tests that a route resolves to a controller, # action, and params; and that the controller, action, and params # generates the same route. For an RSpec suite, this is like using a # combination of `route_to` and `be_routable`. In a test suite using # Minitest + Shoulda, it provides a more expressive syntax over # `assert_routing`. # # You can use this matcher either in a controller test case or in a # routing test case. For instance, given these routes: # # My::Application.routes.draw do # get '/posts', to: 'posts#index' # get '/posts/:id', to: 'posts#show' # end # # You could choose to write tests for these routes alongside other tests # for PostsController: # # class PostsController < ApplicationController # # ... # end # # # RSpec # RSpec.describe PostsController, type: :controller do # it { should route(:get, '/posts').to(action: :index) } # it { should route(:get, '/posts/1').to(action: :show, id: 1) } # end # # # Minitest (Shoulda) # class PostsControllerTest < ActionController::TestCase # should route(:get, '/posts').to(action: 'index') # should route(:get, '/posts/1').to(action: :show, id: 1) # end # # Or you could place the tests along with other route tests: # # # RSpec # describe 'Routing', type: :routing do # it do # should route(:get, '/posts'). # to(controller: :posts, action: :index) # end # # it do # should route(:get, '/posts/1'). # to('posts#show', id: 1) # end # end # # # Minitest (Shoulda) # class RoutesTest < ActionController::IntegrationTest # should route(:get, '/posts'). # to(controller: :posts, action: :index) # # should route(:get, '/posts/1'). # to('posts#show', id: 1) # end # # Notice that in the former case, as we are inside of a test case for # PostsController, we do not have to specify that the routes resolve to # this controller. In the latter case we specify this using the # `controller` key passed to the `to` qualifier. # # #### Specifying a port # # If the route you're testing has a constraint on it that limits the route # to a particular port, you can specify it by passing a `port` option to # the matcher: # # class PortConstraint # def initialize(port) # @port = port # end # # def matches?(request) # request.port == @port # end # end # # My::Application.routes.draw do # get '/posts', # to: 'posts#index', # constraints: PortConstraint.new(12345) # end # # # RSpec # describe 'Routing', type: :routing do # it do # should route(:get, '/posts', port: 12345). # to('posts#index') # end # end # # # Minitest (Shoulda) # class RoutesTest < ActionController::IntegrationTest # should route(:get, '/posts', port: 12345). # to('posts#index') # end # # #### Qualifiers # # ##### to # # Use `to` to specify the action (along with the controller, if needed) # that the route resolves to. # # `to` takes either keyword arguments (`controller` and `action`) or a # string that represents the controller/action pair: # # route(:get, '/posts').to(action: index) # route(:get, '/posts').to(controller: :posts, action: index) # route(:get, '/posts').to('posts#index') # # If there are parameters in your route, then specify those too: # # route(:get, '/posts/1').to('posts#show', id: 1) # # You may also specify special parameters such as `:format`: # # route(:get, '/posts').to('posts#index', format: :json) # # @return [RouteMatcher] # def route(method, path, port: nil) RouteMatcher.new(self, method, path, port: port) end # @private class RouteMatcher def initialize(context, method, path, port: nil) @context = context @method = method @path = add_port_to_path(normalize_path(path), port) @params = {} end attr_reader :failure_message def to(*args) @params = RouteParams.new(args).normalize self end def in_context(context) @context = context self end def matches?(controller) guess_controller_if_necessary(controller) route_recognized? end def description "route #{method.to_s.upcase} #{path} to/from #{params.inspect}" end def failure_message_when_negated "Didn't expect to #{description}" end private attr_reader :context, :method, :path, :params def normalize_path(path) if path.start_with?('/') path else "/#{path}" end end def add_port_to_path(path, port) if port "https://example.com:#{port}" + path else path end end def guess_controller_if_necessary(controller) params[:controller] ||= controller.controller_path end def route_recognized? context.send( :assert_routing, { method: method, path: path }, params, ) true rescue ::ActionController::RoutingError => e @failure_message = e.message false rescue Shoulda::Matchers.assertion_exception_class => e @failure_message = e.message false end end end end end