d46d5ce610
Adds support for with_routing test helpers in ActionDispatch::IntegrationTest. Previously, this helper didn't work in an integration context because the rack app and integration session under test were not mutated. Because controller tests are integration tests by default, we should support test routes for these kinds of test cases as well.
531 lines
13 KiB
Ruby
531 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "active_support/testing/strict_warnings"
|
|
|
|
$:.unshift File.expand_path("lib", __dir__)
|
|
|
|
require "active_support/core_ext/kernel/reporting"
|
|
|
|
# These are the normal settings that will be set up by Railties
|
|
# TODO: Have these tests support other combinations of these values
|
|
silence_warnings do
|
|
Encoding.default_internal = Encoding::UTF_8
|
|
Encoding.default_external = Encoding::UTF_8
|
|
end
|
|
|
|
PROCESS_COUNT = (ENV["MT_CPU"] || 4).to_i
|
|
|
|
require "active_support/testing/autorun"
|
|
require "abstract_controller"
|
|
require "abstract_controller/railties/routes_helpers"
|
|
require "action_controller"
|
|
require "action_view"
|
|
require "action_view/testing/resolvers"
|
|
require "action_dispatch"
|
|
require "active_support/dependencies"
|
|
require "active_model"
|
|
require "zeitwerk"
|
|
|
|
ActiveSupport::Cache.format_version = 7.1
|
|
|
|
module Rails
|
|
class << self
|
|
def env
|
|
@_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
|
|
end
|
|
|
|
def root; end
|
|
end
|
|
end
|
|
|
|
module ActionPackTestSuiteUtils
|
|
def self.require_helpers(helpers_dirs)
|
|
Array(helpers_dirs).each do |helpers_dir|
|
|
Dir.glob("#{helpers_dir}/**/*_helper.rb") do |helper_file|
|
|
require helper_file
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
ActionPackTestSuiteUtils.require_helpers("#{__dir__}/fixtures/helpers")
|
|
ActionPackTestSuiteUtils.require_helpers("#{__dir__}/fixtures/alternate_helpers")
|
|
|
|
Thread.abort_on_exception = true
|
|
|
|
# Show backtraces for deprecated behavior for quicker cleanup.
|
|
ActionController.deprecator.debug = true
|
|
ActionDispatch.deprecator.debug = true
|
|
|
|
# Disable available locale checks to avoid warnings running the test suite.
|
|
I18n.enforce_available_locales = false
|
|
|
|
FIXTURE_LOAD_PATH = File.join(__dir__, "fixtures")
|
|
|
|
SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
|
|
|
|
SharedTestRoutes.draw do
|
|
ActionDispatch.deprecator.silence do
|
|
get ":controller(/:action)"
|
|
end
|
|
end
|
|
|
|
module ActionDispatch
|
|
module SharedRoutes
|
|
def before_setup
|
|
@routes = Routing::RouteSet.new
|
|
ActionDispatch.deprecator.silence do
|
|
@routes.draw { get ":controller(/:action)" }
|
|
end
|
|
super
|
|
end
|
|
end
|
|
end
|
|
|
|
module ActiveSupport
|
|
class TestCase
|
|
if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0
|
|
parallelize(workers: PROCESS_COUNT)
|
|
end
|
|
end
|
|
end
|
|
|
|
class RoutedRackApp
|
|
class Config < Struct.new(:middleware)
|
|
end
|
|
|
|
attr_reader :routes
|
|
|
|
def initialize(routes, &blk)
|
|
@routes = routes
|
|
@stack = ActionDispatch::MiddlewareStack.new(&blk)
|
|
@app = @stack.build(@routes)
|
|
end
|
|
|
|
def call(env)
|
|
@app.call(env)
|
|
end
|
|
|
|
def config
|
|
Config.new(@stack)
|
|
end
|
|
end
|
|
|
|
class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
|
|
def self.build_app(routes = nil)
|
|
RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
|
|
middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
|
|
middleware.use ActionDispatch::DebugExceptions
|
|
middleware.use ActionDispatch::ActionableExceptions
|
|
middleware.use ActionDispatch::Callbacks
|
|
middleware.use ActionDispatch::Cookies
|
|
middleware.use ActionDispatch::Flash
|
|
middleware.use Rack::MethodOverride
|
|
middleware.use Rack::Head
|
|
yield(middleware) if block_given?
|
|
end
|
|
end
|
|
|
|
self.app = build_app
|
|
|
|
app.routes.draw do
|
|
ActionDispatch.deprecator.silence do
|
|
get ":controller(/:action)"
|
|
end
|
|
end
|
|
|
|
class DeadEndRoutes < ActionDispatch::Routing::RouteSet
|
|
# Stub Rails dispatcher so it does not get controller references and
|
|
# simply return the controller#action as Rack::Body.
|
|
class NullController < ::ActionController::Metal
|
|
def self.dispatch(action, req, res)
|
|
[200, { "Content-Type" => "text/html" }, ["#{req.params[:controller]}##{action}"]]
|
|
end
|
|
end
|
|
|
|
class NullControllerRequest < ActionDispatch::Request
|
|
def controller_class
|
|
NullController
|
|
end
|
|
end
|
|
|
|
def make_request(env)
|
|
NullControllerRequest.new env
|
|
end
|
|
end
|
|
|
|
def self.stub_controllers(config = ActionDispatch::Routing::RouteSet::DEFAULT_CONFIG)
|
|
yield DeadEndRoutes.new(config)
|
|
end
|
|
|
|
def with_autoload_path(path)
|
|
path = File.join(__dir__, "fixtures", path)
|
|
Zeitwerk.with_loader do |loader|
|
|
loader.push_dir(path)
|
|
loader.setup
|
|
yield
|
|
ensure
|
|
loader.unload
|
|
end
|
|
end
|
|
end
|
|
|
|
# Temporary base class
|
|
class Rack::TestCase < ActionDispatch::IntegrationTest
|
|
def self.testing(klass = nil)
|
|
if klass
|
|
@testing = "/#{klass.name.underscore}".delete_suffix("_controller")
|
|
else
|
|
@testing
|
|
end
|
|
end
|
|
|
|
def get(thing, *args, **options)
|
|
if thing.is_a?(Symbol)
|
|
super("#{self.class.testing}/#{thing}", *args, **options)
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
def assert_body(body)
|
|
assert_equal body, Array(response.body).join
|
|
end
|
|
|
|
def assert_status(code)
|
|
assert_equal code, response.status
|
|
end
|
|
|
|
def assert_response(body, status = 200, headers = {})
|
|
assert_body body
|
|
assert_status status
|
|
headers.each do |header, value|
|
|
assert_header header, value
|
|
end
|
|
end
|
|
|
|
def assert_content_type(type)
|
|
assert_equal type, response.headers["Content-Type"]
|
|
end
|
|
|
|
def assert_header(name, value)
|
|
assert_equal value, response.headers[name]
|
|
end
|
|
end
|
|
|
|
module ActionController
|
|
class API
|
|
extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes)
|
|
end
|
|
|
|
class Base
|
|
# This stub emulates the Railtie including the URL helpers from a Rails application
|
|
extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes)
|
|
include SharedTestRoutes.mounted_helpers
|
|
|
|
self.view_paths = FIXTURE_LOAD_PATH
|
|
|
|
def self.test_routes(&block)
|
|
routes = ActionDispatch::Routing::RouteSet.new
|
|
routes.draw(&block)
|
|
include routes.url_helpers
|
|
routes
|
|
end
|
|
end
|
|
|
|
class TestCase
|
|
include ActionDispatch::TestProcess
|
|
include ActionDispatch::SharedRoutes
|
|
end
|
|
end
|
|
|
|
class ::ApplicationController < ActionController::Base
|
|
end
|
|
|
|
module ActionDispatch
|
|
class DebugExceptions
|
|
private
|
|
remove_method :stderr_logger
|
|
# Silence logger
|
|
def stderr_logger
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
module ActionDispatch
|
|
module RoutingVerbs
|
|
def send_request(uri_or_host, method, path)
|
|
host = uri_or_host.host unless path
|
|
path ||= uri_or_host.path
|
|
|
|
params = { "PATH_INFO" => path,
|
|
"REQUEST_METHOD" => method,
|
|
"HTTP_HOST" => host }
|
|
|
|
routes.call(params)
|
|
end
|
|
|
|
def request_path_params(path, options = {})
|
|
method = options[:method] || "GET"
|
|
resp = send_request URI("http://localhost" + path), method.to_s.upcase, nil
|
|
status = resp.first
|
|
if status == 404
|
|
raise ActionController::RoutingError, "No route matches #{path.inspect}"
|
|
end
|
|
controller.request.path_parameters
|
|
end
|
|
|
|
def get(uri_or_host, path = nil)
|
|
send_request(uri_or_host, "GET", path)[2].join
|
|
end
|
|
|
|
def post(uri_or_host, path = nil)
|
|
send_request(uri_or_host, "POST", path)[2].join
|
|
end
|
|
|
|
def put(uri_or_host, path = nil)
|
|
send_request(uri_or_host, "PUT", path)[2].join
|
|
end
|
|
|
|
def delete(uri_or_host, path = nil)
|
|
send_request(uri_or_host, "DELETE", path)[2].join
|
|
end
|
|
|
|
def patch(uri_or_host, path = nil)
|
|
send_request(uri_or_host, "PATCH", path)[2].join
|
|
end
|
|
end
|
|
end
|
|
|
|
module RoutingTestHelpers
|
|
def url_for(set, options)
|
|
route_name = options.delete :use_route
|
|
set.url_for options.merge(only_path: true), route_name
|
|
end
|
|
|
|
def make_set(strict = true)
|
|
tc = self
|
|
TestSet.new ->(c) { tc.controller = c }, strict
|
|
end
|
|
|
|
class TestSet < ActionDispatch::Routing::RouteSet
|
|
class Request < DelegateClass(ActionDispatch::Request)
|
|
def initialize(target, helpers, block, strict)
|
|
super(target)
|
|
@helpers = helpers
|
|
@block = block
|
|
@strict = strict
|
|
end
|
|
|
|
def controller_class
|
|
helpers = @helpers
|
|
block = @block
|
|
Class.new(@strict ? super : ActionController::Base) {
|
|
include helpers
|
|
define_method(:process) { |name| block.call(self) }
|
|
def to_a; [200, {}, []]; end
|
|
}
|
|
end
|
|
end
|
|
|
|
attr_reader :strict
|
|
|
|
def initialize(block, strict = false)
|
|
@block = block
|
|
@strict = strict
|
|
super()
|
|
end
|
|
|
|
private
|
|
def make_request(env)
|
|
Request.new super, url_helpers, @block, strict
|
|
end
|
|
end
|
|
end
|
|
|
|
class ResourcesController < ActionController::Base
|
|
def index() head :ok end
|
|
alias_method :show, :index
|
|
end
|
|
|
|
class CommentsController < ResourcesController; end
|
|
class AccountsController < ResourcesController; end
|
|
class ImagesController < ResourcesController; end
|
|
|
|
require "active_support/testing/method_call_assertions"
|
|
|
|
class ActiveSupport::TestCase
|
|
include ActiveSupport::Testing::MethodCallAssertions
|
|
end
|
|
|
|
module CookieAssertions
|
|
def parse_set_cookie_attributes(fields, attributes = {})
|
|
if fields.is_a?(String)
|
|
fields = fields.split(";").map(&:strip)
|
|
end
|
|
|
|
fields.each do |field|
|
|
key, value = field.split("=", 2)
|
|
|
|
# Normalize the key to lowercase:
|
|
key.downcase!
|
|
|
|
if value
|
|
value.downcase!
|
|
attributes[key] = value
|
|
else
|
|
attributes[key] = true
|
|
end
|
|
end
|
|
|
|
attributes
|
|
end
|
|
|
|
# Parse the set-cookie header and return a hash of cookie names and values.
|
|
#
|
|
# Example:
|
|
# set_cookies = headers["set-cookie"]
|
|
# parse_set_cookies_headers(set_cookies)
|
|
def parse_set_cookies_headers(set_cookies)
|
|
if set_cookies.is_a?(String)
|
|
set_cookies = set_cookies.split("\n")
|
|
end
|
|
|
|
cookies = {}
|
|
|
|
set_cookies&.each do |cookie_string|
|
|
attributes = {}
|
|
|
|
fields = cookie_string.split(";").map(&:strip)
|
|
|
|
# The first one is the cookie name:
|
|
name, value = fields.shift.split("=", 2)
|
|
|
|
attributes[:value] = value
|
|
|
|
cookies[name] = parse_set_cookie_attributes(fields, attributes)
|
|
end
|
|
|
|
cookies
|
|
end
|
|
|
|
def assert_set_cookie_attributes(name, attributes, header = @response.headers["Set-Cookie"])
|
|
cookies = parse_set_cookies_headers(header)
|
|
attributes = parse_set_cookie_attributes(attributes) if attributes.is_a?(String)
|
|
|
|
assert cookies.key?(name), "No cookie found with the name '#{name}', found cookies: #{cookies.keys.join(', ')}"
|
|
cookie = cookies[name]
|
|
|
|
attributes.each do |key, value|
|
|
assert cookie.key?(key), "No attribute '#{key}' found for cookie '#{name}'"
|
|
assert_equal value, cookie[key]
|
|
end
|
|
end
|
|
|
|
def assert_not_set_cookie_attributes(name, attributes, header = @response.headers["Set-Cookie"])
|
|
cookies = parse_set_cookies_headers(header)
|
|
attributes = parse_set_cookie_attributes(attributes) if attributes.is_a?(String)
|
|
|
|
assert cookies.key?(name), "No cookie found with the name '#{name}'"
|
|
cookie = cookies[name]
|
|
|
|
attributes.each do |key, value|
|
|
if value == true
|
|
assert_nil cookie[key]
|
|
else
|
|
assert_not_equal value, cookie[key]
|
|
end
|
|
end
|
|
end
|
|
|
|
def assert_set_cookie_header(expected, header = @response.headers["Set-Cookie"])
|
|
# In Rack v2, this is newline delimited. In Rack v3, this is an array.
|
|
# Normalize the comparison so that we can assert equality in both cases.
|
|
|
|
if header.is_a?(String)
|
|
header = header.split("\n").sort
|
|
end
|
|
|
|
if expected.is_a?(String)
|
|
expected = expected.split("\n").sort
|
|
end
|
|
|
|
# While not strictly speaking correct, this is probably good enough for now:
|
|
header = parse_set_cookies_headers(header)
|
|
expected = parse_set_cookies_headers(expected)
|
|
|
|
expected.each do |key, value|
|
|
assert_equal value, header[key]
|
|
end
|
|
end
|
|
|
|
def assert_not_set_cookie_header(expected, header = @response.headers["Set-Cookie"])
|
|
if header.is_a?(String)
|
|
header = header.split("\n").sort
|
|
end
|
|
|
|
if expected.is_a?(String)
|
|
expected = expected.split("\n").sort
|
|
end
|
|
|
|
# While not strictly speaking correct, this is probably good enough for now:
|
|
header = parse_set_cookies_headers(header)
|
|
|
|
expected.each do |name|
|
|
assert_not_includes(header, name)
|
|
end
|
|
end
|
|
end
|
|
|
|
module HeadersAssertions
|
|
def normalize_headers(headers)
|
|
headers.transform_keys(&:downcase)
|
|
end
|
|
|
|
def assert_headers(expected, actual = @response.headers)
|
|
actual = normalize_headers(actual)
|
|
expected.each do |key, value|
|
|
assert_equal value, actual[key]
|
|
end
|
|
end
|
|
|
|
def assert_header(key, value, actual = @response.headers)
|
|
actual = normalize_headers(actual)
|
|
assert_equal value, actual[key]
|
|
end
|
|
|
|
def assert_not_header(key, actual = @response.headers)
|
|
actual = normalize_headers(actual)
|
|
assert_not_includes(actual, key)
|
|
end
|
|
|
|
# This works for most headers, but not all, e.g. `set-cookie`.
|
|
def normalized_join_header(header)
|
|
header.is_a?(Array) ? header.join(",") : header
|
|
end
|
|
|
|
def assert_header_value(expected, header)
|
|
header = normalized_join_header(header)
|
|
assert_equal header, expected
|
|
end
|
|
end
|
|
|
|
class DrivenByRackTest < ActionDispatch::SystemTestCase
|
|
driven_by :rack_test
|
|
end
|
|
|
|
class DrivenBySeleniumWithChrome < ActionDispatch::SystemTestCase
|
|
driven_by :selenium, using: :chrome
|
|
end
|
|
|
|
class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase
|
|
driven_by :selenium, using: :headless_chrome
|
|
end
|
|
|
|
class DrivenBySeleniumWithHeadlessFirefox < ActionDispatch::SystemTestCase
|
|
driven_by :selenium, using: :headless_firefox
|
|
end
|
|
|
|
require_relative "../../tools/test_common"
|