lib/rspec/rails/matchers/routing_matchers.rb
module RSpec
module Rails
module Matchers
# Matchers to help with specs for routing code.
module RoutingMatchers
extend RSpec::Matchers::DSL
# @private
class RouteToMatcher < RSpec::Rails::Matchers::BaseMatcher
def initialize(scope, *expected)
@scope = scope
@expected = expected[1] || {}
if Hash === expected[0]
@expected.merge!(expected[0])
else
controller, action = expected[0].split('#')
@expected.merge!(controller: controller, action: action)
end
end
def matches?(verb_to_path_map)
@actual = verb_to_path_map
# assert_recognizes does not consider ActionController::RoutingError an
# assertion failure, so we have to capture that and Assertion here.
match_unless_raises ActiveSupport::TestCase::Assertion, ActionController::RoutingError do
path, query = *verb_to_path_map.values.first.split('?')
@scope.assert_recognizes(
@expected,
{method: verb_to_path_map.keys.first, path: path},
Rack::Utils.parse_nested_query(query)
)
end
end
def failure_message
rescued_exception.message
end
def failure_message_when_negated
"expected #{@actual.inspect} not to route to #{@expected.inspect}"
end
def description
"route #{@actual.inspect} to #{@expected.inspect}"
end
end
# Delegates to `assert_recognizes`. Supports short-hand controller/action
# declarations (e.g. `"controller#action"`).
#
# @example
#
# expect(get: "/things/special").to route_to(
# controller: "things",
# action: "special"
# )
#
# expect(get: "/things/special").to route_to("things#special")
#
# @see https://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_recognizes
def route_to(*expected)
RouteToMatcher.new(self, *expected)
end
# @private
class BeRoutableMatcher < RSpec::Rails::Matchers::BaseMatcher
def initialize(scope)
@scope = scope
end
def matches?(path)
@actual = path
match_unless_raises ActionController::RoutingError do
@routing_options = @scope.routes.recognize_path(
path.values.first, method: path.keys.first
)
end
end
def failure_message
"expected #{@actual.inspect} to be routable"
end
def failure_message_when_negated
"expected #{@actual.inspect} not to be routable, but it routes to #{@routing_options.inspect}"
end
def description
"be routable"
end
end
# Passes if the route expression is recognized by the Rails router based on
# the declarations in `config/routes.rb`. Delegates to
# `RouteSet#recognize_path`.
#
# @example You can use route helpers provided by rspec-rails.
# expect(get: "/a/path").to be_routable
# expect(post: "/another/path").to be_routable
# expect(put: "/yet/another/path").to be_routable
def be_routable
BeRoutableMatcher.new(self)
end
# Helpers for matching different route types.
module RouteHelpers
# @!method get
# @!method post
# @!method put
# @!method patch
# @!method delete
# @!method options
# @!method head
#
# Shorthand method for matching this type of route.
%w[get post put patch delete options head].each do |method|
define_method method do |path|
{method.to_sym => path}
end
end
end
end
end
end
end