aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/test/controller
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/test/controller')
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb491
-rw-r--r--actionpack/test/controller/api/conditional_get_test.rb59
-rw-r--r--actionpack/test/controller/api/data_streaming_test.rb28
-rw-r--r--actionpack/test/controller/api/force_ssl_test.rb24
-rw-r--r--actionpack/test/controller/api/implicit_render_test.rb17
-rw-r--r--actionpack/test/controller/api/params_wrapper_test.rb28
-rw-r--r--actionpack/test/controller/api/redirect_to_test.rb21
-rw-r--r--actionpack/test/controller/api/renderers_test.rb50
-rw-r--r--actionpack/test/controller/api/url_for_test.rb22
-rw-r--r--actionpack/test/controller/api/with_cookies_test.rb23
-rw-r--r--actionpack/test/controller/api/with_helpers_test.rb44
-rw-r--r--actionpack/test/controller/base_test.rb323
-rw-r--r--actionpack/test/controller/caching_test.rb511
-rw-r--r--actionpack/test/controller/content_type_test.rb169
-rw-r--r--actionpack/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb0
-rw-r--r--actionpack/test/controller/controller_fixtures/app/controllers/user_controller.rb0
-rw-r--r--actionpack/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb0
-rw-r--r--actionpack/test/controller/default_url_options_with_before_action_test.rb28
-rw-r--r--actionpack/test/controller/filters_test.rb1048
-rw-r--r--actionpack/test/controller/flash_hash_test.rb216
-rw-r--r--actionpack/test/controller/flash_test.rb388
-rw-r--r--actionpack/test/controller/force_ssl_test.rb345
-rw-r--r--actionpack/test/controller/form_builder_test.rb19
-rw-r--r--actionpack/test/controller/helper_test.rb295
-rw-r--r--actionpack/test/controller/http_basic_authentication_test.rb179
-rw-r--r--actionpack/test/controller/http_digest_authentication_test.rb283
-rw-r--r--actionpack/test/controller/http_token_authentication_test.rb216
-rw-r--r--actionpack/test/controller/integration_test.rb1146
-rw-r--r--actionpack/test/controller/live_stream_test.rb518
-rw-r--r--actionpack/test/controller/localized_templates_test.rb48
-rw-r--r--actionpack/test/controller/log_subscriber_test.rb369
-rw-r--r--actionpack/test/controller/metal/renderers_test.rb50
-rw-r--r--actionpack/test/controller/metal_test.rb32
-rw-r--r--actionpack/test/controller/mime/accept_format_test.rb94
-rw-r--r--actionpack/test/controller/mime/respond_to_test.rb877
-rw-r--r--actionpack/test/controller/new_base/bare_metal_test.rb184
-rw-r--r--actionpack/test/controller/new_base/base_test.rb131
-rw-r--r--actionpack/test/controller/new_base/content_negotiation_test.rb28
-rw-r--r--actionpack/test/controller/new_base/content_type_test.rb116
-rw-r--r--actionpack/test/controller/new_base/middleware_test.rb112
-rw-r--r--actionpack/test/controller/new_base/render_action_test.rb314
-rw-r--r--actionpack/test/controller/new_base/render_body_test.rb172
-rw-r--r--actionpack/test/controller/new_base/render_context_test.rb55
-rw-r--r--actionpack/test/controller/new_base/render_file_test.rb72
-rw-r--r--actionpack/test/controller/new_base/render_html_test.rb192
-rw-r--r--actionpack/test/controller/new_base/render_implicit_action_test.rb59
-rw-r--r--actionpack/test/controller/new_base/render_layout_test.rb128
-rw-r--r--actionpack/test/controller/new_base/render_partial_test.rb62
-rw-r--r--actionpack/test/controller/new_base/render_plain_test.rb170
-rw-r--r--actionpack/test/controller/new_base/render_streaming_test.rb116
-rw-r--r--actionpack/test/controller/new_base/render_template_test.rb240
-rw-r--r--actionpack/test/controller/new_base/render_test.rb142
-rw-r--r--actionpack/test/controller/new_base/render_xml_test.rb12
-rw-r--r--actionpack/test/controller/output_escaping_test.rb17
-rw-r--r--actionpack/test/controller/parameter_encoding_test.rb52
-rw-r--r--actionpack/test/controller/parameters/accessors_test.rb338
-rw-r--r--actionpack/test/controller/parameters/always_permitted_parameters_test.rb30
-rw-r--r--actionpack/test/controller/parameters/dup_test.rb67
-rw-r--r--actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb70
-rw-r--r--actionpack/test/controller/parameters/multi_parameter_attributes_test.rb39
-rw-r--r--actionpack/test/controller/parameters/mutators_test.rb121
-rw-r--r--actionpack/test/controller/parameters/nested_parameters_permit_test.rb184
-rw-r--r--actionpack/test/controller/parameters/parameters_permit_test.rb510
-rw-r--r--actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb33
-rw-r--r--actionpack/test/controller/parameters/serialization_test.rb54
-rw-r--r--actionpack/test/controller/params_parse_test.rb34
-rw-r--r--actionpack/test/controller/params_wrapper_test.rb422
-rw-r--r--actionpack/test/controller/permitted_params_test.rb27
-rw-r--r--actionpack/test/controller/redirect_test.rb402
-rw-r--r--actionpack/test/controller/render_js_test.rb36
-rw-r--r--actionpack/test/controller/render_json_test.rb137
-rw-r--r--actionpack/test/controller/render_test.rb877
-rw-r--r--actionpack/test/controller/render_xml_test.rb102
-rw-r--r--actionpack/test/controller/renderer_test.rb136
-rw-r--r--actionpack/test/controller/renderers_test.rb91
-rw-r--r--actionpack/test/controller/request/test_request_test.rb42
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb1018
-rw-r--r--actionpack/test/controller/required_params_test.rb100
-rw-r--r--actionpack/test/controller/rescue_test.rb364
-rw-r--r--actionpack/test/controller/resources_test.rb1390
-rw-r--r--actionpack/test/controller/routing_test.rb2105
-rw-r--r--actionpack/test/controller/runner_test.rb24
-rw-r--r--actionpack/test/controller/send_file_test.rb259
-rw-r--r--actionpack/test/controller/show_exceptions_test.rb114
-rw-r--r--actionpack/test/controller/streaming_test.rb28
-rw-r--r--actionpack/test/controller/test_case_test.rb1197
-rw-r--r--actionpack/test/controller/url_for_integration_test.rb192
-rw-r--r--actionpack/test/controller/url_for_test.rb519
-rw-r--r--actionpack/test/controller/url_rewriter_test.rb92
-rw-r--r--actionpack/test/controller/webservice_test.rb135
90 files changed, 21624 insertions, 0 deletions
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
new file mode 100644
index 0000000000..ecb8c37e6b
--- /dev/null
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -0,0 +1,491 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
+
+class ActionPackAssertionsController < ActionController::Base
+ def nothing() head :ok end
+
+ def hello_xml_world() render template: "test/hello_xml_world"; end
+
+ def hello_xml_world_pdf
+ self.content_type = "application/pdf"
+ render template: "test/hello_xml_world"
+ end
+
+ def hello_xml_world_pdf_header
+ response.headers["Content-Type"] = "application/pdf; charset=utf-8"
+ render template: "test/hello_xml_world"
+ end
+
+ def redirect_internal() redirect_to "/nothing"; end
+
+ def redirect_to_action() redirect_to action: "flash_me", id: 1, params: { "panda" => "fun" }; end
+
+ def redirect_to_controller() redirect_to controller: "elsewhere", action: "flash_me"; end
+
+ def redirect_to_controller_with_symbol() redirect_to controller: :elsewhere, action: :flash_me; end
+
+ def redirect_to_path() redirect_to "/some/path" end
+
+ def redirect_invalid_external_route() redirect_to "ht_tp://www.rubyonrails.org" end
+
+ def redirect_to_named_route() redirect_to route_one_url end
+
+ def redirect_external() redirect_to "http://www.rubyonrails.org"; end
+
+ def redirect_external_protocol_relative() redirect_to "//www.rubyonrails.org"; end
+
+ def response404() head "404 AWOL" end
+
+ def response500() head "500 Sorry" end
+
+ def response599() head "599 Whoah!" end
+
+ def flash_me
+ flash["hello"] = "my name is inigo montoya..."
+ render plain: "Inconceivable!"
+ end
+
+ def flash_me_naked
+ flash.clear
+ render plain: "wow!"
+ end
+
+ def assign_this
+ @howdy = "ho"
+ render inline: "Mr. Henke"
+ end
+
+ def render_based_on_parameters
+ render plain: "Mr. #{params[:name]}"
+ end
+
+ def render_url
+ render html: "<div>#{url_for(action: 'flash_me', only_path: true)}</div>"
+ end
+
+ def render_text_with_custom_content_type
+ render body: "Hello!", content_type: Mime[:rss]
+ end
+
+ def session_stuffing
+ session["xmas"] = "turkey"
+ render plain: "ho ho ho"
+ end
+
+ def raise_exception_on_get
+ raise "get" if request.get?
+ render plain: "request method: #{request.env['REQUEST_METHOD']}"
+ end
+
+ def raise_exception_on_post
+ raise "post" if request.post?
+ render plain: "request method: #{request.env['REQUEST_METHOD']}"
+ end
+
+ def render_file_absolute_path
+ render file: File.expand_path("../../README.rdoc", __dir__)
+ end
+
+ def render_file_relative_path
+ render file: "README.rdoc"
+ end
+end
+
+# Used to test that assert_response includes the exception message
+# in the failure message when an action raises and assert_response
+# is expecting something other than an error.
+class AssertResponseWithUnexpectedErrorController < ActionController::Base
+ def index
+ raise "FAIL"
+ end
+
+ def show
+ render plain: "Boom", status: 500
+ end
+end
+
+module Admin
+ class InnerModuleController < ActionController::Base
+ def index
+ head :ok
+ end
+
+ def redirect_to_index
+ redirect_to admin_inner_module_path
+ end
+
+ def redirect_to_absolute_controller
+ redirect_to controller: "/content"
+ end
+
+ def redirect_to_fellow_controller
+ redirect_to controller: "user"
+ end
+
+ def redirect_to_top_level_named_route
+ redirect_to top_level_url(id: "foo")
+ end
+ end
+end
+
+class ApiOnlyController < ActionController::API
+ def nothing
+ head :ok
+ end
+
+ def redirect_to_new_route
+ redirect_to new_route_url
+ end
+end
+
+class ActionPackAssertionsControllerTest < ActionController::TestCase
+ def test_render_file_absolute_path
+ get :render_file_absolute_path
+ assert_match(/\A= Action Pack/, @response.body)
+ end
+
+ def test_render_file_relative_path
+ get :render_file_relative_path
+ assert_match(/\A= Action Pack/, @response.body)
+ end
+
+ def test_get_request
+ assert_raise(RuntimeError) { get :raise_exception_on_get }
+ get :raise_exception_on_post
+ assert_equal "request method: GET", @response.body
+ end
+
+ def test_post_request
+ assert_raise(RuntimeError) { post :raise_exception_on_post }
+ post :raise_exception_on_get
+ assert_equal "request method: POST", @response.body
+ end
+
+ def test_get_post_request_switch
+ post :raise_exception_on_get
+ assert_equal "request method: POST", @response.body
+ get :raise_exception_on_post
+ assert_equal "request method: GET", @response.body
+ post :raise_exception_on_get
+ assert_equal "request method: POST", @response.body
+ get :raise_exception_on_post
+ assert_equal "request method: GET", @response.body
+ end
+
+ def test_string_constraint
+ with_routing do |set|
+ set.draw do
+ get "photos", to: "action_pack_assertions#nothing", constraints: { subdomain: "admin" }
+ end
+ end
+ end
+
+ def test_with_routing_works_with_api_only_controllers
+ @controller = ApiOnlyController.new
+
+ with_routing do |set|
+ set.draw do
+ get "new_route", to: "api_only#nothing"
+ get "redirect_to_new_route", to: "api_only#redirect_to_new_route"
+ end
+
+ process :redirect_to_new_route
+ assert_redirected_to "http://test.host/new_route"
+ end
+ end
+
+ def test_assert_redirect_to_named_route_failure
+ with_routing do |set|
+ set.draw do
+ get "route_one", to: "action_pack_assertions#nothing", as: :route_one
+ get "route_two", to: "action_pack_assertions#nothing", id: "two", as: :route_two
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+ process :redirect_to_named_route
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_redirected_to "http://test.host/route_two"
+ end
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_redirected_to %r(^http://test.host/route_two)
+ end
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_redirected_to controller: "action_pack_assertions", action: "nothing", id: "two"
+ end
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_redirected_to route_two_url
+ end
+ end
+ end
+
+ def test_assert_redirect_to_nested_named_route
+ @controller = Admin::InnerModuleController.new
+
+ with_routing do |set|
+ set.draw do
+ get "admin/inner_module", to: "admin/inner_module#index", as: :admin_inner_module
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+ process :redirect_to_index
+ # redirection is <{"action"=>"index", "controller"=>"admin/admin/inner_module"}>
+ assert_redirected_to admin_inner_module_path
+ end
+ end
+
+ def test_assert_redirected_to_top_level_named_route_from_nested_controller
+ @controller = Admin::InnerModuleController.new
+
+ with_routing do |set|
+ set.draw do
+ get "/action_pack_assertions/:id", to: "action_pack_assertions#index", as: :top_level
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+ process :redirect_to_top_level_named_route
+ # assert_redirected_to "http://test.host/action_pack_assertions/foo" would pass because of exact match early return
+ assert_redirected_to "/action_pack_assertions/foo"
+ assert_redirected_to %r(/action_pack_assertions/foo)
+ end
+ end
+
+ def test_assert_redirected_to_top_level_named_route_with_same_controller_name_in_both_namespaces
+ @controller = Admin::InnerModuleController.new
+
+ with_routing do |set|
+ set.draw do
+ # this controller exists in the admin namespace as well which is the only difference from previous test
+ get "/user/:id", to: "user#index", as: :top_level
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+ process :redirect_to_top_level_named_route
+ # assert_redirected_to top_level_url('foo') would pass because of exact match early return
+ assert_redirected_to top_level_path("foo")
+ end
+ end
+
+ def test_assert_redirect_failure_message_with_protocol_relative_url
+ process :redirect_external_protocol_relative
+ assert_redirected_to "/foo"
+ rescue ActiveSupport::TestCase::Assertion => ex
+ assert_no_match(
+ /#{request.protocol}#{request.host}\/\/www.rubyonrails.org/,
+ ex.message,
+ "protocol relative url was incorrectly normalized"
+ )
+ end
+
+ def test_template_objects_exist
+ process :assign_this
+ assert_not @controller.instance_variable_defined?(:"@hi")
+ assert @controller.instance_variable_get(:"@howdy")
+ end
+
+ def test_template_objects_missing
+ process :nothing
+ assert_not @controller.instance_variable_defined?(:@howdy)
+ end
+
+ def test_empty_flash
+ process :flash_me_naked
+ assert_empty flash
+ end
+
+ def test_flash_exist
+ process :flash_me
+ assert_predicate flash, :any?
+ assert_predicate flash["hello"], :present?
+ end
+
+ def test_flash_does_not_exist
+ process :nothing
+ assert_empty flash
+ end
+
+ def test_session_exist
+ process :session_stuffing
+ assert_equal "turkey", session["xmas"]
+ end
+
+ def session_does_not_exist
+ process :nothing
+ assert_empty session
+ end
+
+ def test_redirection_location
+ process :redirect_internal
+ assert_equal "http://test.host/nothing", @response.redirect_url
+
+ process :redirect_external
+ assert_equal "http://www.rubyonrails.org", @response.redirect_url
+
+ process :redirect_external_protocol_relative
+ assert_equal "//www.rubyonrails.org", @response.redirect_url
+ end
+
+ def test_no_redirect_url
+ process :nothing
+ assert_nil @response.redirect_url
+ end
+
+ def test_server_error_response_code
+ process :response500
+ assert_predicate @response, :server_error?
+
+ process :response599
+ assert_predicate @response, :server_error?
+
+ process :response404
+ assert_not_predicate @response, :server_error?
+ end
+
+ def test_missing_response_code
+ process :response404
+ assert_predicate @response, :not_found?
+ end
+
+ def test_client_error_response_code
+ process :response404
+ assert_predicate @response, :client_error?
+ end
+
+ def test_redirect_url_match
+ process :redirect_external
+ assert_predicate @response, :redirect?
+ assert_match(/rubyonrails/, @response.redirect_url)
+ assert_no_match(/perloffrails/, @response.redirect_url)
+ end
+
+ def test_redirection
+ process :redirect_internal
+ assert_predicate @response, :redirect?
+
+ process :redirect_external
+ assert_predicate @response, :redirect?
+
+ process :nothing
+ assert_not_predicate @response, :redirect?
+ end
+
+ def test_successful_response_code
+ process :nothing
+ assert_predicate @response, :successful?
+ end
+
+ def test_response_object
+ process :nothing
+ assert_kind_of ActionDispatch::TestResponse, @response
+ end
+
+ def test_render_based_on_parameters
+ process :render_based_on_parameters,
+ method: "GET",
+ params: { name: "David" }
+ assert_equal "Mr. David", @response.body
+ end
+
+ def test_assert_redirection_fails_with_incorrect_controller
+ process :redirect_to_controller
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_redirected_to controller: "action_pack_assertions", action: "flash_me"
+ end
+ end
+
+ def test_assert_redirection_with_extra_controller_option
+ get :redirect_to_action
+ assert_redirected_to controller: "action_pack_assertions", action: "flash_me", id: 1, params: { panda: "fun" }
+ end
+
+ def test_redirected_to_url_leading_slash
+ process :redirect_to_path
+ assert_redirected_to "/some/path"
+ end
+
+ def test_redirected_to_url_no_leading_slash_fails
+ process :redirect_to_path
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_redirected_to "some/path"
+ end
+ end
+
+ def test_redirect_invalid_external_route
+ process :redirect_invalid_external_route
+ assert_redirected_to "http://test.hostht_tp://www.rubyonrails.org"
+ end
+
+ def test_redirected_to_url_full_url
+ process :redirect_to_path
+ assert_redirected_to "http://test.host/some/path"
+ end
+
+ def test_assert_redirection_with_symbol
+ process :redirect_to_controller_with_symbol
+ assert_nothing_raised {
+ assert_redirected_to controller: "elsewhere", action: "flash_me"
+ }
+ process :redirect_to_controller_with_symbol
+ assert_nothing_raised {
+ assert_redirected_to controller: :elsewhere, action: :flash_me
+ }
+ end
+
+ def test_redirected_to_with_nested_controller
+ @controller = Admin::InnerModuleController.new
+ get :redirect_to_absolute_controller
+ assert_redirected_to controller: "/content"
+
+ get :redirect_to_fellow_controller
+ assert_redirected_to controller: "admin/user"
+ end
+
+ def test_assert_response_uses_exception_message
+ @controller = AssertResponseWithUnexpectedErrorController.new
+ e = assert_raise RuntimeError, "Expected non-success response" do
+ get :index
+ end
+ assert_response :success
+ assert_includes "FAIL", e.message
+ end
+
+ def test_assert_response_failure_response_with_no_exception
+ @controller = AssertResponseWithUnexpectedErrorController.new
+ get :show
+ assert_response 500
+ assert_equal "Boom", response.body
+ end
+end
+
+class ActionPackHeaderTest < ActionController::TestCase
+ tests ActionPackAssertionsController
+
+ def test_rendering_xml_sets_content_type
+ process :hello_xml_world
+ assert_equal("application/xml; charset=utf-8", @response.headers["Content-Type"])
+ end
+
+ def test_rendering_xml_respects_content_type
+ process :hello_xml_world_pdf
+ assert_equal("application/pdf; charset=utf-8", @response.headers["Content-Type"])
+ end
+
+ def test_rendering_xml_respects_content_type_when_set_in_the_header
+ process :hello_xml_world_pdf_header
+ assert_equal("application/pdf; charset=utf-8", @response.headers["Content-Type"])
+ end
+
+ def test_render_text_with_custom_content_type
+ get :render_text_with_custom_content_type
+ assert_equal "application/rss+xml; charset=utf-8", @response.headers["Content-Type"]
+ end
+end
diff --git a/actionpack/test/controller/api/conditional_get_test.rb b/actionpack/test/controller/api/conditional_get_test.rb
new file mode 100644
index 0000000000..e366ce9532
--- /dev/null
+++ b/actionpack/test/controller/api/conditional_get_test.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/integer/time"
+require "active_support/core_ext/numeric/time"
+
+class ConditionalGetApiController < ActionController::API
+ before_action :handle_last_modified_and_etags, only: :two
+
+ def one
+ if stale?(last_modified: Time.now.utc.beginning_of_day, etag: [:foo, 123])
+ render plain: "Hi!"
+ end
+ end
+
+ def two
+ render plain: "Hi!"
+ end
+
+ private
+
+ def handle_last_modified_and_etags
+ fresh_when(last_modified: Time.now.utc.beginning_of_day, etag: [ :foo, 123 ])
+ end
+end
+
+class ConditionalGetApiTest < ActionController::TestCase
+ tests ConditionalGetApiController
+
+ def setup
+ @last_modified = Time.now.utc.beginning_of_day.httpdate
+ end
+
+ def test_request_gets_last_modified
+ get :two
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ assert_response :success
+ end
+
+ def test_request_obeys_last_modified
+ @request.if_modified_since = @last_modified
+ get :two
+ assert_response :not_modified
+ end
+
+ def test_last_modified_works_with_less_than_too
+ @request.if_modified_since = 5.years.ago.httpdate
+ get :two
+ assert_response :success
+ end
+
+ def test_request_not_modified
+ @request.if_modified_since = @last_modified
+ get :one
+ assert_equal 304, @response.status.to_i
+ assert_predicate @response.body, :blank?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ end
+end
diff --git a/actionpack/test/controller/api/data_streaming_test.rb b/actionpack/test/controller/api/data_streaming_test.rb
new file mode 100644
index 0000000000..6446ff9e40
--- /dev/null
+++ b/actionpack/test/controller/api/data_streaming_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module TestApiFileUtils
+ def file_path() __FILE__ end
+ def file_data() @data ||= File.open(file_path, "rb") { |f| f.read } end
+end
+
+class DataStreamingApiController < ActionController::API
+ include TestApiFileUtils
+
+ def one; end
+ def two
+ send_data(file_data, {})
+ end
+end
+
+class DataStreamingApiTest < ActionController::TestCase
+ include TestApiFileUtils
+ tests DataStreamingApiController
+
+ def test_data
+ response = process("two")
+ assert_kind_of String, response.body
+ assert_equal file_data, response.body
+ end
+end
diff --git a/actionpack/test/controller/api/force_ssl_test.rb b/actionpack/test/controller/api/force_ssl_test.rb
new file mode 100644
index 0000000000..8191578eb0
--- /dev/null
+++ b/actionpack/test/controller/api/force_ssl_test.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ForceSSLApiController < ActionController::API
+ ActiveSupport::Deprecation.silence do
+ force_ssl
+ end
+
+ def one; end
+ def two
+ head :ok
+ end
+end
+
+class ForceSSLApiTest < ActionController::TestCase
+ tests ForceSSLApiController
+
+ def test_redirects_to_https
+ get :two
+ assert_response 301
+ assert_equal "https://test.host/force_ssl_api/two", redirect_to_url
+ end
+end
diff --git a/actionpack/test/controller/api/implicit_render_test.rb b/actionpack/test/controller/api/implicit_render_test.rb
new file mode 100644
index 0000000000..288fb333b0
--- /dev/null
+++ b/actionpack/test/controller/api/implicit_render_test.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ImplicitRenderAPITestController < ActionController::API
+ def empty_action
+ end
+end
+
+class ImplicitRenderAPITest < ActionController::TestCase
+ tests ImplicitRenderAPITestController
+
+ def test_implicit_no_content_response
+ get :empty_action
+ assert_response :no_content
+ end
+end
diff --git a/actionpack/test/controller/api/params_wrapper_test.rb b/actionpack/test/controller/api/params_wrapper_test.rb
new file mode 100644
index 0000000000..814c24bfd8
--- /dev/null
+++ b/actionpack/test/controller/api/params_wrapper_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ParamsWrapperForApiTest < ActionController::TestCase
+ class UsersController < ActionController::API
+ attr_accessor :last_parameters
+
+ wrap_parameters :person, format: [:json]
+
+ def test
+ self.last_parameters = params.except(:controller, :action).to_unsafe_h
+ head :ok
+ end
+ end
+
+ class Person; end
+
+ tests UsersController
+
+ def test_specify_wrapper_name
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :test, params: { "username" => "sikachu" }
+
+ expected = { "username" => "sikachu", "person" => { "username" => "sikachu" } }
+ assert_equal expected, @controller.last_parameters
+ end
+end
diff --git a/actionpack/test/controller/api/redirect_to_test.rb b/actionpack/test/controller/api/redirect_to_test.rb
new file mode 100644
index 0000000000..f8230dd6a9
--- /dev/null
+++ b/actionpack/test/controller/api/redirect_to_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class RedirectToApiController < ActionController::API
+ def one
+ redirect_to action: "two"
+ end
+
+ def two; end
+end
+
+class RedirectToApiTest < ActionController::TestCase
+ tests RedirectToApiController
+
+ def test_redirect_to
+ get :one
+ assert_response :redirect
+ assert_equal "http://test.host/redirect_to_api/two", redirect_to_url
+ end
+end
diff --git a/actionpack/test/controller/api/renderers_test.rb b/actionpack/test/controller/api/renderers_test.rb
new file mode 100644
index 0000000000..e7a9a4b2da
--- /dev/null
+++ b/actionpack/test/controller/api/renderers_test.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/hash/conversions"
+
+class RenderersApiController < ActionController::API
+ class Model
+ def to_json(options = {})
+ { a: "b" }.to_json(options)
+ end
+
+ def to_xml(options = {})
+ { a: "b" }.to_xml(options)
+ end
+ end
+
+ def one
+ render json: Model.new
+ end
+
+ def two
+ render xml: Model.new
+ end
+
+ def plain
+ render plain: "Hi from plain", status: 500
+ end
+end
+
+class RenderersApiTest < ActionController::TestCase
+ tests RenderersApiController
+
+ def test_render_json
+ get :one
+ assert_response :success
+ assert_equal({ a: "b" }.to_json, @response.body)
+ end
+
+ def test_render_xml
+ get :two
+ assert_response :success
+ assert_equal({ a: "b" }.to_xml, @response.body)
+ end
+
+ def test_render_plain
+ get :plain
+ assert_response :internal_server_error
+ assert_equal("Hi from plain", @response.body)
+ end
+end
diff --git a/actionpack/test/controller/api/url_for_test.rb b/actionpack/test/controller/api/url_for_test.rb
new file mode 100644
index 0000000000..aa3428bc85
--- /dev/null
+++ b/actionpack/test/controller/api/url_for_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class UrlForApiController < ActionController::API
+ def one; end
+ def two; end
+end
+
+class UrlForApiTest < ActionController::TestCase
+ tests UrlForApiController
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ end
+
+ def test_url_for
+ get :one
+ assert_equal "http://www.example.com/url_for_api/one", @controller.url_for
+ end
+end
diff --git a/actionpack/test/controller/api/with_cookies_test.rb b/actionpack/test/controller/api/with_cookies_test.rb
new file mode 100644
index 0000000000..1a6e12a4f3
--- /dev/null
+++ b/actionpack/test/controller/api/with_cookies_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class WithCookiesController < ActionController::API
+ include ActionController::Cookies
+
+ def with_cookies
+ render plain: cookies[:foobar]
+ end
+end
+
+class WithCookiesTest < ActionController::TestCase
+ tests WithCookiesController
+
+ def test_with_cookies
+ request.cookies[:foobar] = "bazbang"
+
+ get :with_cookies
+
+ assert_equal "bazbang", response.body
+ end
+end
diff --git a/actionpack/test/controller/api/with_helpers_test.rb b/actionpack/test/controller/api/with_helpers_test.rb
new file mode 100644
index 0000000000..00179d3505
--- /dev/null
+++ b/actionpack/test/controller/api/with_helpers_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ApiWithHelper
+ def my_helper
+ "helper"
+ end
+end
+
+class WithHelpersController < ActionController::API
+ include ActionController::Helpers
+ helper ApiWithHelper
+
+ def with_helpers
+ render plain: self.class.helpers.my_helper
+ end
+end
+
+class SubclassWithHelpersController < WithHelpersController
+ def with_helpers
+ render plain: self.class.helpers.my_helper
+ end
+end
+
+class WithHelpersTest < ActionController::TestCase
+ tests WithHelpersController
+
+ def test_with_helpers
+ get :with_helpers
+
+ assert_equal "helper", response.body
+ end
+end
+
+class SubclassWithHelpersTest < ActionController::TestCase
+ tests WithHelpersController
+
+ def test_with_helpers
+ get :with_helpers
+
+ assert_equal "helper", response.body
+ end
+end
diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb
new file mode 100644
index 0000000000..558e710df9
--- /dev/null
+++ b/actionpack/test/controller/base_test.rb
@@ -0,0 +1,323 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/logger"
+require "controller/fake_models"
+
+# Provide some controller to run the tests on.
+module Submodule
+ class ContainedEmptyController < ActionController::Base
+ end
+end
+
+class EmptyController < ActionController::Base
+end
+
+class SimpleController < ActionController::Base
+ def hello
+ self.response_body = "hello"
+ end
+end
+
+class NonEmptyController < ActionController::Base
+ def public_action
+ head :ok
+ end
+end
+
+class DefaultUrlOptionsController < ActionController::Base
+ def from_view
+ render inline: "<%= #{params[:route]} %>"
+ end
+
+ def default_url_options
+ { host: "www.override.com", action: "new", locale: "en" }
+ end
+end
+
+class OptionalDefaultUrlOptionsController < ActionController::Base
+ def show
+ head :ok
+ end
+
+ def default_url_options
+ { format: "atom", id: "default-id" }
+ end
+end
+
+class UrlOptionsController < ActionController::Base
+ def from_view
+ render inline: "<%= #{params[:route]} %>"
+ end
+
+ def url_options
+ super.merge(host: "www.override.com")
+ end
+end
+
+class RecordIdentifierIncludedController < ActionController::Base
+ include ActionView::RecordIdentifier
+end
+
+class ActionMissingController < ActionController::Base
+ def action_missing(action)
+ render plain: "Response for #{action}"
+ end
+end
+
+class ControllerClassTests < ActiveSupport::TestCase
+ def test_controller_path
+ assert_equal "empty", EmptyController.controller_path
+ assert_equal EmptyController.controller_path, EmptyController.new.controller_path
+ assert_equal "submodule/contained_empty", Submodule::ContainedEmptyController.controller_path
+ assert_equal Submodule::ContainedEmptyController.controller_path, Submodule::ContainedEmptyController.new.controller_path
+ end
+
+ def test_controller_name
+ assert_equal "empty", EmptyController.controller_name
+ assert_equal "contained_empty", Submodule::ContainedEmptyController.controller_name
+ end
+
+ def test_no_deprecation_when_action_view_record_identifier_is_included
+ record = Comment.new
+ record.save
+
+ dom_id = nil
+ assert_not_deprecated do
+ dom_id = RecordIdentifierIncludedController.new.dom_id(record)
+ end
+
+ assert_equal "comment_1", dom_id
+
+ dom_class = nil
+ assert_not_deprecated do
+ dom_class = RecordIdentifierIncludedController.new.dom_class(record)
+ end
+ assert_equal "comment", dom_class
+ end
+end
+
+class ControllerInstanceTests < ActiveSupport::TestCase
+ def setup
+ @empty = EmptyController.new
+ @empty.set_request!(ActionDispatch::Request.empty)
+ @empty.set_response!(EmptyController.make_response!(@empty.request))
+ @contained = Submodule::ContainedEmptyController.new
+ @empty_controllers = [@empty, @contained]
+ end
+
+ def test_performed?
+ assert_not_predicate @empty, :performed?
+ @empty.response_body = ["sweet"]
+ assert_predicate @empty, :performed?
+ end
+
+ def test_action_methods
+ @empty_controllers.each do |c|
+ assert_equal Set.new, c.class.action_methods, "#{c.controller_path} should be empty!"
+ end
+ end
+
+ def test_temporary_anonymous_controllers
+ name = "ExamplesController"
+ klass = Class.new(ActionController::Base)
+ Object.const_set(name, klass)
+
+ controller = klass.new
+ assert_equal "examples", controller.controller_path
+ end
+
+ def test_response_has_default_headers
+ original_default_headers = ActionDispatch::Response.default_headers
+
+ ActionDispatch::Response.default_headers = {
+ "X-Frame-Options" => "DENY",
+ "X-Content-Type-Options" => "nosniff",
+ "X-XSS-Protection" => "1;"
+ }
+
+ response_headers = SimpleController.action("hello").call(
+ "REQUEST_METHOD" => "GET",
+ "rack.input" => -> { }
+ )[1]
+
+ assert response_headers.key?("X-Frame-Options")
+ assert response_headers.key?("X-Content-Type-Options")
+ assert response_headers.key?("X-XSS-Protection")
+ ensure
+ ActionDispatch::Response.default_headers = original_default_headers
+ end
+end
+
+class PerformActionTest < ActionController::TestCase
+ def use_controller(controller_class)
+ @controller = controller_class.new
+
+ # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
+ # a more accurate simulation of what happens in "real life".
+ @controller.logger = ActiveSupport::Logger.new(nil)
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_process_should_be_precise
+ use_controller EmptyController
+ exception = assert_raise AbstractController::ActionNotFound do
+ get :non_existent
+ end
+ assert_equal "The action 'non_existent' could not be found for EmptyController", exception.message
+ end
+
+ def test_action_missing_should_work
+ use_controller ActionMissingController
+ get :arbitrary_action
+ assert_equal "Response for arbitrary_action", @response.body
+ end
+end
+
+class UrlOptionsTest < ActionController::TestCase
+ tests UrlOptionsController
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ end
+
+ def test_url_for_query_params_included
+ rs = ActionDispatch::Routing::RouteSet.new
+ rs.draw do
+ get "home" => "pages#home"
+ end
+
+ options = {
+ action: "home",
+ controller: "pages",
+ only_path: true,
+ token: "secret"
+ }
+
+ assert_equal "/home?token=secret", rs.url_for(options)
+ end
+
+ def test_url_options_override
+ with_routing do |set|
+ set.draw do
+ get "from_view", to: "url_options#from_view", as: :from_view
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+
+ get :from_view, params: { route: "from_view_url" }
+
+ assert_equal "http://www.override.com/from_view", @response.body
+ assert_equal "http://www.override.com/from_view", @controller.send(:from_view_url)
+ assert_equal "http://www.override.com/default_url_options/index", @controller.url_for(controller: "default_url_options")
+ end
+ end
+
+ def test_url_helpers_does_not_become_actions
+ with_routing do |set|
+ set.draw do
+ get "account/overview"
+ end
+
+ assert_not_includes @controller.class.action_methods, "account_overview_path"
+ end
+ end
+end
+
+class DefaultUrlOptionsTest < ActionController::TestCase
+ tests DefaultUrlOptionsController
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ end
+
+ def test_default_url_options_override
+ with_routing do |set|
+ set.draw do
+ get "from_view", to: "default_url_options#from_view", as: :from_view
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+
+ get :from_view, params: { route: "from_view_url" }
+
+ assert_equal "http://www.override.com/from_view?locale=en", @response.body
+ assert_equal "http://www.override.com/from_view?locale=en", @controller.send(:from_view_url)
+ assert_equal "http://www.override.com/default_url_options/new?locale=en", @controller.url_for(controller: "default_url_options")
+ end
+ end
+
+ def test_default_url_options_are_used_in_non_positional_parameters
+ with_routing do |set|
+ set.draw do
+ scope("/:locale") do
+ resources :descriptions
+ end
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+
+ get :from_view, params: { route: "description_path(1)" }
+
+ assert_equal "/en/descriptions/1", @response.body
+ assert_equal "/en/descriptions", @controller.send(:descriptions_path)
+ assert_equal "/pl/descriptions", @controller.send(:descriptions_path, "pl")
+ assert_equal "/pl/descriptions", @controller.send(:descriptions_path, locale: "pl")
+ assert_equal "/pl/descriptions.xml", @controller.send(:descriptions_path, "pl", "xml")
+ assert_equal "/en/descriptions.xml", @controller.send(:descriptions_path, format: "xml")
+ assert_equal "/en/descriptions/1", @controller.send(:description_path, 1)
+ assert_equal "/pl/descriptions/1", @controller.send(:description_path, "pl", 1)
+ assert_equal "/pl/descriptions/1", @controller.send(:description_path, 1, locale: "pl")
+ assert_equal "/pl/descriptions/1.xml", @controller.send(:description_path, "pl", 1, "xml")
+ assert_equal "/en/descriptions/1.xml", @controller.send(:description_path, 1, format: "xml")
+ end
+ end
+end
+
+class OptionalDefaultUrlOptionsControllerTest < ActionController::TestCase
+ def test_default_url_options_override_missing_positional_arguments
+ with_routing do |set|
+ set.draw do
+ get "/things/:id(.:format)" => "things#show", :as => :thing
+ end
+ assert_equal "/things/1.atom", thing_path("1")
+ assert_equal "/things/default-id.atom", thing_path
+ end
+ end
+end
+
+class EmptyUrlOptionsTest < ActionController::TestCase
+ tests NonEmptyController
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ end
+
+ def test_ensure_url_for_works_as_expected_when_called_with_no_options_if_default_url_options_is_not_set
+ get :public_action
+ assert_equal "http://www.example.com/non_empty/public_action", @controller.url_for
+ end
+
+ def test_named_routes_with_path_without_doing_a_request_first
+ @controller = EmptyController.new
+ @controller.request = @request
+
+ with_routing do |set|
+ set.draw do
+ resources :things
+ end
+
+ assert_equal "/things", @controller.send(:things_path)
+ end
+ end
+end
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
new file mode 100644
index 0000000000..6fe036dd15
--- /dev/null
+++ b/actionpack/test/controller/caching_test.rb
@@ -0,0 +1,511 @@
+# frozen_string_literal: true
+
+require "fileutils"
+require "abstract_unit"
+require "lib/controller/fake_models"
+
+CACHE_DIR = "test_cache"
+# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
+FILE_STORE_PATH = File.join(__dir__, "../temp/", CACHE_DIR)
+
+class FragmentCachingMetalTestController < ActionController::Metal
+ abstract!
+
+ include ActionController::Caching
+
+ def some_action; end
+end
+
+class FragmentCachingMetalTest < ActionController::TestCase
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @controller = FragmentCachingMetalTestController.new
+ @controller.perform_caching = true
+ @controller.cache_store = @store
+ @params = { controller: "posts", action: "index" }
+ @controller.params = @params
+ @controller.request = @request
+ @controller.response = @response
+ end
+end
+
+class CachingController < ActionController::Base
+ abstract!
+
+ self.cache_store = :file_store, FILE_STORE_PATH
+end
+
+class FragmentCachingTestController < CachingController
+ def some_action; end
+end
+
+class FragmentCachingTest < ActionController::TestCase
+ ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version)
+
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @controller = FragmentCachingTestController.new
+ @controller.perform_caching = true
+ @controller.cache_store = @store
+ @params = { controller: "posts", action: "index" }
+ @controller.params = @params
+ @controller.request = @request
+ @controller.response = @response
+
+ @m1v1 = ModelWithKeyAndVersion.new("model/1", "1")
+ @m1v2 = ModelWithKeyAndVersion.new("model/1", "2")
+ @m2v1 = ModelWithKeyAndVersion.new("model/2", "1")
+ @m2v2 = ModelWithKeyAndVersion.new("model/2", "2")
+ end
+
+ def test_fragment_cache_key
+ assert_deprecated do
+ assert_equal "views/what a key", @controller.fragment_cache_key("what a key")
+ assert_equal "views/test.host/fragment_caching_test/some_action",
+ @controller.fragment_cache_key(controller: "fragment_caching_test", action: "some_action")
+ end
+ end
+
+ def test_combined_fragment_cache_key
+ assert_equal [ :views, "what a key" ], @controller.combined_fragment_cache_key("what a key")
+ assert_equal [ :views, "test.host/fragment_caching_test/some_action" ],
+ @controller.combined_fragment_cache_key(controller: "fragment_caching_test", action: "some_action")
+ end
+
+ def test_read_fragment_with_caching_enabled
+ @store.write("views/name", "value")
+ assert_equal "value", @controller.read_fragment("name")
+ end
+
+ def test_read_fragment_with_caching_disabled
+ @controller.perform_caching = false
+ @store.write("views/name", "value")
+ assert_nil @controller.read_fragment("name")
+ end
+
+ def test_read_fragment_with_versioned_model
+ @controller.write_fragment([ "stuff", @m1v1 ], "hello")
+ assert_equal "hello", @controller.read_fragment([ "stuff", @m1v1 ])
+ assert_nil @controller.read_fragment([ "stuff", @m1v2 ])
+ end
+
+ def test_fragment_exist_with_caching_enabled
+ @store.write("views/name", "value")
+ assert @controller.fragment_exist?("name")
+ assert_not @controller.fragment_exist?("other_name")
+ end
+
+ def test_fragment_exist_with_caching_disabled
+ @controller.perform_caching = false
+ @store.write("views/name", "value")
+ assert_not @controller.fragment_exist?("name")
+ assert_not @controller.fragment_exist?("other_name")
+ end
+
+ def test_write_fragment_with_caching_enabled
+ assert_nil @store.read("views/name")
+ assert_equal "value", @controller.write_fragment("name", "value")
+ assert_equal "value", @store.read("views/name")
+ end
+
+ def test_write_fragment_with_caching_disabled
+ assert_nil @store.read("views/name")
+ @controller.perform_caching = false
+ assert_equal "value", @controller.write_fragment("name", "value")
+ assert_nil @store.read("views/name")
+ end
+
+ def test_expire_fragment_with_simple_key
+ @store.write("views/name", "value")
+ @controller.expire_fragment "name"
+ assert_nil @store.read("views/name")
+ end
+
+ def test_expire_fragment_with_regexp
+ @store.write("views/name", "value")
+ @store.write("views/another_name", "another_value")
+ @store.write("views/primalgrasp", "will not expire ;-)")
+
+ @controller.expire_fragment(/name/)
+
+ assert_nil @store.read("views/name")
+ assert_nil @store.read("views/another_name")
+ assert_equal "will not expire ;-)", @store.read("views/primalgrasp")
+ end
+
+ def test_fragment_for
+ @store.write("views/expensive", "fragment content")
+ fragment_computed = false
+
+ view_context = @controller.view_context
+
+ buffer = "generated till now -> ".html_safe
+ buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true }
+
+ assert_not fragment_computed
+ assert_equal "generated till now -> fragment content", buffer
+ end
+
+ def test_html_safety
+ assert_nil @store.read("views/name")
+ content = "value".html_safe
+ assert_equal content, @controller.write_fragment("name", content)
+
+ cached = @store.read("views/name")
+ assert_equal content, cached
+ assert_equal String, cached.class
+
+ html_safe = @controller.read_fragment("name")
+ assert_equal content, html_safe
+ assert_predicate html_safe, :html_safe?
+ end
+end
+
+class FunctionalCachingController < CachingController
+ def fragment_cached
+ end
+
+ def html_fragment_cached_with_partial
+ respond_to do |format|
+ format.html
+ end
+ end
+
+ def xml_fragment_cached_with_html_partial
+ end
+
+ def formatted_fragment_cached
+ respond_to do |format|
+ format.html
+ format.xml
+ end
+ end
+
+ def formatted_fragment_cached_with_variant
+ request.variant = :phone if params[:v] == "phone"
+
+ respond_to do |format|
+ format.html.phone
+ format.html
+ end
+ end
+
+ def fragment_cached_without_digest
+ end
+
+ def fragment_cached_with_options
+ end
+end
+
+class FunctionalFragmentCachingTest < ActionController::TestCase
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @controller = FunctionalCachingController.new
+ @controller.perform_caching = true
+ @controller.cache_store = @store
+ @controller.enable_fragment_cache_logging = true
+ end
+
+ def test_fragment_caching
+ get :fragment_cached
+ assert_response :success
+ expected_body = <<-CACHED
+Hello
+This bit's fragment cached
+Ciao
+CACHED
+ assert_equal expected_body, @response.body
+
+ assert_equal "This bit's fragment cached",
+ @store.read("views/functional_caching/fragment_cached:#{template_digest("functional_caching/fragment_cached")}/fragment")
+ end
+
+ def test_fragment_caching_in_partials
+ get :html_fragment_cached_with_partial
+ assert_response :success
+ assert_match(/Old fragment caching in a partial/, @response.body)
+
+ assert_match("Old fragment caching in a partial",
+ @store.read("views/functional_caching/_partial:#{template_digest("functional_caching/_partial")}/test.host/functional_caching/html_fragment_cached_with_partial"))
+ end
+
+ def test_skipping_fragment_cache_digesting
+ get :fragment_cached_without_digest, format: "html"
+ assert_response :success
+ expected_body = "<body>\n<p>ERB</p>\n</body>\n"
+
+ assert_equal expected_body, @response.body
+ assert_equal "<p>ERB</p>", @store.read("views/nodigest")
+ end
+
+ def test_fragment_caching_with_options
+ time = Time.now
+ get :fragment_cached_with_options
+ assert_response :success
+ expected_body = "<body>\n<p>ERB</p>\n</body>\n"
+
+ assert_equal expected_body, @response.body
+ Time.stub(:now, time + 11) do
+ assert_nil @store.read("views/with_options")
+ end
+ end
+
+ def test_render_inline_before_fragment_caching
+ get :inline_fragment_cached
+ assert_response :success
+ assert_match(/Some inline content/, @response.body)
+ assert_match(/Some cached content/, @response.body)
+ assert_match("Some cached content",
+ @store.read("views/functional_caching/inline_fragment_cached:#{template_digest("functional_caching/inline_fragment_cached")}/test.host/functional_caching/inline_fragment_cached"))
+ end
+
+ def test_fragment_cache_instrumentation
+ payload = nil
+
+ subscriber = proc do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ payload = event.payload
+ end
+
+ ActiveSupport::Notifications.subscribed(subscriber, "read_fragment.action_controller") do
+ get :inline_fragment_cached
+ end
+
+ assert_equal "functional_caching", payload[:controller]
+ assert_equal "inline_fragment_cached", payload[:action]
+ end
+
+ def test_html_formatted_fragment_caching
+ get :formatted_fragment_cached, format: "html"
+ assert_response :success
+ expected_body = "<body>\n<p>ERB</p>\n</body>\n"
+
+ assert_equal expected_body, @response.body
+
+ assert_equal "<p>ERB</p>",
+ @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment")
+ end
+
+ def test_xml_formatted_fragment_caching
+ get :formatted_fragment_cached, format: "xml"
+ assert_response :success
+ expected_body = "<body>\n <p>Builder</p>\n</body>\n"
+
+ assert_equal expected_body, @response.body
+
+ assert_equal " <p>Builder</p>\n",
+ @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment")
+ end
+
+ def test_fragment_caching_with_variant
+ get :formatted_fragment_cached_with_variant, format: "html", params: { v: :phone }
+ assert_response :success
+ expected_body = "<body>\n<p>PHONE</p>\n</body>\n"
+
+ assert_equal expected_body, @response.body
+
+ assert_equal "<p>PHONE</p>",
+ @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment")
+ end
+
+ def test_fragment_caching_with_html_partials_in_xml
+ get :xml_fragment_cached_with_html_partial, format: "*/*"
+ assert_response :success
+ end
+
+ private
+ def template_digest(name)
+ ActionView::Digestor.digest(name: name, finder: @controller.lookup_context)
+ end
+end
+
+class CacheHelperOutputBufferTest < ActionController::TestCase
+ class MockController
+ def read_fragment(name, options)
+ false
+ end
+
+ def write_fragment(name, fragment, options)
+ fragment
+ end
+ end
+
+ def setup
+ super
+ end
+
+ def test_output_buffer
+ output_buffer = ActionView::OutputBuffer.new
+ controller = MockController.new
+ cache_helper = Class.new do
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
+ end
+ cache_helper.extend(ActionView::Helpers::CacheHelper)
+
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
+ end
+ end
+ end
+ end
+ end
+
+ def test_safe_buffer
+ output_buffer = ActiveSupport::SafeBuffer.new
+ controller = MockController.new
+ cache_helper = Class.new do
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
+ end
+ cache_helper.extend(ActionView::Helpers::CacheHelper)
+
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
+ end
+ end
+ end
+ end
+ end
+end
+
+class ViewCacheDependencyTest < ActionController::TestCase
+ class NoDependenciesController < ActionController::Base
+ end
+
+ class HasDependenciesController < ActionController::Base
+ view_cache_dependency { "trombone" }
+ view_cache_dependency { "flute" }
+ end
+
+ def test_view_cache_dependencies_are_empty_by_default
+ assert_empty NoDependenciesController.new.view_cache_dependencies
+ end
+
+ def test_view_cache_dependencies_are_listed_in_declaration_order
+ assert_equal %w(trombone flute), HasDependenciesController.new.view_cache_dependencies
+ end
+end
+
+class CollectionCacheController < ActionController::Base
+ attr_accessor :partial_rendered_times
+
+ def index
+ @customers = [Customer.new("david", params[:id] || 1)]
+ end
+
+ def index_ordered
+ @customers = [Customer.new("david", 1), Customer.new("david", 2), Customer.new("david", 3)]
+ render "index"
+ end
+
+ def index_explicit_render_in_controller
+ @customers = [Customer.new("david", 1)]
+ render partial: "customers/customer", collection: @customers, cached: true
+ end
+
+ def index_with_comment
+ @customers = [Customer.new("david", 1)]
+ render partial: "customers/commented_customer", collection: @customers, as: :customer, cached: true
+ end
+
+ def index_with_callable_cache_key
+ @customers = [Customer.new("david", 1)]
+ render partial: "customers/customer", collection: @customers, cached: -> customer { "cached_david" }
+ end
+end
+
+class CollectionCacheTest < ActionController::TestCase
+ def setup
+ super
+ @controller = CollectionCacheController.new
+ @controller.perform_caching = true
+ @controller.partial_rendered_times = 0
+ @controller.cache_store = ActiveSupport::Cache::MemoryStore.new
+ ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new
+ end
+
+ def test_collection_fetches_cached_views
+ get :index
+ assert_equal 1, @controller.partial_rendered_times
+ assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/david/1")
+
+ get :index
+ assert_equal 1, @controller.partial_rendered_times
+ end
+
+ def test_preserves_order_when_reading_from_cache_plus_rendering
+ get :index, params: { id: 2 }
+ assert_equal 1, @controller.partial_rendered_times
+ assert_select ":root", "david, 2"
+
+ get :index_ordered
+ assert_equal 3, @controller.partial_rendered_times
+ assert_select ":root", "david, 1\n david, 2\n david, 3"
+ end
+
+ def test_explicit_render_call_with_options
+ get :index_explicit_render_in_controller
+
+ assert_select ":root", "david, 1"
+ end
+
+ def test_caching_works_with_beginning_comment
+ get :index_with_comment
+ assert_equal 1, @controller.partial_rendered_times
+
+ get :index_with_comment
+ assert_equal 1, @controller.partial_rendered_times
+ end
+
+ def test_caching_with_callable_cache_key
+ get :index_with_callable_cache_key
+ assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/cached_david")
+ end
+end
+
+class FragmentCacheKeyTestController < CachingController
+ attr_accessor :account_id
+
+ fragment_cache_key "v1"
+ fragment_cache_key { account_id }
+end
+
+class FragmentCacheKeyTest < ActionController::TestCase
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @controller = FragmentCacheKeyTestController.new
+ @controller.perform_caching = true
+ @controller.cache_store = @store
+ end
+
+ def test_combined_fragment_cache_key
+ @controller.account_id = "123"
+ assert_equal [ :views, "v1", "123", "what a key" ], @controller.combined_fragment_cache_key("what a key")
+
+ @controller.account_id = nil
+ assert_equal [ :views, "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key")
+ end
+
+ def test_combined_fragment_cache_key_with_envs
+ ENV["RAILS_APP_VERSION"] = "55"
+ assert_equal [ :views, "55", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key")
+
+ ENV["RAILS_CACHE_ID"] = "66"
+ assert_equal [ :views, "66", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key")
+ ensure
+ ENV["RAILS_CACHE_ID"] = ENV["RAILS_APP_VERSION"] = nil
+ end
+end
diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb
new file mode 100644
index 0000000000..636b025f2c
--- /dev/null
+++ b/actionpack/test/controller/content_type_test.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class OldContentTypeController < ActionController::Base
+ # :ported:
+ def render_content_type_from_body
+ response.content_type = Mime[:rss]
+ render body: "hello world!"
+ end
+
+ # :ported:
+ def render_defaults
+ render body: "hello world!"
+ end
+
+ # :ported:
+ def render_content_type_from_render
+ render body: "hello world!", content_type: Mime[:rss]
+ end
+
+ # :ported:
+ def render_charset_from_body
+ response.charset = "utf-16"
+ render body: "hello world!"
+ end
+
+ # :ported:
+ def render_nil_charset_from_body
+ response.charset = nil
+ render body: "hello world!"
+ end
+
+ def render_default_for_erb
+ end
+
+ def render_default_for_builder
+ end
+
+ def render_change_for_builder
+ response.content_type = Mime[:html]
+ render action: "render_default_for_builder"
+ end
+
+ def render_default_content_types_for_respond_to
+ respond_to do |format|
+ format.html { render body: "hello world!" }
+ format.xml { render action: "render_default_content_types_for_respond_to" }
+ format.js { render body: "hello world!" }
+ format.rss { render body: "hello world!", content_type: Mime[:xml] }
+ end
+ end
+end
+
+class ContentTypeTest < ActionController::TestCase
+ tests OldContentTypeController
+
+ def setup
+ super
+ # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
+ # a more accurate simulation of what happens in "real life".
+ @controller.logger = ActiveSupport::Logger.new(nil)
+ end
+
+ # :ported:
+ def test_render_defaults
+ get :render_defaults
+ assert_equal "utf-8", @response.charset
+ assert_equal Mime[:text], @response.content_type
+ end
+
+ def test_render_changed_charset_default
+ with_default_charset "utf-16" do
+ get :render_defaults
+ assert_equal "utf-16", @response.charset
+ assert_equal Mime[:text], @response.content_type
+ end
+ end
+
+ # :ported:
+ def test_content_type_from_body
+ get :render_content_type_from_body
+ assert_equal Mime[:rss], @response.content_type
+ assert_equal "utf-8", @response.charset
+ end
+
+ # :ported:
+ def test_content_type_from_render
+ get :render_content_type_from_render
+ assert_equal Mime[:rss], @response.content_type
+ assert_equal "utf-8", @response.charset
+ end
+
+ # :ported:
+ def test_charset_from_body
+ get :render_charset_from_body
+ assert_equal Mime[:text], @response.content_type
+ assert_equal "utf-16", @response.charset
+ end
+
+ # :ported:
+ def test_nil_charset_from_body
+ get :render_nil_charset_from_body
+ assert_equal Mime[:text], @response.content_type
+ assert_equal "utf-8", @response.charset, @response.headers.inspect
+ end
+
+ def test_nil_default_for_erb
+ with_default_charset nil do
+ get :render_default_for_erb
+ assert_equal Mime[:html], @response.content_type
+ assert_nil @response.charset, @response.headers.inspect
+ end
+ end
+
+ def test_default_for_erb
+ get :render_default_for_erb
+ assert_equal Mime[:html], @response.content_type
+ assert_equal "utf-8", @response.charset
+ end
+
+ def test_default_for_builder
+ get :render_default_for_builder
+ assert_equal Mime[:xml], @response.content_type
+ assert_equal "utf-8", @response.charset
+ end
+
+ def test_change_for_builder
+ get :render_change_for_builder
+ assert_equal Mime[:html], @response.content_type
+ assert_equal "utf-8", @response.charset
+ end
+
+ private
+
+ def with_default_charset(charset)
+ old_default_charset = ActionDispatch::Response.default_charset
+ ActionDispatch::Response.default_charset = charset
+ yield
+ ensure
+ ActionDispatch::Response.default_charset = old_default_charset
+ end
+end
+
+class AcceptBasedContentTypeTest < ActionController::TestCase
+ tests OldContentTypeController
+
+ def test_render_default_content_types_for_respond_to
+ @request.accept = Mime[:html].to_s
+ get :render_default_content_types_for_respond_to
+ assert_equal Mime[:html], @response.content_type
+
+ @request.accept = Mime[:js].to_s
+ get :render_default_content_types_for_respond_to
+ assert_equal Mime[:js], @response.content_type
+ end
+
+ def test_render_default_content_types_for_respond_to_with_template
+ @request.accept = Mime[:xml].to_s
+ get :render_default_content_types_for_respond_to
+ assert_equal Mime[:xml], @response.content_type
+ end
+
+ def test_render_default_content_types_for_respond_to_with_overwrite
+ @request.accept = Mime[:rss].to_s
+ get :render_default_content_types_for_respond_to
+ assert_equal Mime[:xml], @response.content_type
+ end
+end
diff --git a/actionpack/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb b/actionpack/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionpack/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb
diff --git a/actionpack/test/controller/controller_fixtures/app/controllers/user_controller.rb b/actionpack/test/controller/controller_fixtures/app/controllers/user_controller.rb
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionpack/test/controller/controller_fixtures/app/controllers/user_controller.rb
diff --git a/actionpack/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb b/actionpack/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionpack/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb
diff --git a/actionpack/test/controller/default_url_options_with_before_action_test.rb b/actionpack/test/controller/default_url_options_with_before_action_test.rb
new file mode 100644
index 0000000000..fc5b8288cd
--- /dev/null
+++ b/actionpack/test/controller/default_url_options_with_before_action_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ControllerWithBeforeActionAndDefaultUrlOptions < ActionController::Base
+ before_action { I18n.locale = params[:locale] }
+ after_action { I18n.locale = "en" }
+
+ def target
+ render plain: "final response"
+ end
+
+ def redirect
+ redirect_to action: "target"
+ end
+
+ def default_url_options
+ { locale: "de" }
+ end
+end
+
+class ControllerWithBeforeActionAndDefaultUrlOptionsTest < ActionController::TestCase
+ # This test has its roots in issue #1872
+ test "should redirect with correct locale :de" do
+ get :redirect, params: { locale: "de" }
+ assert_redirected_to "/controller_with_before_action_and_default_url_options/target?locale=de"
+ end
+end
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
new file mode 100644
index 0000000000..104c9eeade
--- /dev/null
+++ b/actionpack/test/controller/filters_test.rb
@@ -0,0 +1,1048 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ActionController::Base
+ class << self
+ %w(append_around_action prepend_after_action prepend_around_action prepend_before_action skip_after_action skip_before_action).each do |pending|
+ define_method(pending) do |*args|
+ $stderr.puts "#{pending} unimplemented: #{args.inspect}"
+ end unless method_defined?(pending)
+ end
+
+ def before_actions
+ filters = _process_action_callbacks.select { |c| c.kind == :before }
+ filters.map!(&:raw_filter)
+ end
+ end
+end
+
+class FilterTest < ActionController::TestCase
+ class TestController < ActionController::Base
+ before_action :ensure_login
+ after_action :clean_up
+
+ def show
+ render inline: "ran action"
+ end
+
+ private
+ def ensure_login
+ @ran_filter ||= []
+ @ran_filter << "ensure_login"
+ end
+
+ def clean_up
+ @ran_after_action ||= []
+ @ran_after_action << "clean_up"
+ end
+ end
+
+ class ChangingTheRequirementsController < TestController
+ before_action :ensure_login, except: [:go_wild]
+
+ def go_wild
+ render plain: "gobble"
+ end
+ end
+
+ class TestMultipleFiltersController < ActionController::Base
+ before_action :try_1
+ before_action :try_2
+ before_action :try_3
+
+ (1..3).each do |i|
+ define_method "fail_#{i}" do
+ render plain: i.to_s
+ end
+ end
+
+ private
+ (1..3).each do |i|
+ define_method "try_#{i}" do
+ instance_variable_set :@try, i
+ if action_name == "fail_#{i}"
+ head(404)
+ end
+ end
+ end
+ end
+
+ class RenderingController < ActionController::Base
+ before_action :before_action_rendering
+ after_action :unreached_after_action
+
+ def show
+ @ran_action = true
+ render inline: "ran action"
+ end
+
+ private
+ def before_action_rendering
+ @ran_filter ||= []
+ @ran_filter << "before_action_rendering"
+ render inline: "something else"
+ end
+
+ def unreached_after_action
+ @ran_filter << "unreached_after_action_after_render"
+ end
+ end
+
+ class RenderingForPrependAfterActionController < RenderingController
+ prepend_after_action :unreached_prepend_after_action
+
+ private
+ def unreached_prepend_after_action
+ @ran_filter << "unreached_preprend_after_action_after_render"
+ end
+ end
+
+ class BeforeActionRedirectionController < ActionController::Base
+ before_action :before_action_redirects
+ after_action :unreached_after_action
+
+ def show
+ @ran_action = true
+ render inline: "ran show action"
+ end
+
+ def target_of_redirection
+ @ran_target_of_redirection = true
+ render inline: "ran target_of_redirection action"
+ end
+
+ private
+ def before_action_redirects
+ @ran_filter ||= []
+ @ran_filter << "before_action_redirects"
+ redirect_to(action: "target_of_redirection")
+ end
+
+ def unreached_after_action
+ @ran_filter << "unreached_after_action_after_redirection"
+ end
+ end
+
+ class BeforeActionRedirectionForPrependAfterActionController < BeforeActionRedirectionController
+ prepend_after_action :unreached_prepend_after_action_after_redirection
+
+ private
+ def unreached_prepend_after_action_after_redirection
+ @ran_filter << "unreached_prepend_after_action_after_redirection"
+ end
+ end
+
+ class ConditionalFilterController < ActionController::Base
+ def show
+ render inline: "ran action"
+ end
+
+ def another_action
+ render inline: "ran action"
+ end
+
+ def show_without_action
+ render inline: "ran action without action"
+ end
+
+ private
+ def ensure_login
+ @ran_filter ||= []
+ @ran_filter << "ensure_login"
+ end
+
+ def clean_up_tmp
+ @ran_filter ||= []
+ @ran_filter << "clean_up_tmp"
+ end
+ end
+
+ class ConditionalCollectionFilterController < ConditionalFilterController
+ before_action :ensure_login, except: [ :show_without_action, :another_action ]
+ end
+
+ class OnlyConditionSymController < ConditionalFilterController
+ before_action :ensure_login, only: :show
+ end
+
+ class ExceptConditionSymController < ConditionalFilterController
+ before_action :ensure_login, except: :show_without_action
+ end
+
+ class BeforeAndAfterConditionController < ConditionalFilterController
+ before_action :ensure_login, only: :show
+ after_action :clean_up_tmp, only: :show
+ end
+
+ class OnlyConditionProcController < ConditionalFilterController
+ before_action(only: :show) { |c| c.instance_variable_set(:"@ran_proc_action", true) }
+ end
+
+ class ExceptConditionProcController < ConditionalFilterController
+ before_action(except: :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action", true) }
+ end
+
+ class ConditionalClassFilter
+ def self.before(controller) controller.instance_variable_set(:"@ran_class_action", true) end
+ end
+
+ class OnlyConditionClassController < ConditionalFilterController
+ before_action ConditionalClassFilter, only: :show
+ end
+
+ class ExceptConditionClassController < ConditionalFilterController
+ before_action ConditionalClassFilter, except: :show_without_action
+ end
+
+ class AnomolousYetValidConditionController < ConditionalFilterController
+ before_action(ConditionalClassFilter, :ensure_login, Proc.new { |c| c.instance_variable_set(:"@ran_proc_action1", true) }, except: :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action2", true) }
+ end
+
+ class OnlyConditionalOptionsFilter < ConditionalFilterController
+ before_action :ensure_login, only: :index, if: Proc.new { |c| c.instance_variable_set(:"@ran_conditional_index_proc", true) }
+ end
+
+ class ConditionalOptionsFilter < ConditionalFilterController
+ before_action :ensure_login, if: Proc.new { |c| true }
+ before_action :clean_up_tmp, if: Proc.new { |c| false }
+ end
+
+ class ConditionalOptionsSkipFilter < ConditionalFilterController
+ before_action :ensure_login
+ before_action :clean_up_tmp
+
+ skip_before_action :ensure_login, if: -> { false }
+ skip_before_action :clean_up_tmp, if: -> { true }
+ end
+
+ class SkipFilterUsingOnlyAndIf < ConditionalFilterController
+ before_action :clean_up_tmp
+ before_action :ensure_login
+
+ skip_before_action :ensure_login, only: :login, if: -> { false }
+ skip_before_action :clean_up_tmp, only: :login, if: -> { true }
+
+ def login
+ render plain: "ok"
+ end
+ end
+
+ class SkipFilterUsingIfAndExcept < ConditionalFilterController
+ before_action :clean_up_tmp
+ before_action :ensure_login
+
+ skip_before_action :ensure_login, if: -> { false }, except: :login
+ skip_before_action :clean_up_tmp, if: -> { true }, except: :login
+
+ def login
+ render plain: "ok"
+ end
+ end
+
+ class ClassController < ConditionalFilterController
+ before_action ConditionalClassFilter
+ end
+
+ class PrependingController < TestController
+ prepend_before_action :wonderful_life
+ # skip_before_action :fire_flash
+
+ private
+ def wonderful_life
+ @ran_filter ||= []
+ @ran_filter << "wonderful_life"
+ end
+ end
+
+ class SkippingAndLimitedController < TestController
+ skip_before_action :ensure_login
+ before_action :ensure_login, only: :index
+
+ def index
+ render plain: "ok"
+ end
+
+ def public
+ render plain: "ok"
+ end
+ end
+
+ class SkippingAndReorderingController < TestController
+ skip_before_action :ensure_login
+ before_action :find_record
+ before_action :ensure_login
+
+ def index
+ render plain: "ok"
+ end
+
+ private
+ def find_record
+ @ran_filter ||= []
+ @ran_filter << "find_record"
+ end
+ end
+
+ class ConditionalSkippingController < TestController
+ skip_before_action :ensure_login, only: [ :login ]
+ skip_after_action :clean_up, only: [ :login ]
+
+ before_action :find_user, only: [ :change_password ]
+
+ def login
+ render inline: "ran action"
+ end
+
+ def change_password
+ render inline: "ran action"
+ end
+
+ private
+ def find_user
+ @ran_filter ||= []
+ @ran_filter << "find_user"
+ end
+ end
+
+ class ConditionalParentOfConditionalSkippingController < ConditionalFilterController
+ before_action :conditional_in_parent_before, only: [:show, :another_action]
+ after_action :conditional_in_parent_after, only: [:show, :another_action]
+
+ private
+
+ def conditional_in_parent_before
+ @ran_filter ||= []
+ @ran_filter << "conditional_in_parent_before"
+ end
+
+ def conditional_in_parent_after
+ @ran_filter ||= []
+ @ran_filter << "conditional_in_parent_after"
+ end
+ end
+
+ class ChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController
+ skip_before_action :conditional_in_parent_before, only: :another_action
+ skip_after_action :conditional_in_parent_after, only: :another_action
+ end
+
+ class AnotherChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController
+ skip_before_action :conditional_in_parent_before, only: :show
+ end
+
+ class ProcController < PrependingController
+ before_action(proc { |c| c.instance_variable_set(:"@ran_proc_action", true) })
+ end
+
+ class ImplicitProcController < PrependingController
+ before_action { |c| c.instance_variable_set(:"@ran_proc_action", true) }
+ end
+
+ class AuditFilter
+ def self.before(controller)
+ controller.instance_variable_set(:"@was_audited", true)
+ end
+ end
+
+ class AroundFilter
+ def before(controller)
+ @execution_log = "before"
+ controller.class.execution_log += " before aroundfilter " if controller.respond_to? :execution_log
+ controller.instance_variable_set(:"@before_ran", true)
+ end
+
+ def after(controller)
+ controller.instance_variable_set(:"@execution_log", @execution_log + " and after")
+ controller.instance_variable_set(:"@after_ran", true)
+ controller.class.execution_log << " after aroundfilter " if controller.respond_to? :execution_log
+ end
+
+ def around(controller)
+ before(controller)
+ yield
+ after(controller)
+ end
+ end
+
+ class AppendedAroundFilter
+ def before(controller)
+ controller.class.execution_log << " before appended aroundfilter "
+ end
+
+ def after(controller)
+ controller.class.execution_log << " after appended aroundfilter "
+ end
+
+ def around(controller)
+ before(controller)
+ yield
+ after(controller)
+ end
+ end
+
+ class AuditController < ActionController::Base
+ before_action(AuditFilter)
+
+ def show
+ render plain: "hello"
+ end
+ end
+
+ class AroundFilterController < PrependingController
+ around_action AroundFilter.new
+ end
+
+ class BeforeAfterClassFilterController < PrependingController
+ begin
+ filter = AroundFilter.new
+ before_action filter
+ after_action filter
+ end
+ end
+
+ class MixedFilterController < PrependingController
+ cattr_accessor :execution_log
+
+ def initialize
+ @@execution_log = ""
+ super()
+ end
+
+ before_action { |c| c.class.execution_log << " before procfilter " }
+ prepend_around_action AroundFilter.new
+
+ after_action { |c| c.class.execution_log << " after procfilter " }
+ append_around_action AppendedAroundFilter.new
+ end
+
+ class MixedSpecializationController < ActionController::Base
+ class OutOfOrder < StandardError; end
+
+ before_action :first
+ before_action :second, only: :foo
+
+ def foo
+ render plain: "foo"
+ end
+
+ def bar
+ render plain: "bar"
+ end
+
+ private
+ def first
+ @first = true
+ end
+
+ def second
+ raise OutOfOrder unless @first
+ end
+ end
+
+ class DynamicDispatchController < ActionController::Base
+ before_action :choose
+
+ %w(foo bar baz).each do |action|
+ define_method(action) { render plain: action }
+ end
+
+ private
+ def choose
+ self.action_name = params[:choose]
+ end
+ end
+
+ class PrependingBeforeAndAfterController < ActionController::Base
+ prepend_before_action :before_all
+ prepend_after_action :after_all
+ before_action :between_before_all_and_after_all
+ after_action :between_before_all_and_after_all
+
+ def before_all
+ @ran_filter ||= []
+ @ran_filter << "before_all"
+ end
+
+ def after_all
+ @ran_filter ||= []
+ @ran_filter << "after_all"
+ end
+
+ def between_before_all_and_after_all
+ @ran_filter ||= []
+ @ran_filter << "between_before_all_and_after_all"
+ end
+
+ def show
+ render plain: "hello"
+ end
+ end
+
+ class ErrorToRescue < Exception; end
+
+ class RescuingAroundFilterWithBlock
+ def around(controller)
+ yield
+ rescue ErrorToRescue => ex
+ controller.__send__ :render, plain: "I rescued this: #{ex.inspect}"
+ end
+ end
+
+ class RescuedController < ActionController::Base
+ around_action RescuingAroundFilterWithBlock.new
+
+ def show
+ raise ErrorToRescue.new("Something made the bad noise.")
+ end
+ end
+
+ class NonYieldingAroundFilterController < ActionController::Base
+ before_action :filter_one
+ around_action :non_yielding_action
+ before_action :action_two
+ after_action :action_three
+
+ def index
+ render inline: "index"
+ end
+
+ private
+
+ def filter_one
+ @filters ||= []
+ @filters << "filter_one"
+ end
+
+ def action_two
+ @filters << "action_two"
+ end
+
+ def non_yielding_action
+ @filters << "it didn't yield"
+ end
+
+ def action_three
+ @filters << "action_three"
+ end
+ end
+
+ class ImplicitActionsController < ActionController::Base
+ before_action :find_only, only: :edit
+ before_action :find_except, except: :edit
+
+ private
+
+ def find_only
+ @only = "Only"
+ end
+
+ def find_except
+ @except = "Except"
+ end
+ end
+
+ def test_non_yielding_around_actions_do_not_raise
+ controller = NonYieldingAroundFilterController.new
+ assert_nothing_raised do
+ test_process(controller, "index")
+ end
+ end
+
+ def test_after_actions_are_not_run_if_around_action_does_not_yield
+ controller = NonYieldingAroundFilterController.new
+ test_process(controller, "index")
+ assert_equal ["filter_one", "it didn't yield"], controller.instance_variable_get(:@filters)
+ end
+
+ def test_added_action_to_inheritance_graph
+ assert_equal [ :ensure_login ], TestController.before_actions
+ end
+
+ def test_base_class_in_isolation
+ assert_equal [ ], ActionController::Base.before_actions
+ end
+
+ def test_prepending_action
+ assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_actions
+ end
+
+ def test_running_actions
+ test_process(PrependingController)
+ assert_equal %w( wonderful_life ensure_login ),
+ @controller.instance_variable_get(:@ran_filter)
+ end
+
+ def test_running_actions_with_proc
+ test_process(ProcController)
+ assert @controller.instance_variable_get(:@ran_proc_action)
+ end
+
+ def test_running_actions_with_implicit_proc
+ test_process(ImplicitProcController)
+ assert @controller.instance_variable_get(:@ran_proc_action)
+ end
+
+ def test_running_actions_with_class
+ test_process(AuditController)
+ assert @controller.instance_variable_get(:@was_audited)
+ end
+
+ def test_running_anomalous_yet_valid_condition_actions
+ test_process(AnomolousYetValidConditionController)
+ assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter)
+ assert @controller.instance_variable_get(:@ran_class_action)
+ assert @controller.instance_variable_get(:@ran_proc_action1)
+ assert @controller.instance_variable_get(:@ran_proc_action2)
+
+ test_process(AnomolousYetValidConditionController, "show_without_action")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+ assert_not @controller.instance_variable_defined?(:@ran_class_action)
+ assert_not @controller.instance_variable_defined?(:@ran_proc_action1)
+ assert_not @controller.instance_variable_defined?(:@ran_proc_action2)
+ end
+
+ def test_running_conditional_options
+ test_process(ConditionalOptionsFilter)
+ assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter)
+ end
+
+ def test_running_conditional_skip_options
+ test_process(ConditionalOptionsSkipFilter)
+ assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter)
+ end
+
+ def test_if_is_ignored_when_used_with_only
+ test_process(SkipFilterUsingOnlyAndIf, "login")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+ end
+
+ def test_except_is_ignored_when_used_with_if
+ test_process(SkipFilterUsingIfAndExcept, "login")
+ assert_equal %w(ensure_login), @controller.instance_variable_get(:@ran_filter)
+ end
+
+ def test_skipping_class_actions
+ test_process(ClassController)
+ assert_equal true, @controller.instance_variable_get(:@ran_class_action)
+
+ skipping_class_controller = Class.new(ClassController) do
+ skip_before_action ConditionalClassFilter
+ end
+
+ test_process(skipping_class_controller)
+ assert_not @controller.instance_variable_defined?(:@ran_class_action)
+ end
+
+ def test_running_collection_condition_actions
+ test_process(ConditionalCollectionFilterController)
+ assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter)
+ test_process(ConditionalCollectionFilterController, "show_without_action")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+ test_process(ConditionalCollectionFilterController, "another_action")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+ end
+
+ def test_running_only_condition_actions
+ test_process(OnlyConditionSymController)
+ assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter)
+ test_process(OnlyConditionSymController, "show_without_action")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+
+ test_process(OnlyConditionProcController)
+ assert @controller.instance_variable_get(:@ran_proc_action)
+ test_process(OnlyConditionProcController, "show_without_action")
+ assert_not @controller.instance_variable_defined?(:@ran_proc_action)
+
+ test_process(OnlyConditionClassController)
+ assert @controller.instance_variable_get(:@ran_class_action)
+ test_process(OnlyConditionClassController, "show_without_action")
+ assert_not @controller.instance_variable_defined?(:@ran_class_action)
+ end
+
+ def test_running_except_condition_actions
+ test_process(ExceptConditionSymController)
+ assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter)
+ test_process(ExceptConditionSymController, "show_without_action")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+
+ test_process(ExceptConditionProcController)
+ assert @controller.instance_variable_get(:@ran_proc_action)
+ test_process(ExceptConditionProcController, "show_without_action")
+ assert_not @controller.instance_variable_defined?(:@ran_proc_action)
+
+ test_process(ExceptConditionClassController)
+ assert @controller.instance_variable_get(:@ran_class_action)
+ test_process(ExceptConditionClassController, "show_without_action")
+ assert_not @controller.instance_variable_defined?(:@ran_class_action)
+ end
+
+ def test_running_only_condition_and_conditional_options
+ test_process(OnlyConditionalOptionsFilter, "show")
+ assert_not @controller.instance_variable_defined?(:@ran_conditional_index_proc)
+ end
+
+ def test_running_before_and_after_condition_actions
+ test_process(BeforeAndAfterConditionController)
+ assert_equal %w( ensure_login clean_up_tmp), @controller.instance_variable_get(:@ran_filter)
+ test_process(BeforeAndAfterConditionController, "show_without_action")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+ end
+
+ def test_around_action
+ test_process(AroundFilterController)
+ assert @controller.instance_variable_get(:@before_ran)
+ assert @controller.instance_variable_get(:@after_ran)
+ end
+
+ def test_before_after_class_action
+ test_process(BeforeAfterClassFilterController)
+ assert @controller.instance_variable_get(:@before_ran)
+ assert @controller.instance_variable_get(:@after_ran)
+ end
+
+ def test_having_properties_in_around_action
+ test_process(AroundFilterController)
+ assert_equal "before and after", @controller.instance_variable_get(:@execution_log)
+ end
+
+ def test_prepending_and_appending_around_action
+ test_process(MixedFilterController)
+ assert_equal " before aroundfilter before procfilter before appended aroundfilter " \
+ " after appended aroundfilter after procfilter after aroundfilter ",
+ MixedFilterController.execution_log
+ end
+
+ def test_rendering_breaks_actioning_chain
+ response = test_process(RenderingController)
+ assert_equal "something else", response.body
+ assert_not @controller.instance_variable_defined?(:@ran_action)
+ end
+
+ def test_before_action_rendering_breaks_actioning_chain_for_after_action
+ test_process(RenderingController)
+ assert_equal %w( before_action_rendering ), @controller.instance_variable_get(:@ran_filter)
+ assert_not @controller.instance_variable_defined?(:@ran_action)
+ end
+
+ def test_before_action_redirects_breaks_actioning_chain_for_after_action
+ test_process(BeforeActionRedirectionController)
+ assert_response :redirect
+ assert_equal "http://test.host/filter_test/before_action_redirection/target_of_redirection", redirect_to_url
+ assert_equal %w( before_action_redirects ), @controller.instance_variable_get(:@ran_filter)
+ end
+
+ def test_before_action_rendering_breaks_actioning_chain_for_preprend_after_action
+ test_process(RenderingForPrependAfterActionController)
+ assert_equal %w( before_action_rendering ), @controller.instance_variable_get(:@ran_filter)
+ assert_not @controller.instance_variable_defined?(:@ran_action)
+ end
+
+ def test_before_action_redirects_breaks_actioning_chain_for_preprend_after_action
+ test_process(BeforeActionRedirectionForPrependAfterActionController)
+ assert_response :redirect
+ assert_equal "http://test.host/filter_test/before_action_redirection_for_prepend_after_action/target_of_redirection", redirect_to_url
+ assert_equal %w( before_action_redirects ), @controller.instance_variable_get(:@ran_filter)
+ end
+
+ def test_actions_with_mixed_specialization_run_in_order
+ assert_nothing_raised do
+ response = test_process(MixedSpecializationController, "bar")
+ assert_equal "bar", response.body
+ end
+
+ assert_nothing_raised do
+ response = test_process(MixedSpecializationController, "foo")
+ assert_equal "foo", response.body
+ end
+ end
+
+ def test_dynamic_dispatch
+ %w(foo bar baz).each do |action|
+ @request.query_parameters[:choose] = action
+ response = DynamicDispatchController.action(action).call(@request.env).last
+ assert_equal action, response.body
+ end
+ end
+
+ def test_running_prepended_before_and_after_action
+ test_process(PrependingBeforeAndAfterController)
+ assert_equal %w( before_all between_before_all_and_after_all between_before_all_and_after_all after_all ), @controller.instance_variable_get(:@ran_filter)
+ end
+
+ def test_skipping_and_limiting_controller
+ test_process(SkippingAndLimitedController, "index")
+ assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter)
+ test_process(SkippingAndLimitedController, "public")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+ end
+
+ def test_skipping_and_reordering_controller
+ test_process(SkippingAndReorderingController, "index")
+ assert_equal %w( find_record ensure_login ), @controller.instance_variable_get(:@ran_filter)
+ end
+
+ def test_conditional_skipping_of_actions
+ test_process(ConditionalSkippingController, "login")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+ test_process(ConditionalSkippingController, "change_password")
+ assert_equal %w( ensure_login find_user ), @controller.instance_variable_get(:@ran_filter)
+
+ test_process(ConditionalSkippingController, "login")
+ assert_not @controller.instance_variable_defined?("@ran_after_action")
+ test_process(ConditionalSkippingController, "change_password")
+ assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_action")
+ end
+
+ def test_conditional_skipping_of_actions_when_parent_action_is_also_conditional
+ test_process(ChildOfConditionalParentController)
+ assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter)
+ test_process(ChildOfConditionalParentController, "another_action")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+ end
+
+ def test_condition_skipping_of_actions_when_siblings_also_have_conditions
+ test_process(ChildOfConditionalParentController)
+ assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter)
+ test_process(AnotherChildOfConditionalParentController)
+ assert_equal %w( conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter)
+ test_process(ChildOfConditionalParentController)
+ assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter)
+ end
+
+ def test_changing_the_requirements
+ test_process(ChangingTheRequirementsController, "go_wild")
+ assert_not @controller.instance_variable_defined?(:@ran_filter)
+ end
+
+ def test_a_rescuing_around_action
+ response = nil
+ assert_nothing_raised do
+ response = test_process(RescuedController)
+ end
+
+ assert_predicate response, :successful?
+ assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body)
+ end
+
+ def test_actions_obey_only_and_except_for_implicit_actions
+ test_process(ImplicitActionsController, "show")
+ assert_equal "Except", @controller.instance_variable_get(:@except)
+ assert_not @controller.instance_variable_defined?(:@only)
+ assert_equal "show", response.body
+
+ test_process(ImplicitActionsController, "edit")
+ assert_equal "Only", @controller.instance_variable_get(:@only)
+ assert_not @controller.instance_variable_defined?(:@except)
+ assert_equal "edit", response.body
+ end
+
+ private
+ def test_process(controller, action = "show")
+ @controller = controller.is_a?(Class) ? controller.new : controller
+
+ process(action)
+ end
+end
+
+class PostsController < ActionController::Base
+ module AroundExceptions
+ class Error < StandardError ; end
+ class Before < Error ; end
+ class After < Error ; end
+ end
+ include AroundExceptions
+
+ class DefaultFilter
+ include AroundExceptions
+ end
+
+ module_eval %w(raises_before raises_after raises_both no_raise no_action).map { |action| "def #{action}; default_action end" }.join("\n")
+
+ private
+ def default_action
+ render inline: "#{action_name} called"
+ end
+end
+
+class ControllerWithSymbolAsFilter < PostsController
+ around_action :raise_before, only: :raises_before
+ around_action :raise_after, only: :raises_after
+ around_action :without_exception, only: :no_raise
+
+ private
+ def raise_before
+ raise Before
+ yield
+ end
+
+ def raise_after
+ yield
+ raise After
+ end
+
+ def without_exception
+ # Do stuff...
+ wtf = 1 + 1
+
+ yield
+
+ # Do stuff...
+ wtf += 1
+ end
+end
+
+class ControllerWithFilterClass < PostsController
+ class YieldingFilter < DefaultFilter
+ def self.around(controller)
+ yield
+ raise After
+ end
+ end
+
+ around_action YieldingFilter, only: :raises_after
+end
+
+class ControllerWithFilterInstance < PostsController
+ class YieldingFilter < DefaultFilter
+ def around(controller)
+ yield
+ raise After
+ end
+ end
+
+ around_action YieldingFilter.new, only: :raises_after
+end
+
+class ControllerWithProcFilter < PostsController
+ around_action(only: :no_raise) do |c, b|
+ c.instance_variable_set(:"@before", true)
+ b.call
+ c.instance_variable_set(:"@after", true)
+ end
+end
+
+class ControllerWithNestedFilters < ControllerWithSymbolAsFilter
+ around_action :raise_before, :raise_after, :without_exception, only: :raises_both
+end
+
+class ControllerWithAllTypesOfFilters < PostsController
+ before_action :before
+ around_action :around
+ after_action :after
+ around_action :around_again
+
+ private
+ def before
+ @ran_filter ||= []
+ @ran_filter << "before"
+ end
+
+ def around
+ @ran_filter << "around (before yield)"
+ yield
+ @ran_filter << "around (after yield)"
+ end
+
+ def after
+ @ran_filter << "after"
+ end
+
+ def around_again
+ @ran_filter << "around_again (before yield)"
+ yield
+ @ran_filter << "around_again (after yield)"
+ end
+end
+
+class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters
+ skip_around_action :around_again
+ skip_after_action :after
+end
+
+class YieldingAroundFiltersTest < ActionController::TestCase
+ include PostsController::AroundExceptions
+
+ def test_base
+ controller = PostsController
+ assert_nothing_raised { test_process(controller, "no_raise") }
+ assert_nothing_raised { test_process(controller, "raises_before") }
+ assert_nothing_raised { test_process(controller, "raises_after") }
+ assert_nothing_raised { test_process(controller, "no_action") }
+ end
+
+ def test_with_symbol
+ controller = ControllerWithSymbolAsFilter
+ assert_nothing_raised { test_process(controller, "no_raise") }
+ assert_raise(Before) { test_process(controller, "raises_before") }
+ assert_raise(After) { test_process(controller, "raises_after") }
+ assert_nothing_raised { test_process(controller, "no_raise") }
+ end
+
+ def test_with_class
+ controller = ControllerWithFilterClass
+ assert_nothing_raised { test_process(controller, "no_raise") }
+ assert_raise(After) { test_process(controller, "raises_after") }
+ end
+
+ def test_with_instance
+ controller = ControllerWithFilterInstance
+ assert_nothing_raised { test_process(controller, "no_raise") }
+ assert_raise(After) { test_process(controller, "raises_after") }
+ end
+
+ def test_with_proc
+ test_process(ControllerWithProcFilter, "no_raise")
+ assert @controller.instance_variable_get(:@before)
+ assert @controller.instance_variable_get(:@after)
+ end
+
+ def test_nested_actions
+ controller = ControllerWithNestedFilters
+ assert_nothing_raised do
+ test_process(controller, "raises_both")
+ rescue Before, After
+ end
+ assert_raise Before do
+ test_process(controller, "raises_both")
+ rescue After
+ end
+ end
+
+ def test_action_order_with_all_action_types
+ test_process(ControllerWithAllTypesOfFilters, "no_raise")
+ assert_equal "before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)", @controller.instance_variable_get(:@ran_filter).join(" ")
+ end
+
+ def test_action_order_with_skip_action_method
+ test_process(ControllerWithTwoLessFilters, "no_raise")
+ assert_equal "before around (before yield) around (after yield)", @controller.instance_variable_get(:@ran_filter).join(" ")
+ end
+
+ def test_first_action_in_multiple_before_action_chain_halts
+ controller = ::FilterTest::TestMultipleFiltersController.new
+ response = test_process(controller, "fail_1")
+ assert_equal "", response.body
+ assert_equal 1, controller.instance_variable_get(:@try)
+ end
+
+ def test_second_action_in_multiple_before_action_chain_halts
+ controller = ::FilterTest::TestMultipleFiltersController.new
+ response = test_process(controller, "fail_2")
+ assert_equal "", response.body
+ assert_equal 2, controller.instance_variable_get(:@try)
+ end
+
+ def test_last_action_in_multiple_before_action_chain_halts
+ controller = ::FilterTest::TestMultipleFiltersController.new
+ response = test_process(controller, "fail_3")
+ assert_equal "", response.body
+ assert_equal 3, controller.instance_variable_get(:@try)
+ end
+
+ private
+ def test_process(controller, action = "show")
+ @controller = controller.is_a?(Class) ? controller.new : controller
+ process(action)
+ end
+end
diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb
new file mode 100644
index 0000000000..e3ec5bb7fc
--- /dev/null
+++ b/actionpack/test/controller/flash_hash_test.rb
@@ -0,0 +1,216 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ActionDispatch
+ class FlashHashTest < ActiveSupport::TestCase
+ def setup
+ @hash = Flash::FlashHash.new
+ end
+
+ def test_set_get
+ @hash[:foo] = "zomg"
+ assert_equal "zomg", @hash[:foo]
+ end
+
+ def test_keys
+ assert_equal [], @hash.keys
+
+ @hash["foo"] = "zomg"
+ assert_equal ["foo"], @hash.keys
+
+ @hash["bar"] = "zomg"
+ assert_equal ["foo", "bar"].sort, @hash.keys.sort
+ end
+
+ def test_update
+ @hash["foo"] = "bar"
+ @hash.update("foo" => "baz", "hello" => "world")
+
+ assert_equal "baz", @hash["foo"]
+ assert_equal "world", @hash["hello"]
+ end
+
+ def test_key
+ @hash["foo"] = "bar"
+
+ assert @hash.key?("foo")
+ assert @hash.key?(:foo)
+ assert_not @hash.key?("bar")
+ assert_not @hash.key?(:bar)
+ end
+
+ def test_delete
+ @hash["foo"] = "bar"
+ @hash.delete "foo"
+
+ assert_not @hash.key?("foo")
+ assert_nil @hash["foo"]
+ end
+
+ def test_to_hash
+ @hash["foo"] = "bar"
+ assert_equal({ "foo" => "bar" }, @hash.to_hash)
+
+ @hash.to_hash["zomg"] = "aaron"
+ assert_not @hash.key?("zomg")
+ assert_equal({ "foo" => "bar" }, @hash.to_hash)
+ end
+
+ def test_to_session_value
+ @hash["foo"] = "bar"
+ assert_equal({ "discard" => [], "flashes" => { "foo" => "bar" } }, @hash.to_session_value)
+
+ @hash.now["qux"] = 1
+ assert_equal({ "flashes" => { "foo" => "bar" }, "discard" => [] }, @hash.to_session_value)
+
+ @hash.discard("foo")
+ assert_nil(@hash.to_session_value)
+
+ @hash.sweep
+ assert_nil(@hash.to_session_value)
+ end
+
+ def test_from_session_value
+ # {"session_id"=>"f8e1b8152ba7609c28bbb17ec9263ba7", "flash"=>#<ActionDispatch::Flash::FlashHash:0x00000000000000 @used=#<Set: {"farewell"}>, @closed=false, @flashes={"greeting"=>"Hello", "farewell"=>"Goodbye"}, @now=nil>}
+ rails_3_2_cookie = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY4ZTFiODE1MmJhNzYwOWMyOGJiYjE3ZWM5MjYzYmE3BjsAVEkiCmZsYXNoBjsARm86JUFjdGlvbkRpc3BhdGNoOjpGbGFzaDo6Rmxhc2hIYXNoCToKQHVzZWRvOghTZXQGOgpAaGFzaHsGSSINZmFyZXdlbGwGOwBUVDoMQGNsb3NlZEY6DUBmbGFzaGVzewdJIg1ncmVldGluZwY7AFRJIgpIZWxsbwY7AFRJIg1mYXJld2VsbAY7AFRJIgxHb29kYnllBjsAVDoJQG5vdzA="
+ session = Marshal.load(Base64.decode64(rails_3_2_cookie))
+ hash = Flash::FlashHash.from_session_value(session["flash"])
+ assert_equal({ "greeting" => "Hello" }, hash.to_hash)
+ assert_nil(hash.to_session_value)
+ end
+
+ def test_from_session_value_on_json_serializer
+ decrypted_data = "{ \"session_id\":\"d98bdf6d129618fc2548c354c161cfb5\", \"flash\":{\"discard\":[\"farewell\"], \"flashes\":{\"greeting\":\"Hello\",\"farewell\":\"Goodbye\"}} }"
+ session = ActionDispatch::Cookies::JsonSerializer.load(decrypted_data)
+ hash = Flash::FlashHash.from_session_value(session["flash"])
+
+ assert_equal({ "greeting" => "Hello" }, hash.to_hash)
+ assert_nil(hash.to_session_value)
+ assert_equal "Hello", hash[:greeting]
+ assert_equal "Hello", hash["greeting"]
+ end
+
+ def test_empty?
+ assert_empty @hash
+ @hash["zomg"] = "bears"
+ assert_not_empty @hash
+ @hash.clear
+ assert_empty @hash
+ end
+
+ def test_each
+ @hash["hello"] = "world"
+ @hash["foo"] = "bar"
+
+ things = []
+ @hash.each do |k, v|
+ things << [k, v]
+ end
+
+ assert_equal([%w{ hello world }, %w{ foo bar }].sort, things.sort)
+ end
+
+ def test_replace
+ @hash["hello"] = "world"
+ @hash.replace("omg" => "aaron")
+ assert_equal({ "omg" => "aaron" }, @hash.to_hash)
+ end
+
+ def test_discard_no_args
+ @hash["hello"] = "world"
+ @hash.discard
+
+ @hash.sweep
+ assert_equal({}, @hash.to_hash)
+ end
+
+ def test_discard_one_arg
+ @hash["hello"] = "world"
+ @hash["omg"] = "world"
+ @hash.discard "hello"
+
+ @hash.sweep
+ assert_equal({ "omg" => "world" }, @hash.to_hash)
+ end
+
+ def test_keep_sweep
+ @hash["hello"] = "world"
+
+ @hash.sweep
+ assert_equal({ "hello" => "world" }, @hash.to_hash)
+ end
+
+ def test_update_sweep
+ @hash["hello"] = "world"
+ @hash.update("hi" => "mom")
+
+ @hash.sweep
+ assert_equal({ "hello" => "world", "hi" => "mom" }, @hash.to_hash)
+ end
+
+ def test_update_delete_sweep
+ @hash["hello"] = "world"
+ @hash.delete "hello"
+ @hash.update("hello" => "mom")
+
+ @hash.sweep
+ assert_equal({ "hello" => "mom" }, @hash.to_hash)
+ end
+
+ def test_delete_sweep
+ @hash["hello"] = "world"
+ @hash["hi"] = "mom"
+ @hash.delete "hi"
+
+ @hash.sweep
+ assert_equal({ "hello" => "world" }, @hash.to_hash)
+ end
+
+ def test_clear_sweep
+ @hash["hello"] = "world"
+ @hash.clear
+
+ @hash.sweep
+ assert_equal({}, @hash.to_hash)
+ end
+
+ def test_replace_sweep
+ @hash["hello"] = "world"
+ @hash.replace("hi" => "mom")
+
+ @hash.sweep
+ assert_equal({ "hi" => "mom" }, @hash.to_hash)
+ end
+
+ def test_discard_then_add
+ @hash["hello"] = "world"
+ @hash["omg"] = "world"
+ @hash.discard "hello"
+ @hash["hello"] = "world"
+
+ @hash.sweep
+ assert_equal({ "omg" => "world", "hello" => "world" }, @hash.to_hash)
+ end
+
+ def test_keep_all_sweep
+ @hash["hello"] = "world"
+ @hash["omg"] = "world"
+ @hash.discard "hello"
+ @hash.keep
+
+ @hash.sweep
+ assert_equal({ "omg" => "world", "hello" => "world" }, @hash.to_hash)
+ end
+
+ def test_double_sweep
+ @hash["hello"] = "world"
+ @hash.sweep
+
+ assert_equal({ "hello" => "world" }, @hash.to_hash)
+
+ @hash.sweep
+ assert_equal({}, @hash.to_hash)
+ end
+ end
+end
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
new file mode 100644
index 0000000000..409a4ec2e6
--- /dev/null
+++ b/actionpack/test/controller/flash_test.rb
@@ -0,0 +1,388 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/messages/rotation_configuration"
+
+class FlashTest < ActionController::TestCase
+ class TestController < ActionController::Base
+ def set_flash
+ flash["that"] = "hello"
+ render inline: "hello"
+ end
+
+ def set_flash_now
+ flash.now["that"] = "hello"
+ flash.now["foo"] ||= "bar"
+ flash.now["foo"] ||= "err"
+ @flashy = flash.now["that"]
+ @flash_copy = {}.update flash
+ render inline: "hello"
+ end
+
+ def attempt_to_use_flash_now
+ @flash_copy = {}.update flash
+ @flashy = flash["that"]
+ render inline: "hello"
+ end
+
+ def use_flash
+ @flash_copy = {}.update flash
+ @flashy = flash["that"]
+ render inline: "hello"
+ end
+
+ def use_flash_and_keep_it
+ @flash_copy = {}.update flash
+ @flashy = flash["that"]
+ flash.keep
+ render inline: "hello"
+ end
+
+ def use_flash_and_update_it
+ flash.update("this" => "hello again")
+ @flash_copy = {}.update flash
+ render inline: "hello"
+ end
+
+ def use_flash_after_reset_session
+ flash["that"] = "hello"
+ @flashy_that = flash["that"]
+ reset_session
+ @flashy_that_reset = flash["that"]
+ flash["this"] = "good-bye"
+ @flashy_this = flash["this"]
+ render inline: "hello"
+ end
+
+ # methods for test_sweep_after_halted_action_chain
+ before_action :halt_and_redir, only: "filter_halting_action"
+
+ def std_action
+ @flash_copy = {}.update(flash)
+ head :ok
+ end
+
+ def filter_halting_action
+ @flash_copy = {}.update(flash)
+ end
+
+ def halt_and_redir
+ flash["foo"] = "bar"
+ redirect_to action: "std_action"
+ @flash_copy = {}.update(flash)
+ end
+
+ def redirect_with_alert
+ redirect_to "/nowhere", alert: "Beware the nowheres!"
+ end
+
+ def redirect_with_notice
+ redirect_to "/somewhere", notice: "Good luck in the somewheres!"
+ end
+
+ def render_with_flash_now_alert
+ flash.now.alert = "Beware the nowheres now!"
+ render inline: "hello"
+ end
+
+ def render_with_flash_now_notice
+ flash.now.notice = "Good luck in the somewheres now!"
+ render inline: "hello"
+ end
+
+ def redirect_with_other_flashes
+ redirect_to "/wonderland", flash: { joyride: "Horses!" }
+ end
+
+ def redirect_with_foo_flash
+ redirect_to "/wonderland", foo: "for great justice"
+ end
+ end
+
+ tests TestController
+
+ def test_flash
+ get :set_flash
+
+ get :use_flash
+ assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"]
+ assert_equal "hello", @controller.instance_variable_get(:@flashy)
+
+ get :use_flash
+ assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On second flash"
+ end
+
+ def test_keep_flash
+ get :set_flash
+
+ get :use_flash_and_keep_it
+ assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"]
+ assert_equal "hello", @controller.instance_variable_get(:@flashy)
+
+ get :use_flash
+ assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"], "On second flash"
+
+ get :use_flash
+ assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On third flash"
+ end
+
+ def test_flash_now
+ get :set_flash_now
+ assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"]
+ assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"]
+ assert_equal "hello", @controller.instance_variable_get(:@flashy)
+
+ get :attempt_to_use_flash_now
+ assert_nil @controller.instance_variable_get(:@flash_copy)["that"]
+ assert_nil @controller.instance_variable_get(:@flash_copy)["foo"]
+ assert_nil @controller.instance_variable_get(:@flashy)
+ end
+
+ def test_update_flash
+ get :set_flash
+ get :use_flash_and_update_it
+ assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"]
+ assert_equal "hello again", @controller.instance_variable_get(:@flash_copy)["this"]
+ get :use_flash
+ assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On second flash"
+ assert_equal "hello again",
+ @controller.instance_variable_get(:@flash_copy)["this"], "On second flash"
+ end
+
+ def test_flash_after_reset_session
+ get :use_flash_after_reset_session
+ assert_equal "hello", @controller.instance_variable_get(:@flashy_that)
+ assert_equal "good-bye", @controller.instance_variable_get(:@flashy_this)
+ assert_nil @controller.instance_variable_get(:@flashy_that_reset)
+ end
+
+ def test_does_not_set_the_session_if_the_flash_is_empty
+ get :std_action
+ assert_nil session["flash"]
+ end
+
+ def test_sweep_after_halted_action_chain
+ get :std_action
+ assert_nil @controller.instance_variable_get(:@flash_copy)["foo"]
+ get :filter_halting_action
+ assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"]
+ get :std_action # follow redirection
+ assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"]
+ get :std_action
+ assert_nil @controller.instance_variable_get(:@flash_copy)["foo"]
+ end
+
+ def test_keep_and_discard_return_values
+ flash = ActionDispatch::Flash::FlashHash.new
+ flash.update(foo: :foo_indeed, bar: :bar_indeed)
+
+ assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed
+ assert_nil flash.discard(:unknown) # non existent key passed
+ assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.discard().to_hash) # nothing passed
+ assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.discard(nil).to_hash) # nothing passed
+
+ assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed
+ assert_nil flash.keep(:unknown) # non existent key passed
+ assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.keep().to_hash) # nothing passed
+ assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.keep(nil).to_hash) # nothing passed
+ end
+
+ def test_redirect_to_with_alert
+ get :redirect_with_alert
+ assert_equal "Beware the nowheres!", @controller.send(:flash)[:alert]
+ end
+
+ def test_redirect_to_with_notice
+ get :redirect_with_notice
+ assert_equal "Good luck in the somewheres!", @controller.send(:flash)[:notice]
+ end
+
+ def test_render_with_flash_now_alert
+ get :render_with_flash_now_alert
+ assert_equal "Beware the nowheres now!", @controller.send(:flash)[:alert]
+ end
+
+ def test_render_with_flash_now_notice
+ get :render_with_flash_now_notice
+ assert_equal "Good luck in the somewheres now!", @controller.send(:flash)[:notice]
+ end
+
+ def test_redirect_to_with_other_flashes
+ get :redirect_with_other_flashes
+ assert_equal "Horses!", @controller.send(:flash)[:joyride]
+ end
+
+ def test_redirect_to_with_adding_flash_types
+ original_controller = @controller
+ test_controller_with_flash_type_foo = Class.new(TestController) do
+ add_flash_types :foo
+ end
+ @controller = test_controller_with_flash_type_foo.new
+ get :redirect_with_foo_flash
+ assert_equal "for great justice", @controller.send(:flash)[:foo]
+ ensure
+ @controller = original_controller
+ end
+
+ def test_add_flash_type_to_subclasses
+ test_controller_with_flash_type_foo = Class.new(TestController) do
+ add_flash_types :foo
+ end
+ subclass_controller_with_no_flash_type = Class.new(test_controller_with_flash_type_foo)
+ assert_includes subclass_controller_with_no_flash_type._flash_types, :foo
+ end
+
+ def test_does_not_add_flash_type_to_parent_class
+ Class.new(TestController) do
+ add_flash_types :bar
+ end
+ assert_not TestController._flash_types.include?(:bar)
+ end
+end
+
+class FlashIntegrationTest < ActionDispatch::IntegrationTest
+ SessionKey = "_myapp_session"
+ Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
+ Rotations = ActiveSupport::Messages::RotationConfiguration.new
+
+ class TestController < ActionController::Base
+ add_flash_types :bar
+
+ def set_flash
+ flash["that"] = "hello"
+ head :ok
+ end
+
+ def set_flash_now
+ flash.now["that"] = "hello"
+ head :ok
+ end
+
+ def use_flash
+ render inline: "flash: #{flash["that"]}"
+ end
+
+ def set_bar
+ flash[:bar] = "for great justice"
+ head :ok
+ end
+
+ def set_flash_optionally
+ flash.now.notice = params[:flash]
+ if stale? etag: "abe"
+ render inline: "maybe flash"
+ end
+ end
+ end
+
+ def test_flash
+ with_test_route_set do
+ get "/set_flash"
+ assert_response :success
+ assert_equal "hello", @request.flash["that"]
+
+ get "/use_flash"
+ assert_response :success
+ assert_equal "flash: hello", @response.body
+ end
+ end
+
+ def test_just_using_flash_does_not_stream_a_cookie_back
+ with_test_route_set do
+ get "/use_flash"
+ assert_response :success
+ assert_nil @response.headers["Set-Cookie"]
+ assert_equal "flash: ", @response.body
+ end
+ end
+
+ def test_setting_flash_does_not_raise_in_following_requests
+ with_test_route_set do
+ env = { "action_dispatch.request.flash_hash" => ActionDispatch::Flash::FlashHash.new }
+ get "/set_flash", env: env
+ get "/set_flash", env: env
+ end
+ end
+
+ def test_setting_flash_now_does_not_raise_in_following_requests
+ with_test_route_set do
+ env = { "action_dispatch.request.flash_hash" => ActionDispatch::Flash::FlashHash.new }
+ get "/set_flash_now", env: env
+ get "/set_flash_now", env: env
+ end
+ end
+
+ def test_added_flash_types_method
+ with_test_route_set do
+ get "/set_bar"
+ assert_response :success
+ assert_equal "for great justice", @controller.bar
+ end
+ end
+
+ def test_flash_factored_into_etag
+ with_test_route_set do
+ get "/set_flash_optionally"
+ no_flash_etag = response.etag
+
+ get "/set_flash_optionally", params: { flash: "hello!" }
+ hello_flash_etag = response.etag
+
+ assert_not_equal no_flash_etag, hello_flash_etag
+
+ get "/set_flash_optionally", params: { flash: "hello!" }
+ another_hello_flash_etag = response.etag
+
+ assert_equal another_hello_flash_etag, hello_flash_etag
+
+ get "/set_flash_optionally", params: { flash: "goodbye!" }
+ goodbye_flash_etag = response.etag
+
+ assert_not_equal another_hello_flash_etag, goodbye_flash_etag
+ end
+ end
+
+ def test_flash_usable_in_metal_without_helper
+ controller_class = nil
+
+ assert_nothing_raised do
+ controller_class = Class.new(ActionController::Metal) do
+ include ActionController::Flash
+ end
+ end
+
+ controller = controller_class.new
+
+ assert_respond_to controller, :alert
+ assert_respond_to controller, :notice
+ end
+
+ private
+
+ # Overwrite get to send SessionSecret in env hash
+ def get(path, *args)
+ args[0] ||= {}
+ args[0][:env] ||= {}
+ args[0][:env]["action_dispatch.key_generator"] ||= Generator
+ args[0][:env]["action_dispatch.cookies_rotations"] = Rotations
+ super(path, *args)
+ end
+
+ def with_test_route_set
+ with_routing do |set|
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action", to: FlashIntegrationTest::TestController
+ end
+ end
+
+ @app = self.class.build_app(set) do |middleware|
+ middleware.use ActionDispatch::Session::CookieStore, key: SessionKey
+ middleware.use ActionDispatch::Flash
+ middleware.delete ActionDispatch::ShowExceptions
+ end
+
+ yield
+ end
+ end
+end
diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb
new file mode 100644
index 0000000000..7f59f6acaf
--- /dev/null
+++ b/actionpack/test/controller/force_ssl_test.rb
@@ -0,0 +1,345 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ForceSSLController < ActionController::Base
+ def banana
+ render plain: "monkey"
+ end
+
+ def cheeseburger
+ render plain: "sikachu"
+ end
+end
+
+class ForceSSLControllerLevel < ForceSSLController
+ ActiveSupport::Deprecation.silence do
+ force_ssl
+ end
+end
+
+class ForceSSLCustomOptions < ForceSSLController
+ ActiveSupport::Deprecation.silence do
+ force_ssl host: "secure.example.com", only: :redirect_host
+ force_ssl port: 8443, only: :redirect_port
+ force_ssl subdomain: "secure", only: :redirect_subdomain
+ force_ssl domain: "secure.com", only: :redirect_domain
+ force_ssl path: "/foo", only: :redirect_path
+ force_ssl status: :found, only: :redirect_status
+ force_ssl flash: { message: "Foo, Bar!" }, only: :redirect_flash
+ force_ssl alert: "Foo, Bar!", only: :redirect_alert
+ force_ssl notice: "Foo, Bar!", only: :redirect_notice
+ end
+
+ def force_ssl_action
+ render plain: action_name
+ end
+
+ alias_method :redirect_host, :force_ssl_action
+ alias_method :redirect_port, :force_ssl_action
+ alias_method :redirect_subdomain, :force_ssl_action
+ alias_method :redirect_domain, :force_ssl_action
+ alias_method :redirect_path, :force_ssl_action
+ alias_method :redirect_status, :force_ssl_action
+ alias_method :redirect_flash, :force_ssl_action
+ alias_method :redirect_alert, :force_ssl_action
+ alias_method :redirect_notice, :force_ssl_action
+
+ def use_flash
+ render plain: flash[:message]
+ end
+
+ def use_alert
+ render plain: flash[:alert]
+ end
+
+ def use_notice
+ render plain: flash[:notice]
+ end
+end
+
+class ForceSSLOnlyAction < ForceSSLController
+ ActiveSupport::Deprecation.silence do
+ force_ssl only: :cheeseburger
+ end
+end
+
+class ForceSSLExceptAction < ForceSSLController
+ ActiveSupport::Deprecation.silence do
+ force_ssl except: :banana
+ end
+end
+
+class ForceSSLIfCondition < ForceSSLController
+ ActiveSupport::Deprecation.silence do
+ force_ssl if: :use_force_ssl?
+ end
+
+ def use_force_ssl?
+ action_name == "cheeseburger"
+ end
+end
+
+class ForceSSLFlash < ForceSSLController
+ ActiveSupport::Deprecation.silence do
+ force_ssl except: [:banana, :set_flash, :use_flash]
+ end
+
+ def set_flash
+ flash["that"] = "hello"
+ redirect_to "/force_ssl_flash/cheeseburger"
+ end
+
+ def use_flash
+ @flash_copy = {}.update flash
+ @flashy = flash["that"]
+ render inline: "hello"
+ end
+end
+
+class RedirectToSSL < ForceSSLController
+ def banana
+ force_ssl_redirect || render(plain: "monkey")
+ end
+ def cheeseburger
+ force_ssl_redirect("secure.cheeseburger.host") || render(plain: "ihaz")
+ end
+end
+
+class ForceSSLControllerLevelTest < ActionController::TestCase
+ def test_banana_redirects_to_https
+ get :banana
+ assert_response 301
+ assert_equal "https://test.host/force_ssl_controller_level/banana", redirect_to_url
+ end
+
+ def test_banana_redirects_to_https_with_extra_params
+ get :banana, params: { token: "secret" }
+ assert_response 301
+ assert_equal "https://test.host/force_ssl_controller_level/banana?token=secret", redirect_to_url
+ end
+
+ def test_cheeseburger_redirects_to_https
+ get :cheeseburger
+ assert_response 301
+ assert_equal "https://test.host/force_ssl_controller_level/cheeseburger", redirect_to_url
+ end
+end
+
+class ForceSSLCustomOptionsTest < ActionController::TestCase
+ def setup
+ @request.env["HTTP_HOST"] = "www.example.com:80"
+ end
+
+ def test_redirect_to_custom_host
+ get :redirect_host
+ assert_response 301
+ assert_equal "https://secure.example.com/force_ssl_custom_options/redirect_host", redirect_to_url
+ end
+
+ def test_redirect_to_custom_port
+ get :redirect_port
+ assert_response 301
+ assert_equal "https://www.example.com:8443/force_ssl_custom_options/redirect_port", redirect_to_url
+ end
+
+ def test_redirect_to_custom_subdomain
+ get :redirect_subdomain
+ assert_response 301
+ assert_equal "https://secure.example.com/force_ssl_custom_options/redirect_subdomain", redirect_to_url
+ end
+
+ def test_redirect_to_custom_domain
+ get :redirect_domain
+ assert_response 301
+ assert_equal "https://www.secure.com/force_ssl_custom_options/redirect_domain", redirect_to_url
+ end
+
+ def test_redirect_to_custom_path
+ get :redirect_path
+ assert_response 301
+ assert_equal "https://www.example.com/foo", redirect_to_url
+ end
+
+ def test_redirect_to_custom_status
+ get :redirect_status
+ assert_response 302
+ assert_equal "https://www.example.com/force_ssl_custom_options/redirect_status", redirect_to_url
+ end
+
+ def test_redirect_to_custom_flash
+ get :redirect_flash
+ assert_response 301
+ assert_equal "https://www.example.com/force_ssl_custom_options/redirect_flash", redirect_to_url
+
+ get :use_flash
+ assert_response 200
+ assert_equal "Foo, Bar!", @response.body
+ end
+
+ def test_redirect_to_custom_alert
+ get :redirect_alert
+ assert_response 301
+ assert_equal "https://www.example.com/force_ssl_custom_options/redirect_alert", redirect_to_url
+
+ get :use_alert
+ assert_response 200
+ assert_equal "Foo, Bar!", @response.body
+ end
+
+ def test_redirect_to_custom_notice
+ get :redirect_notice
+ assert_response 301
+ assert_equal "https://www.example.com/force_ssl_custom_options/redirect_notice", redirect_to_url
+
+ get :use_notice
+ assert_response 200
+ assert_equal "Foo, Bar!", @response.body
+ end
+end
+
+class ForceSSLOnlyActionTest < ActionController::TestCase
+ def test_banana_not_redirects_to_https
+ get :banana
+ assert_response 200
+ end
+
+ def test_cheeseburger_redirects_to_https
+ get :cheeseburger
+ assert_response 301
+ assert_equal "https://test.host/force_ssl_only_action/cheeseburger", redirect_to_url
+ end
+end
+
+class ForceSSLExceptActionTest < ActionController::TestCase
+ def test_banana_not_redirects_to_https
+ get :banana
+ assert_response 200
+ end
+
+ def test_cheeseburger_redirects_to_https
+ get :cheeseburger
+ assert_response 301
+ assert_equal "https://test.host/force_ssl_except_action/cheeseburger", redirect_to_url
+ end
+end
+
+class ForceSSLIfConditionTest < ActionController::TestCase
+ def test_banana_not_redirects_to_https
+ get :banana
+ assert_response 200
+ end
+
+ def test_cheeseburger_redirects_to_https
+ get :cheeseburger
+ assert_response 301
+ assert_equal "https://test.host/force_ssl_if_condition/cheeseburger", redirect_to_url
+ end
+end
+
+class ForceSSLFlashTest < ActionController::TestCase
+ def test_cheeseburger_redirects_to_https
+ get :set_flash
+ assert_response 302
+ assert_equal "http://test.host/force_ssl_flash/cheeseburger", redirect_to_url
+
+ @request.env.delete("PATH_INFO")
+
+ get :cheeseburger
+ assert_response 301
+ assert_equal "https://test.host/force_ssl_flash/cheeseburger", redirect_to_url
+
+ @request.env.delete("PATH_INFO")
+
+ get :use_flash
+ assert_equal "hello", @controller.instance_variable_get("@flash_copy")["that"]
+ assert_equal "hello", @controller.instance_variable_get("@flashy")
+ end
+end
+
+class ForceSSLDuplicateRoutesTest < ActionController::TestCase
+ tests ForceSSLControllerLevel
+
+ def test_force_ssl_redirects_to_same_path
+ with_routing do |set|
+ set.draw do
+ get "/foo", to: "force_ssl_controller_level#banana"
+ get "/bar", to: "force_ssl_controller_level#banana"
+ end
+
+ @request.env["PATH_INFO"] = "/bar"
+
+ get :banana
+ assert_response 301
+ assert_equal "https://test.host/bar", redirect_to_url
+ end
+ end
+end
+
+class ForceSSLFormatTest < ActionController::TestCase
+ tests ForceSSLControllerLevel
+
+ def test_force_ssl_redirects_to_same_format
+ with_routing do |set|
+ set.draw do
+ get "/foo", to: "force_ssl_controller_level#banana"
+ end
+
+ get :banana, format: :json
+ assert_response 301
+ assert_equal "https://test.host/foo.json", redirect_to_url
+ end
+ end
+end
+
+class ForceSSLOptionalSegmentsTest < ActionController::TestCase
+ tests ForceSSLControllerLevel
+
+ def test_force_ssl_redirects_to_same_format
+ with_routing do |set|
+ set.draw do
+ scope "(:locale)" do
+ defaults locale: "en" do
+ get "/foo", to: "force_ssl_controller_level#banana"
+ end
+ end
+ end
+
+ @request.env["PATH_INFO"] = "/en/foo"
+ get :banana, params: { locale: "en" }
+ assert_equal "en", @controller.params[:locale]
+ assert_response 301
+ assert_equal "https://test.host/en/foo", redirect_to_url
+ end
+ end
+end
+
+class RedirectToSSLTest < ActionController::TestCase
+ def test_banana_redirects_to_https_if_not_https
+ get :banana
+ assert_response 301
+ assert_equal "https://test.host/redirect_to_ssl/banana", redirect_to_url
+ end
+
+ def test_cheeseburgers_redirects_to_https_with_new_host_if_not_https
+ get :cheeseburger
+ assert_response 301
+ assert_equal "https://secure.cheeseburger.host/redirect_to_ssl/cheeseburger", redirect_to_url
+ end
+
+ def test_cheeseburgers_does_not_redirect_if_already_https
+ request.env["HTTPS"] = "on"
+ get :cheeseburger
+ assert_response 200
+ assert_equal "ihaz", response.body
+ end
+end
+
+class ForceSSLControllerLevelTest < ActionController::TestCase
+ def test_no_redirect_websocket_ssl_request
+ request.env["rack.url_scheme"] = "wss"
+ request.env["Upgrade"] = "websocket"
+ get :cheeseburger
+ assert_response 200
+ end
+end
diff --git a/actionpack/test/controller/form_builder_test.rb b/actionpack/test/controller/form_builder_test.rb
new file mode 100644
index 0000000000..2db0834c5e
--- /dev/null
+++ b/actionpack/test/controller/form_builder_test.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class FormBuilderController < ActionController::Base
+ class SpecializedFormBuilder < ActionView::Helpers::FormBuilder ; end
+
+ default_form_builder SpecializedFormBuilder
+end
+
+class ControllerFormBuilderTest < ActiveSupport::TestCase
+ setup do
+ @controller = FormBuilderController.new
+ end
+
+ def test_default_form_builder_assigned
+ assert_equal FormBuilderController::SpecializedFormBuilder, @controller.default_form_builder
+ end
+end
diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb
new file mode 100644
index 0000000000..de8072a994
--- /dev/null
+++ b/actionpack/test/controller/helper_test.rb
@@ -0,0 +1,295 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+ActionController::Base.helpers_path = File.expand_path("../fixtures/helpers", __dir__)
+
+module Fun
+ class GamesController < ActionController::Base
+ def render_hello_world
+ render inline: "hello: <%= stratego %>"
+ end
+ end
+
+ class PdfController < ActionController::Base
+ def test
+ render inline: "test: <%= foobar %>"
+ end
+ end
+end
+
+class AllHelpersController < ActionController::Base
+ helper :all
+end
+
+module ImpressiveLibrary
+ extend ActiveSupport::Concern
+ included do
+ helper_method :useful_function
+ end
+
+ def useful_function() end
+end
+
+ActionController::Base.include(ImpressiveLibrary)
+
+class JustMeController < ActionController::Base
+ clear_helpers
+
+ def flash
+ render inline: "<h1><%= notice %></h1>"
+ end
+
+ def lib
+ render inline: "<%= useful_function %>"
+ end
+end
+
+class MeTooController < JustMeController
+end
+
+class HelpersPathsController < ActionController::Base
+ paths = ["helpers2_pack", "helpers1_pack"].map do |path|
+ File.join(File.expand_path("../fixtures", __dir__), path)
+ end
+ $:.unshift(*paths)
+
+ self.helpers_path = paths
+ helper :all
+
+ def index
+ render inline: "<%= conflicting_helper %>"
+ end
+end
+
+class HelpersTypoController < ActionController::Base
+ path = File.expand_path("../fixtures/helpers_typo", __dir__)
+ $:.unshift(path)
+ self.helpers_path = path
+end
+
+module LocalAbcHelper
+ def a() end
+ def b() end
+ def c() end
+end
+
+class HelperPathsTest < ActiveSupport::TestCase
+ def test_helpers_paths_priority
+ responses = HelpersPathsController.action(:index).call(ActionController::TestRequest::DEFAULT_ENV.dup)
+
+ # helpers1_pack was given as a second path, so pack1_helper should be
+ # included as the second one
+ assert_equal "pack1", responses.last.body
+ end
+end
+
+class HelpersTypoControllerTest < ActiveSupport::TestCase
+ def setup
+ @autoload_paths = ActiveSupport::Dependencies.autoload_paths
+ ActiveSupport::Dependencies.autoload_paths = Array(HelpersTypoController.helpers_path)
+ end
+
+ def test_helper_typo_error_message
+ e = assert_raise(NameError) { HelpersTypoController.helper "admin/users" }
+ assert_equal "Couldn't find Admin::UsersHelper, expected it to be defined in helpers/admin/users_helper.rb", e.message
+ end
+
+ def teardown
+ ActiveSupport::Dependencies.autoload_paths = @autoload_paths
+ end
+end
+
+class HelperTest < ActiveSupport::TestCase
+ class TestController < ActionController::Base
+ attr_accessor :delegate_attr
+ def delegate_method() end
+ end
+
+ def setup
+ # Increment symbol counter.
+ @symbol = (@@counter ||= "A0").succ.dup
+
+ # Generate new controller class.
+ controller_class_name = "Helper#{@symbol}Controller"
+ eval("class #{controller_class_name} < TestController; end")
+ @controller_class = self.class.const_get(controller_class_name)
+
+ # Set default test helper.
+ self.test_helper = LocalAbcHelper
+ end
+
+ def test_helper
+ assert_equal expected_helper_methods, missing_methods
+ assert_nothing_raised { @controller_class.helper TestHelper }
+ assert_equal [], missing_methods
+ end
+
+ def test_helper_method
+ assert_nothing_raised { @controller_class.helper_method :delegate_method }
+ assert_includes master_helper_methods, :delegate_method
+ end
+
+ def test_helper_attr
+ assert_nothing_raised { @controller_class.helper_attr :delegate_attr }
+ assert_includes master_helper_methods, :delegate_attr
+ assert_includes master_helper_methods, :delegate_attr=
+ end
+
+ def call_controller(klass, action)
+ klass.action(action).call(ActionController::TestRequest::DEFAULT_ENV.dup)
+ end
+
+ def test_helper_for_nested_controller
+ assert_equal "hello: Iz guuut!",
+ call_controller(Fun::GamesController, "render_hello_world").last.body
+ end
+
+ def test_helper_for_acronym_controller
+ assert_equal "test: baz", call_controller(Fun::PdfController, "test").last.body
+ end
+
+ def test_default_helpers_only
+ assert_equal [JustMeHelper], JustMeController._helpers.ancestors.reject(&:anonymous?)
+ assert_equal [MeTooHelper, JustMeHelper], MeTooController._helpers.ancestors.reject(&:anonymous?)
+ end
+
+ def test_base_helper_methods_after_clear_helpers
+ assert_nothing_raised do
+ call_controller(JustMeController, "flash")
+ end
+ end
+
+ def test_lib_helper_methods_after_clear_helpers
+ assert_nothing_raised do
+ call_controller(JustMeController, "lib")
+ end
+ end
+
+ def test_all_helpers
+ methods = AllHelpersController._helpers.instance_methods
+
+ # abc_helper.rb
+ assert_includes methods, :bare_a
+
+ # fun/games_helper.rb
+ assert_includes methods, :stratego
+
+ # fun/pdf_helper.rb
+ assert_includes methods, :foobar
+ end
+
+ def test_all_helpers_with_alternate_helper_dir
+ @controller_class.helpers_path = File.expand_path("../fixtures/alternate_helpers", __dir__)
+
+ # Reload helpers
+ @controller_class._helpers = Module.new
+ @controller_class.helper :all
+
+ # helpers/abc_helper.rb should not be included
+ assert_not_includes master_helper_methods, :bare_a
+
+ # alternate_helpers/foo_helper.rb
+ assert_includes master_helper_methods, :baz
+ end
+
+ def test_helper_proxy
+ methods = AllHelpersController.helpers.methods
+
+ # Action View
+ assert_includes methods, :pluralize
+
+ # abc_helper.rb
+ assert_includes methods, :bare_a
+
+ # fun/games_helper.rb
+ assert_includes methods, :stratego
+
+ # fun/pdf_helper.rb
+ assert_includes methods, :foobar
+ end
+
+ def test_helper_proxy_in_instance
+ methods = AllHelpersController.new.helpers.methods
+
+ # Action View
+ assert_includes methods, :pluralize
+
+ # abc_helper.rb
+ assert_includes methods, :bare_a
+
+ # fun/games_helper.rb
+ assert_includes methods, :stratego
+
+ # fun/pdf_helper.rb
+ assert_includes methods, :foobar
+ end
+
+ def test_helper_proxy_config
+ AllHelpersController.config.my_var = "smth"
+
+ assert_equal "smth", AllHelpersController.helpers.config.my_var
+ end
+
+ private
+ def expected_helper_methods
+ TestHelper.instance_methods
+ end
+
+ def master_helper_methods
+ @controller_class._helpers.instance_methods
+ end
+
+ def missing_methods
+ expected_helper_methods - master_helper_methods
+ end
+
+ def test_helper=(helper_module)
+ silence_warnings { self.class.const_set("TestHelper", helper_module) }
+ end
+end
+
+class IsolatedHelpersTest < ActionController::TestCase
+ class A < ActionController::Base
+ def index
+ render inline: "<%= shout %>"
+ end
+ end
+
+ class B < A
+ helper { def shout; "B" end }
+
+ def index
+ render inline: "<%= shout %>"
+ end
+ end
+
+ class C < A
+ helper { def shout; "C" end }
+
+ def index
+ render inline: "<%= shout %>"
+ end
+ end
+
+ def call_controller(klass, action)
+ klass.action(action).call(@request.env)
+ end
+
+ def setup
+ super
+ @request.action = "index"
+ end
+
+ def test_helper_in_a
+ assert_raise(ActionView::Template::Error) { call_controller(A, "index") }
+ end
+
+ def test_helper_in_b
+ assert_equal "B", call_controller(B, "index").last.body
+ end
+
+ def test_helper_in_c
+ assert_equal "C", call_controller(C, "index").last.body
+ end
+end
diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb
new file mode 100644
index 0000000000..1544a627ee
--- /dev/null
+++ b/actionpack/test/controller/http_basic_authentication_test.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class HttpBasicAuthenticationTest < ActionController::TestCase
+ class DummyController < ActionController::Base
+ before_action :authenticate, only: :index
+ before_action :authenticate_with_request, only: :display
+ before_action :authenticate_long_credentials, only: :show
+ before_action :auth_with_special_chars, only: :special_creds
+
+ http_basic_authenticate_with name: "David", password: "Goliath", only: :search
+
+ def index
+ render plain: "Hello Secret"
+ end
+
+ def display
+ render plain: "Definitely Maybe" if @logged_in
+ end
+
+ def show
+ render plain: "Only for loooooong credentials"
+ end
+
+ def special_creds
+ render plain: "Only for special credentials"
+ end
+
+ def search
+ render plain: "All inline"
+ end
+
+ private
+
+ def authenticate
+ authenticate_or_request_with_http_basic do |username, password|
+ username == "lifo" && password == "world"
+ end
+ end
+
+ def authenticate_with_request
+ if authenticate_with_http_basic { |username, password| username == "pretty" && password == "please" }
+ @logged_in = true
+ else
+ request_http_basic_authentication("SuperSecret", "Authentication Failed\n")
+ end
+ end
+
+ def auth_with_special_chars
+ authenticate_or_request_with_http_basic do |username, password|
+ username == 'login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' && password == 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t'
+ end
+ end
+
+ def authenticate_long_credentials
+ authenticate_or_request_with_http_basic do |username, password|
+ username == "1234567890123456789012345678901234567890" && password == "1234567890123456789012345678901234567890"
+ end
+ end
+ end
+
+ AUTH_HEADERS = ["HTTP_AUTHORIZATION", "X-HTTP_AUTHORIZATION", "X_HTTP_AUTHORIZATION", "REDIRECT_X_HTTP_AUTHORIZATION"]
+
+ tests DummyController
+
+ AUTH_HEADERS.each do |header|
+ test "successful authentication with #{header.downcase}" do
+ @request.env[header] = encode_credentials("lifo", "world")
+ get :index
+
+ assert_response :success
+ assert_equal "Hello Secret", @response.body, "Authentication failed for request header #{header}"
+ end
+ test "successful authentication with #{header.downcase} and long credentials" do
+ @request.env[header] = encode_credentials("1234567890123456789012345678901234567890", "1234567890123456789012345678901234567890")
+ get :show
+
+ assert_response :success
+ assert_equal "Only for loooooong credentials", @response.body, "Authentication failed for request header #{header} and long credentials"
+ end
+ end
+
+ AUTH_HEADERS.each do |header|
+ test "unsuccessful authentication with #{header.downcase}" do
+ @request.env[header] = encode_credentials("h4x0r", "world")
+ get :index
+
+ assert_response :unauthorized
+ assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}"
+ end
+ test "unsuccessful authentication with #{header.downcase} and long credentials" do
+ @request.env[header] = encode_credentials("h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r", "worldworldworldworldworldworldworldworld")
+ get :show
+
+ assert_response :unauthorized
+ assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and long credentials"
+ end
+
+ test "unsuccessful authentication with #{header.downcase} and no credentials" do
+ get :show
+
+ assert_response :unauthorized
+ assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and no credentials"
+ end
+ end
+
+ def test_encode_credentials_has_no_newline
+ username = "laskjdfhalksdjfhalkjdsfhalksdjfhklsdjhalksdjfhalksdjfhlakdsjfh"
+ password = "kjfhueyt9485osdfasdkljfh4lkjhakldjfhalkdsjf"
+ result = ActionController::HttpAuthentication::Basic.encode_credentials(
+ username, password)
+ assert_no_match(/\n/, result)
+ end
+
+ test "successful authentication with uppercase authorization scheme" do
+ @request.env["HTTP_AUTHORIZATION"] = "BASIC #{::Base64.encode64("lifo:world")}"
+ get :index
+
+ assert_response :success
+ assert_equal "Hello Secret", @response.body, "Authentication failed when authorization scheme BASIC"
+ end
+
+ test "authentication request without credential" do
+ get :display
+
+ assert_response :unauthorized
+ assert_equal "Authentication Failed\n", @response.body
+ assert_equal 'Basic realm="SuperSecret"', @response.headers["WWW-Authenticate"]
+ end
+
+ test "authentication request with invalid credential" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials("pretty", "foo")
+ get :display
+
+ assert_response :unauthorized
+ assert_equal "Authentication Failed\n", @response.body
+ assert_equal 'Basic realm="SuperSecret"', @response.headers["WWW-Authenticate"]
+ end
+
+ test "authentication request with valid credential" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials("pretty", "please")
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with valid credential special chars" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials('login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t', 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t')
+ get :special_creds
+
+ assert_response :success
+ assert_equal "Only for special credentials", @response.body
+ end
+
+ test "authenticate with class method" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials("David", "Goliath")
+ get :search
+ assert_response :success
+
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials("David", "WRONG!")
+ get :search
+ assert_response :unauthorized
+ end
+
+ test "authentication request with wrong scheme" do
+ header = "Bearer " + encode_credentials("David", "Goliath").split(" ", 2)[1]
+ @request.env["HTTP_AUTHORIZATION"] = header
+ get :search
+ assert_response :unauthorized
+ end
+
+ private
+
+ def encode_credentials(username, password)
+ "Basic #{::Base64.encode64("#{username}:#{password}")}"
+ end
+end
diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb
new file mode 100644
index 0000000000..b133afb343
--- /dev/null
+++ b/actionpack/test/controller/http_digest_authentication_test.rb
@@ -0,0 +1,283 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/key_generator"
+
+class HttpDigestAuthenticationTest < ActionController::TestCase
+ class DummyDigestController < ActionController::Base
+ before_action :authenticate, only: :index
+ before_action :authenticate_with_request, only: :display
+
+ USERS = { "lifo" => "world", "pretty" => "please",
+ "dhh" => ::Digest::MD5.hexdigest(["dhh", "SuperSecret", "secret"].join(":")) }
+
+ def index
+ render plain: "Hello Secret"
+ end
+
+ def display
+ render plain: "Definitely Maybe" if @logged_in
+ end
+
+ private
+
+ def authenticate
+ authenticate_or_request_with_http_digest("SuperSecret") do |username|
+ # Returns the password
+ USERS[username]
+ end
+ end
+
+ def authenticate_with_request
+ if authenticate_with_http_digest("SuperSecret") { |username| USERS[username] }
+ @logged_in = true
+ else
+ request_http_digest_authentication("SuperSecret", "Authentication Failed")
+ end
+ end
+ end
+
+ AUTH_HEADERS = ["HTTP_AUTHORIZATION", "X-HTTP_AUTHORIZATION", "X_HTTP_AUTHORIZATION", "REDIRECT_X_HTTP_AUTHORIZATION"]
+
+ tests DummyDigestController
+
+ setup do
+ # Used as secret in generating nonce to prevent tampering of timestamp
+ @secret = "4fb45da9e4ab4ddeb7580d6a35503d99"
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(@secret)
+ end
+
+ teardown do
+ # ActionController::Base.session_options[:secret] = @old_secret
+ end
+
+ AUTH_HEADERS.each do |header|
+ test "successful authentication with #{header.downcase}" do
+ @request.env[header] = encode_credentials(username: "lifo", password: "world")
+ get :index
+
+ assert_response :success
+ assert_equal "Hello Secret", @response.body, "Authentication failed for request header #{header}"
+ end
+ end
+
+ AUTH_HEADERS.each do |header|
+ test "unsuccessful authentication with #{header.downcase}" do
+ @request.env[header] = encode_credentials(username: "h4x0r", password: "world")
+ get :index
+
+ assert_response :unauthorized
+ assert_equal "HTTP Digest: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}"
+ end
+ end
+
+ test "authentication request without credential" do
+ get :display
+
+ assert_response :unauthorized
+ assert_equal "Authentication Failed", @response.body
+ credentials = decode_credentials(@response.headers["WWW-Authenticate"])
+ assert_equal "SuperSecret", credentials[:realm]
+ end
+
+ test "authentication request with nil credentials" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: nil, password: nil)
+ get :index
+
+ assert_response :unauthorized
+ assert_equal "HTTP Digest: Access denied.\n", @response.body, "Authentication didn't fail for request"
+ assert_not_equal "Hello Secret", @response.body, "Authentication didn't fail for request"
+ end
+
+ test "authentication request with invalid password" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "foo")
+ get :display
+
+ assert_response :unauthorized
+ assert_equal "Authentication Failed", @response.body
+ end
+
+ test "authentication request with invalid nonce" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please", nonce: "xxyyzz")
+ get :display
+
+ assert_response :unauthorized
+ assert_equal "Authentication Failed", @response.body
+ end
+
+ test "authentication request with invalid opaque" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "foo", opaque: "xxyyzz")
+ get :display
+
+ assert_response :unauthorized
+ assert_equal "Authentication Failed", @response.body
+ end
+
+ test "authentication request with invalid realm" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "foo", realm: "NotSecret")
+ get :display
+
+ assert_response :unauthorized
+ assert_equal "Authentication Failed", @response.body
+ end
+
+ test "authentication request with valid credential" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with valid credential and nil session" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
+
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with request-uri that doesn't match credentials digest-uri" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
+ @request.env["PATH_INFO"] = "/proxied/uri"
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with absolute request uri (as in webrick)" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
+ @request.env["SERVER_NAME"] = "test.host"
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest"
+
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with absolute uri in credentials (as in IE)" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(url: "http://test.host/http_digest_authentication_test/dummy_digest",
+ username: "pretty", password: "please")
+
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with absolute uri in both request and credentials (as in Webrick with IE)" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(url: "http://test.host/http_digest_authentication_test/dummy_digest",
+ username: "pretty", password: "please")
+ @request.env["SERVER_NAME"] = "test.host"
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest"
+
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with password stored as ha1 digest hash" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(
+ username: "dhh",
+ password: ::Digest::MD5.hexdigest(["dhh", "SuperSecret", "secret"].join(":")),
+ password_is_ha1: true)
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with _method" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please", method: :post)
+ @request.env["rack.methodoverride.original_method"] = "POST"
+ put :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "validate_digest_response should fail with nil returning password_procedure" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: nil, password: nil)
+ assert_not ActionController::HttpAuthentication::Digest.validate_digest_response(@request, "SuperSecret") { nil }
+ end
+
+ test "authentication request with request-uri ending in '/'" do
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest/"
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
+
+ # simulate normalizing PATH_INFO
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest"
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with request-uri ending in '?'" do
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest/?"
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please")
+
+ # simulate normalizing PATH_INFO
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest"
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "authentication request with absolute uri in credentials (as in IE) ending with /" do
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest/"
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(uri: "http://test.host/http_digest_authentication_test/dummy_digest/",
+ username: "pretty", password: "please")
+
+ # simulate normalizing PATH_INFO
+ @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest"
+ get :display
+
+ assert_response :success
+ assert_equal "Definitely Maybe", @response.body
+ end
+
+ test "when sent a basic auth header, returns Unauthorized" do
+ @request.env["HTTP_AUTHORIZATION"] = "Basic Gwf2aXq8ZLF3Hxq="
+
+ get :display
+
+ assert_response :unauthorized
+ end
+
+ private
+
+ def encode_credentials(options)
+ options.reverse_merge!(nc: "00000001", cnonce: "0a4f113b", password_is_ha1: false)
+ password = options.delete(:password)
+
+ # Perform unauthenticated request to retrieve digest parameters to use on subsequent request
+ method = options.delete(:method) || "GET"
+
+ case method.to_s.upcase
+ when "GET"
+ get :index
+ when "POST"
+ post :index
+ end
+
+ assert_response :unauthorized
+
+ credentials = decode_credentials(@response.headers["WWW-Authenticate"])
+ credentials.merge!(options)
+ path_info = @request.env["PATH_INFO"].to_s
+ uri = options[:uri] || path_info
+ credentials[:uri] = uri
+ @request.env["ORIGINAL_FULLPATH"] = path_info
+ ActionController::HttpAuthentication::Digest.encode_credentials(method, credentials, password, options[:password_is_ha1])
+ end
+
+ def decode_credentials(header)
+ ActionController::HttpAuthentication::Digest.decode_credentials(header)
+ end
+end
diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb
new file mode 100644
index 0000000000..103123f98c
--- /dev/null
+++ b/actionpack/test/controller/http_token_authentication_test.rb
@@ -0,0 +1,216 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class HttpTokenAuthenticationTest < ActionController::TestCase
+ class DummyController < ActionController::Base
+ before_action :authenticate, only: :index
+ before_action :authenticate_with_request, only: :display
+ before_action :authenticate_long_credentials, only: :show
+
+ def index
+ render plain: "Hello Secret"
+ end
+
+ def display
+ render plain: "Definitely Maybe"
+ end
+
+ def show
+ render plain: "Only for loooooong credentials"
+ end
+
+ private
+
+ def authenticate
+ authenticate_or_request_with_http_token do |token, _|
+ token == "lifo"
+ end
+ end
+
+ def authenticate_with_request
+ if authenticate_with_http_token { |token, options| token == '"quote" pretty' && options[:algorithm] == "test" }
+ @logged_in = true
+ else
+ request_http_token_authentication("SuperSecret", "Authentication Failed\n")
+ end
+ end
+
+ def authenticate_long_credentials
+ authenticate_or_request_with_http_token do |token, options|
+ token == "1234567890123456789012345678901234567890" && options[:algorithm] == "test"
+ end
+ end
+ end
+
+ AUTH_HEADERS = ["HTTP_AUTHORIZATION", "X-HTTP_AUTHORIZATION", "X_HTTP_AUTHORIZATION", "REDIRECT_X_HTTP_AUTHORIZATION"]
+
+ tests DummyController
+
+ AUTH_HEADERS.each do |header|
+ test "successful authentication with #{header.downcase}" do
+ @request.env[header] = encode_credentials("lifo")
+ get :index
+
+ assert_response :success
+ assert_equal "Hello Secret", @response.body, "Authentication failed for request header #{header}"
+ end
+ test "successful authentication with #{header.downcase} and long credentials" do
+ @request.env[header] = encode_credentials("1234567890123456789012345678901234567890", algorithm: "test")
+ get :show
+
+ assert_response :success
+ assert_equal "Only for loooooong credentials", @response.body, "Authentication failed for request header #{header} and long credentials"
+ end
+ end
+
+ AUTH_HEADERS.each do |header|
+ test "unsuccessful authentication with #{header.downcase}" do
+ @request.env[header] = encode_credentials("h4x0r")
+ get :index
+
+ assert_response :unauthorized
+ assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}"
+ end
+ test "unsuccessful authentication with #{header.downcase} and long credentials" do
+ @request.env[header] = encode_credentials("h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r")
+ get :show
+
+ assert_response :unauthorized
+ assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and long credentials"
+ end
+ end
+
+ test "authentication request with badly formatted header" do
+ @request.env["HTTP_AUTHORIZATION"] = 'Token token$"lifo"'
+ get :index
+
+ assert_response :unauthorized
+ assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication header was not properly parsed"
+ end
+
+ test "successful authentication request with Bearer instead of Token" do
+ @request.env["HTTP_AUTHORIZATION"] = "Bearer lifo"
+ get :index
+
+ assert_response :success
+ end
+
+ test "authentication request with tab in header" do
+ @request.env["HTTP_AUTHORIZATION"] = "Token\ttoken=\"lifo\""
+ get :index
+
+ assert_response :success
+ assert_equal "Hello Secret", @response.body
+ end
+
+ test "authentication request without credential" do
+ get :display
+
+ assert_response :unauthorized
+ assert_equal "Authentication Failed\n", @response.body
+ assert_equal 'Token realm="SuperSecret"', @response.headers["WWW-Authenticate"]
+ end
+
+ test "authentication request with invalid credential" do
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials('"quote" pretty')
+ get :display
+
+ assert_response :unauthorized
+ assert_equal "Authentication Failed\n", @response.body
+ assert_equal 'Token realm="SuperSecret"', @response.headers["WWW-Authenticate"]
+ end
+
+ test "token_and_options returns correct token" do
+ token = "rcHu+HzSFw89Ypyhn/896A=="
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
+ end
+
+ test "token_and_options returns correct token with value after the equal sign" do
+ token = "rcHu+=HzSFw89Ypyhn/896A==f34"
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
+ end
+
+ test "token_and_options returns correct token with slashes" do
+ token = 'rcHu+\\\\"/896A'
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
+ end
+
+ test "token_and_options returns correct token with quotes" do
+ token = '\"quote\" pretty'
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
+ end
+
+ test "token_and_options returns empty string with empty token" do
+ token = +""
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
+ end
+
+ test "token_and_options returns correct token with nounce option" do
+ token = "rcHu+HzSFw89Ypyhn/896A="
+ nonce_hash = { nonce: "123abc" }
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token, nonce_hash))
+ expected_token = token
+ expected_nonce = { "nonce" => nonce_hash[:nonce] }
+ assert_equal(expected_token, actual.first)
+ assert_equal(expected_nonce, actual.last)
+ end
+
+ test "token_and_options returns nil with no value after the equal sign" do
+ actual = ActionController::HttpAuthentication::Token.token_and_options(malformed_request).first
+ assert_nil actual
+ end
+
+ test "raw_params returns a tuple of two key value pair strings" do
+ auth = sample_request("rcHu+HzSFw89Ypyhn/896A=").authorization.to_s
+ actual = ActionController::HttpAuthentication::Token.raw_params(auth)
+ expected = ["token=\"rcHu+HzSFw89Ypyhn/896A=\"", "nonce=\"def\""]
+ assert_equal(expected, actual)
+ end
+
+ test "token_and_options returns right token when token key is not specified in header" do
+ token = "rcHu+HzSFw89Ypyhn/896A="
+
+ actual = ActionController::HttpAuthentication::Token.token_and_options(
+ sample_request_without_token_key(token)
+ ).first
+
+ expected = token
+ assert_equal(expected, actual)
+ end
+
+ private
+
+ def sample_request(token, options = { nonce: "def" })
+ authorization = options.inject([%{Token token="#{token}"}]) do |arr, (k, v)|
+ arr << "#{k}=\"#{v}\""
+ end.join(", ")
+ mock_authorization_request(authorization)
+ end
+
+ def malformed_request
+ mock_authorization_request(%{Token token=})
+ end
+
+ def sample_request_without_token_key(token)
+ mock_authorization_request(%{Token #{token}})
+ end
+
+ def mock_authorization_request(authorization)
+ OpenStruct.new(authorization: authorization)
+ end
+
+ def encode_credentials(token, options = {})
+ ActionController::HttpAuthentication::Token.encode_credentials(token, options)
+ end
+end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
new file mode 100644
index 0000000000..b5503a9c64
--- /dev/null
+++ b/actionpack/test/controller/integration_test.rb
@@ -0,0 +1,1146 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
+require "rails/engine"
+
+class SessionTest < ActiveSupport::TestCase
+ StubApp = lambda { |env|
+ [200, { "Content-Type" => "text/html", "Content-Length" => "13" }, ["Hello, World!"]]
+ }
+
+ def setup
+ @session = ActionDispatch::Integration::Session.new(StubApp)
+ end
+
+ def test_https_bang_works_and_sets_truth_by_default
+ assert_not_predicate @session, :https?
+ @session.https!
+ assert_predicate @session, :https?
+ @session.https! false
+ assert_not_predicate @session, :https?
+ end
+
+ def test_host!
+ assert_not_equal "glu.ttono.us", @session.host
+ @session.host! "rubyonrails.com"
+ assert_equal "rubyonrails.com", @session.host
+ end
+
+ def test_follow_redirect_raises_when_no_redirect
+ @session.stub :redirect?, false do
+ assert_raise(RuntimeError) { @session.follow_redirect! }
+ end
+ end
+
+ def test_get
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+
+ assert_called_with @session, :process, [:get, path, params: params, headers: headers] do
+ @session.get(path, params: params, headers: headers)
+ end
+ end
+
+ def test_get_with_env_and_headers
+ path = "/index"; params = "blah"; headers = { location: "blah" }; env = { "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" }
+ assert_called_with @session, :process, [:get, path, params: params, headers: headers, env: env] do
+ @session.get(path, params: params, headers: headers, env: env)
+ end
+ end
+
+ def test_post
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:post, path, params: params, headers: headers] do
+ @session.post(path, params: params, headers: headers)
+ end
+ end
+
+ def test_patch
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:patch, path, params: params, headers: headers] do
+ @session.patch(path, params: params, headers: headers)
+ end
+ end
+
+ def test_put
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:put, path, params: params, headers: headers] do
+ @session.put(path, params: params, headers: headers)
+ end
+ end
+
+ def test_delete
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:delete, path, params: params, headers: headers] do
+ @session.delete(path, params: params, headers: headers)
+ end
+ end
+
+ def test_head
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:head, path, params: params, headers: headers] do
+ @session.head(path, params: params, headers: headers)
+ end
+ end
+
+ def test_xml_http_request_get
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:get, path, params: params, headers: headers, xhr: true] do
+ @session.get(path, params: params, headers: headers, xhr: true)
+ end
+ end
+
+ def test_xml_http_request_post
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:post, path, params: params, headers: headers, xhr: true] do
+ @session.post(path, params: params, headers: headers, xhr: true)
+ end
+ end
+
+ def test_xml_http_request_patch
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:patch, path, params: params, headers: headers, xhr: true] do
+ @session.patch(path, params: params, headers: headers, xhr: true)
+ end
+ end
+
+ def test_xml_http_request_put
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:put, path, params: params, headers: headers, xhr: true] do
+ @session.put(path, params: params, headers: headers, xhr: true)
+ end
+ end
+
+ def test_xml_http_request_delete
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do
+ @session.delete(path, params: params, headers: headers, xhr: true)
+ end
+ end
+
+ def test_xml_http_request_head
+ path = "/index"; params = "blah"; headers = { location: "blah" }
+ assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do
+ @session.head(path, params: params, headers: headers, xhr: true)
+ end
+ end
+end
+
+class IntegrationTestTest < ActiveSupport::TestCase
+ def setup
+ @test = ::ActionDispatch::IntegrationTest.new(:app)
+ end
+
+ def test_opens_new_session
+ session1 = @test.open_session { |sess| }
+ session2 = @test.open_session # implicit session
+
+ assert_not session1.equal?(session2)
+ end
+
+ # RSpec mixes Matchers (which has a #method_missing) into
+ # IntegrationTest's superclass. Make sure IntegrationTest does not
+ # try to delegate these methods to the session object.
+ def test_does_not_prevent_method_missing_passing_up_to_ancestors
+ mixin = Module.new do
+ def method_missing(name, *args)
+ name.to_s == "foo" ? "pass" : super
+ end
+ end
+ @test.class.superclass.include(mixin)
+ begin
+ assert_equal "pass", @test.foo
+ ensure
+ # leave other tests as unaffected as possible
+ mixin.remove_method :method_missing
+ end
+ end
+end
+
+# Tests that integration tests don't call Controller test methods for processing.
+# Integration tests have their own setup and teardown.
+class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest
+ def test_integration_methods_called
+ reset!
+
+ %w( get post head patch put delete ).each do |verb|
+ assert_nothing_raised { __send__(verb, "/") }
+ end
+ end
+end
+
+class IntegrationProcessTest < ActionDispatch::IntegrationTest
+ class IntegrationController < ActionController::Base
+ def get
+ respond_to do |format|
+ format.html { render plain: "OK", status: 200 }
+ format.js { render plain: "JS OK", status: 200 }
+ format.json { render json: "JSON OK", status: 200 }
+ format.xml { render xml: "<root></root>", status: 200 }
+ format.rss { render xml: "<root></root>", status: 200 }
+ format.atom { render xml: "<root></root>", status: 200 }
+ end
+ end
+
+ def get_with_params
+ render plain: "foo: #{params[:foo]}", status: 200
+ end
+
+ def post
+ render plain: "Created", status: 201
+ end
+
+ def method
+ render plain: "method: #{request.method.downcase}"
+ end
+
+ def cookie_monster
+ cookies["cookie_1"] = nil
+ cookies["cookie_3"] = "chocolate"
+ render plain: "Gone", status: 410
+ end
+
+ def set_cookie
+ cookies["foo"] = "bar"
+ head :ok
+ end
+
+ def get_cookie
+ render plain: cookies["foo"]
+ end
+
+ def redirect
+ redirect_to action_url("get")
+ end
+
+ def remove_header
+ response.headers.delete params[:header]
+ head :ok, "c" => "3"
+ end
+ end
+
+ def test_get
+ with_test_route_set do
+ get "/get"
+ assert_equal 200, status
+ assert_equal "OK", status_message
+ assert_response 200
+ assert_response :success
+ assert_response :ok
+ assert_equal({}, cookies.to_hash)
+ assert_equal "OK", body
+ assert_equal "OK", response.body
+ assert_kind_of Nokogiri::HTML::Document, html_document
+ assert_equal 1, request_count
+ end
+ end
+
+ def test_get_xml_rss_atom
+ %w[ application/xml application/rss+xml application/atom+xml ].each do |mime_string|
+ with_test_route_set do
+ get "/get", headers: { "HTTP_ACCEPT" => mime_string }
+ assert_equal 200, status
+ assert_equal "OK", status_message
+ assert_response 200
+ assert_response :success
+ assert_response :ok
+ assert_equal({}, cookies.to_hash)
+ assert_equal "<root></root>", body
+ assert_equal "<root></root>", response.body
+ assert_instance_of Nokogiri::XML::Document, html_document
+ assert_equal 1, request_count
+ end
+ end
+ end
+
+ def test_post
+ with_test_route_set do
+ post "/post"
+ assert_equal 201, status
+ assert_equal "Created", status_message
+ assert_response 201
+ assert_response :success
+ assert_response :created
+ assert_equal({}, cookies.to_hash)
+ assert_equal "Created", body
+ assert_equal "Created", response.body
+ assert_kind_of Nokogiri::HTML::Document, html_document
+ assert_equal 1, request_count
+ end
+ end
+
+ test "response cookies are added to the cookie jar for the next request" do
+ with_test_route_set do
+ cookies["cookie_1"] = "sugar"
+ cookies["cookie_2"] = "oatmeal"
+ get "/cookie_monster"
+ assert_equal "cookie_1=; path=/\ncookie_3=chocolate; path=/", headers["Set-Cookie"]
+ assert_equal({ "cookie_1" => "", "cookie_2" => "oatmeal", "cookie_3" => "chocolate" }, cookies.to_hash)
+ end
+ end
+
+ test "cookie persist to next request" do
+ with_test_route_set do
+ get "/set_cookie"
+ assert_response :success
+
+ assert_equal "foo=bar; path=/", headers["Set-Cookie"]
+ assert_equal({ "foo" => "bar" }, cookies.to_hash)
+
+ get "/get_cookie"
+ assert_response :success
+ assert_equal "bar", body
+
+ assert_nil headers["Set-Cookie"]
+ assert_equal({ "foo" => "bar" }, cookies.to_hash)
+ end
+ end
+
+ test "cookie persist to next request on another domain" do
+ with_test_route_set do
+ host! "37s.backpack.test"
+
+ get "/set_cookie"
+ assert_response :success
+
+ assert_equal "foo=bar; path=/", headers["Set-Cookie"]
+ assert_equal({ "foo" => "bar" }, cookies.to_hash)
+
+ get "/get_cookie"
+ assert_response :success
+ assert_equal "bar", body
+
+ assert_nil headers["Set-Cookie"]
+ assert_equal({ "foo" => "bar" }, cookies.to_hash)
+ end
+ end
+
+ def test_redirect
+ with_test_route_set do
+ get "/redirect"
+ assert_equal 302, status
+ assert_equal "Found", status_message
+ assert_response 302
+ assert_response :redirect
+ assert_response :found
+ assert_equal "<html><body>You are being <a href=\"http://www.example.com/get\">redirected</a>.</body></html>", response.body
+ assert_kind_of Nokogiri::HTML::Document, html_document
+ assert_equal 1, request_count
+
+ follow_redirect!
+ assert_response :success
+ assert_equal "/get", path
+
+ get "/moved"
+ assert_response :redirect
+ assert_redirected_to "/method"
+ end
+ end
+
+ def test_redirect_reset_html_document
+ with_test_route_set do
+ get "/redirect"
+ previous_html_document = html_document
+
+ follow_redirect!
+
+ assert_response :ok
+ assert_not_same previous_html_document, html_document
+ end
+ end
+
+ def test_redirect_with_arguments
+ with_test_route_set do
+ get "/redirect"
+ follow_redirect! params: { foo: :bar }
+
+ assert_response :ok
+ assert_equal "bar", request.parameters["foo"]
+ end
+ end
+
+ def test_xml_http_request_get
+ with_test_route_set do
+ get "/get", xhr: true
+ assert_equal 200, status
+ assert_equal "OK", status_message
+ assert_response 200
+ assert_response :success
+ assert_response :ok
+ assert_equal "JS OK", response.body
+ end
+ end
+
+ def test_request_with_bad_format
+ with_test_route_set do
+ get "/get.php", xhr: true
+ assert_equal 406, status
+ assert_response 406
+ assert_response :not_acceptable
+ end
+ end
+
+ test "creation of multiple integration sessions" do
+ integration_session # initialize first session
+ a = open_session
+ b = open_session
+
+ assert_not_same(a.integration_session, b.integration_session)
+ end
+
+ def test_get_with_query_string
+ with_test_route_set do
+ get "/get_with_params?foo=bar"
+ assert_equal "/get_with_params?foo=bar", request.env["REQUEST_URI"]
+ assert_equal "/get_with_params?foo=bar", request.fullpath
+ assert_equal "foo=bar", request.env["QUERY_STRING"]
+ assert_equal "foo=bar", request.query_string
+ assert_equal "bar", request.parameters["foo"]
+
+ assert_equal 200, status
+ assert_equal "foo: bar", response.body
+ end
+ end
+
+ def test_get_with_parameters
+ with_test_route_set do
+ get "/get_with_params", params: { foo: "bar" }
+ assert_equal "/get_with_params", request.env["PATH_INFO"]
+ assert_equal "/get_with_params", request.path_info
+ assert_equal "foo=bar", request.env["QUERY_STRING"]
+ assert_equal "foo=bar", request.query_string
+ assert_equal "bar", request.parameters["foo"]
+
+ assert_equal 200, status
+ assert_equal "foo: bar", response.body
+ end
+ end
+
+ def test_post_then_get_with_parameters_do_not_leak_across_requests
+ with_test_route_set do
+ post "/post", params: { leaks: "does-leak?" }
+
+ get "/get_with_params", params: { foo: "bar" }
+
+ assert_empty request.env["rack.input"].string
+ assert_equal "foo=bar", request.env["QUERY_STRING"]
+ assert_equal "foo=bar", request.query_string
+ assert_equal "bar", request.parameters["foo"]
+ assert_predicate request.parameters["leaks"], :nil?
+ end
+ end
+
+ def test_head
+ with_test_route_set do
+ head "/get"
+ assert_equal 200, status
+ assert_equal "", body
+
+ head "/post"
+ assert_equal 201, status
+ assert_equal "", body
+
+ get "/get/method"
+ assert_equal 200, status
+ assert_equal "method: get", body
+
+ head "/get/method"
+ assert_equal 200, status
+ assert_equal "", body
+ end
+ end
+
+ def test_generate_url_with_controller
+ assert_equal "http://www.example.com/foo", url_for(controller: "foo")
+ end
+
+ def test_port_via_host!
+ with_test_route_set do
+ host! "www.example.com:8080"
+ get "/get"
+ assert_equal 8080, request.port
+ end
+ end
+
+ def test_port_via_process
+ with_test_route_set do
+ get "http://www.example.com:8080/get"
+ assert_equal 8080, request.port
+ end
+ end
+
+ def test_https_and_port_via_host_and_https!
+ with_test_route_set do
+ host! "www.example.com"
+ https! true
+
+ get "/get"
+ assert_equal 443, request.port
+ assert_equal true, request.ssl?
+
+ host! "www.example.com:443"
+ https! true
+
+ get "/get"
+ assert_equal 443, request.port
+ assert_equal true, request.ssl?
+
+ host! "www.example.com:8443"
+ https! true
+
+ get "/get"
+ assert_equal 8443, request.port
+ assert_equal true, request.ssl?
+ end
+ end
+
+ def test_https_and_port_via_process
+ with_test_route_set do
+ get "https://www.example.com/get"
+ assert_equal 443, request.port
+ assert_equal true, request.ssl?
+
+ get "https://www.example.com:8443/get"
+ assert_equal 8443, request.port
+ assert_equal true, request.ssl?
+ end
+ end
+
+ def test_respect_removal_of_default_headers_by_a_controller_action
+ with_test_route_set do
+ with_default_headers "a" => "1", "b" => "2" do
+ get "/remove_header", params: { header: "a" }
+ end
+ end
+
+ assert_not_includes @response.headers, "a", "Response should not include default header removed by the controller action"
+ assert_includes @response.headers, "b"
+ assert_includes @response.headers, "c"
+ end
+
+ def test_accept_not_overridden_when_xhr_true
+ with_test_route_set do
+ get "/get", headers: { "Accept" => "application/json" }, xhr: true
+ assert_equal "application/json", request.accept
+ assert_equal "application/json", response.content_type
+
+ get "/get", headers: { "HTTP_ACCEPT" => "application/json" }, xhr: true
+ assert_equal "application/json", request.accept
+ assert_equal "application/json", response.content_type
+ end
+ end
+
+ private
+ def with_default_headers(headers)
+ original = ActionDispatch::Response.default_headers
+ ActionDispatch::Response.default_headers = headers
+ yield
+ ensure
+ ActionDispatch::Response.default_headers = original
+ end
+
+ def with_test_route_set
+ with_routing do |set|
+ controller = ::IntegrationProcessTest::IntegrationController.clone
+ controller.class_eval do
+ include set.url_helpers
+ end
+
+ set.draw do
+ get "moved" => redirect("/method")
+
+ ActiveSupport::Deprecation.silence do
+ match ":action", to: controller, via: [:get, :post], as: :action
+ get "get/:action", to: controller, as: :get_action
+ end
+ end
+
+ singleton_class.include(set.url_helpers)
+
+ yield
+ end
+ end
+end
+
+class MetalIntegrationTest < ActionDispatch::IntegrationTest
+ include SharedTestRoutes.url_helpers
+
+ class Poller
+ def self.call(env)
+ if env["PATH_INFO"] =~ /^\/success/
+ [200, { "Content-Type" => "text/plain", "Content-Length" => "12" }, ["Hello World!"]]
+ else
+ [404, { "Content-Type" => "text/plain", "Content-Length" => "0" }, []]
+ end
+ end
+ end
+
+ def setup
+ @app = Poller
+ end
+
+ def test_successful_get
+ get "/success"
+ assert_response 200
+ assert_response :success
+ assert_response :ok
+ assert_equal "Hello World!", response.body
+ end
+
+ def test_failed_get
+ get "/failure"
+ assert_response 404
+ assert_response :not_found
+ assert_equal "", response.body
+ end
+
+ def test_generate_url_without_controller
+ assert_equal "http://www.example.com/foo", url_for(controller: "foo")
+ end
+
+ def test_pass_headers
+ get "/success", headers: { "Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com" }
+
+ assert_equal "http://nohost.com", @request.env["HTTP_HOST"]
+ assert_equal "http://www.example.com/foo", @request.env["HTTP_REFERER"]
+ end
+
+ def test_pass_headers_and_env
+ get "/success", headers: { "X-Test-Header" => "value" }, env: { "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" }
+
+ assert_equal "http://test.com", @request.env["HTTP_HOST"]
+ assert_equal "http://test.com/", @request.env["HTTP_REFERER"]
+ assert_equal "value", @request.env["HTTP_X_TEST_HEADER"]
+ end
+
+ def test_pass_env
+ get "/success", env: { "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" }
+
+ assert_equal "http://test.com", @request.env["HTTP_HOST"]
+ assert_equal "http://test.com/", @request.env["HTTP_REFERER"]
+ end
+
+ def test_ignores_common_ports_in_host
+ get "http://test.com"
+ assert_equal "test.com", @request.env["HTTP_HOST"]
+
+ get "https://test.com"
+ assert_equal "test.com", @request.env["HTTP_HOST"]
+ end
+
+ def test_keeps_uncommon_ports_in_host
+ get "http://test.com:123"
+ assert_equal "test.com:123", @request.env["HTTP_HOST"]
+
+ get "http://test.com:443"
+ assert_equal "test.com:443", @request.env["HTTP_HOST"]
+
+ get "https://test.com:80"
+ assert_equal "test.com:80", @request.env["HTTP_HOST"]
+ end
+end
+
+class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
+ class TestController < ActionController::Base
+ def index
+ render plain: "index"
+ end
+ end
+
+ def self.call(env)
+ routes.call(env)
+ end
+
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ class MountedApp < Rails::Engine
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ routes.draw do
+ get "baz", to: "application_integration_test/test#index", as: :baz
+ end
+
+ def self.call(*)
+ end
+ end
+
+ routes.draw do
+ get "", to: "application_integration_test/test#index", as: :empty_string
+
+ get "foo", to: "application_integration_test/test#index", as: :foo
+ get "bar", to: "application_integration_test/test#index", as: :bar
+
+ mount MountedApp => "/mounted", :as => "mounted"
+ get "fooz" => proc { |env| [ 200, { "X-Cascade" => "pass" }, [ "omg" ] ] }, :anchor => false
+ get "fooz", to: "application_integration_test/test#index"
+ end
+
+ def app
+ self.class
+ end
+
+ test "includes route helpers" do
+ assert_equal "/", empty_string_path
+ assert_equal "/foo", foo_path
+ assert_equal "/bar", bar_path
+ end
+
+ test "includes mounted helpers" do
+ assert_equal "/mounted/baz", mounted.baz_path
+ end
+
+ test "path after cascade pass" do
+ get "/fooz"
+ assert_equal "index", response.body
+ assert_equal "/fooz", path
+ end
+
+ test "route helpers after controller access" do
+ get "/"
+ assert_equal "/", empty_string_path
+
+ get "/foo"
+ assert_equal "/foo", foo_path
+
+ get "/bar"
+ assert_equal "/bar", bar_path
+ end
+
+ test "missing route helper before controller access" do
+ assert_raise(NameError) { missing_path }
+ end
+
+ test "missing route helper after controller access" do
+ get "/foo"
+ assert_raise(NameError) { missing_path }
+ end
+
+ test "process do not modify the env passed as argument" do
+ env = { :SERVER_NAME => "server", "action_dispatch.custom" => "custom" }
+ old_env = env.dup
+ get "/foo", env: env
+ assert_equal old_env, env
+ end
+end
+
+class EnvironmentFilterIntegrationTest < ActionDispatch::IntegrationTest
+ class TestController < ActionController::Base
+ def post
+ render plain: "Created", status: 201
+ end
+ end
+
+ def self.call(env)
+ env["action_dispatch.parameter_filter"] = [:password]
+ routes.call(env)
+ end
+
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ routes.draw do
+ match "/post", to: "environment_filter_integration_test/test#post", via: :post
+ end
+
+ def app
+ self.class
+ end
+
+ test "filters rack request form vars" do
+ post "/post", params: { username: "cjolly", password: "secret" }
+
+ assert_equal "cjolly", request.filtered_parameters["username"]
+ assert_equal "[FILTERED]", request.filtered_parameters["password"]
+ assert_equal "[FILTERED]", request.filtered_env["rack.request.form_vars"]
+ end
+end
+
+class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest
+ class FooController < ActionController::Base
+ def index
+ render plain: "foo#index"
+ end
+
+ def show
+ render plain: "foo#show"
+ end
+
+ def edit
+ render plain: "foo#show"
+ end
+ end
+
+ class BarController < ActionController::Base
+ def default_url_options
+ { host: "bar.com" }
+ end
+
+ def index
+ render plain: "foo#index"
+ end
+ end
+
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ def self.call(env)
+ routes.call(env)
+ end
+
+ def app
+ self.class
+ end
+
+ routes.draw do
+ default_url_options host: "foo.com"
+
+ scope module: "url_options_integration_test" do
+ get "/foo" => "foo#index", :as => :foos
+ get "/foo/:id" => "foo#show", :as => :foo
+ get "/foo/:id/edit" => "foo#edit", :as => :edit_foo
+ get "/bar" => "bar#index", :as => :bars
+ end
+ end
+
+ test "session uses default url options from routes" do
+ assert_equal "http://foo.com/foo", foos_url
+ end
+
+ test "current host overrides default url options from routes" do
+ get "/foo"
+ assert_response :success
+ assert_equal "http://www.example.com/foo", foos_url
+ end
+
+ test "controller can override default url options from request" do
+ get "/bar"
+ assert_response :success
+ assert_equal "http://bar.com/foo", foos_url
+ end
+
+ def test_can_override_default_url_options
+ original_host = default_url_options.dup
+
+ default_url_options[:host] = "foobar.com"
+ assert_equal "http://foobar.com/foo", foos_url
+
+ get "/bar"
+ assert_response :success
+ assert_equal "http://foobar.com/foo", foos_url
+ ensure
+ ActionDispatch::Integration::Session.default_url_options = self.default_url_options = original_host
+ end
+
+ test "current request path parameters are recalled" do
+ get "/foo/1"
+ assert_response :success
+ assert_equal "/foo/1/edit", url_for(action: "edit", only_path: true)
+ end
+end
+
+class HeadWithStatusActionIntegrationTest < ActionDispatch::IntegrationTest
+ class FooController < ActionController::Base
+ def status
+ head :ok
+ end
+ end
+
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ def self.call(env)
+ routes.call(env)
+ end
+
+ def app
+ self.class
+ end
+
+ routes.draw do
+ get "/foo/status" => "head_with_status_action_integration_test/foo#status"
+ end
+
+ test "get /foo/status with head result does not cause stack overflow error" do
+ assert_nothing_raised do
+ get "/foo/status"
+ end
+ assert_response :ok
+ end
+end
+
+class IntegrationWithRoutingTest < ActionDispatch::IntegrationTest
+ class FooController < ActionController::Base
+ def index
+ render plain: "ok"
+ end
+ end
+
+ def test_with_routing_resets_session
+ klass_namespace = self.class.name.underscore
+
+ with_routing do |routes|
+ routes.draw do
+ namespace klass_namespace do
+ resources :foo, path: "/with"
+ end
+ end
+
+ get "/integration_with_routing_test/with"
+ assert_response 200
+ assert_equal "ok", response.body
+ end
+
+ with_routing do |routes|
+ routes.draw do
+ namespace klass_namespace do
+ resources :foo, path: "/routing"
+ end
+ end
+
+ get "/integration_with_routing_test/routing"
+ assert_response 200
+ assert_equal "ok", response.body
+ end
+ end
+end
+
+# to work in contexts like rspec before(:all)
+class IntegrationRequestsWithoutSetup < ActionDispatch::IntegrationTest
+ self._setup_callbacks = []
+ self._teardown_callbacks = []
+
+ class FooController < ActionController::Base
+ def ok
+ cookies[:key] = "ok"
+ render plain: "ok"
+ end
+ end
+
+ def test_request
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action" => FooController
+ end
+ end
+
+ get "/ok"
+
+ assert_response 200
+ assert_equal "ok", response.body
+ assert_equal "ok", cookies["key"]
+ end
+ end
+end
+
+# to ensure that session requirements in setup are persisted in the tests
+class IntegrationRequestsWithSessionSetup < ActionDispatch::IntegrationTest
+ setup do
+ cookies["user_name"] = "david"
+ end
+
+ def test_cookies_set_in_setup_are_persisted_through_the_session
+ get "/foo"
+ assert_equal({ "user_name" => "david" }, cookies.to_hash)
+ end
+end
+
+class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest
+ class FooController < ActionController::Base
+ def foos
+ render plain: "ok"
+ end
+
+ def foos_json
+ render json: params.permit(:foo)
+ end
+
+ def foos_wibble
+ render plain: "ok"
+ end
+ end
+
+ def test_standard_json_encoding_works
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ post ":action" => FooController
+ end
+ end
+
+ post "/foos_json.json", params: { foo: "fighters" }.to_json,
+ headers: { "Content-Type" => "application/json" }
+
+ assert_response :success
+ assert_equal({ "foo" => "fighters" }, response.parsed_body)
+ end
+ end
+
+ def test_encoding_as_json
+ post_to_foos as: :json do
+ assert_response :success
+ assert_equal "application/json", request.content_type
+ assert_equal "application/json", request.accepts.first.to_s
+ assert_equal :json, request.format.ref
+ assert_equal({ "foo" => "fighters" }, request.request_parameters)
+ assert_equal({ "foo" => "fighters" }, response.parsed_body)
+ end
+ end
+
+ def test_doesnt_mangle_request_path
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ post ":action" => FooController
+ end
+ end
+
+ post "/foos"
+ assert_equal "/foos", request.path
+
+ post "/foos_json", as: :json
+ assert_equal "/foos_json", request.path
+ end
+ end
+
+ def test_encoding_as_without_mime_registration
+ assert_raise ArgumentError do
+ ActionDispatch::IntegrationTest.register_encoder :wibble
+ end
+ end
+
+ def test_registering_custom_encoder
+ Mime::Type.register "text/wibble", :wibble
+
+ ActionDispatch::IntegrationTest.register_encoder(:wibble,
+ param_encoder: -> params { params })
+
+ post_to_foos as: :wibble do
+ assert_response :success
+ assert_equal "/foos_wibble", request.path
+ assert_equal "text/wibble", request.content_type
+ assert_equal "text/wibble", request.accepts.first.to_s
+ assert_equal :wibble, request.format.ref
+ assert_equal Hash.new, request.request_parameters # Unregistered MIME Type can't be parsed.
+ assert_equal "ok", response.parsed_body
+ end
+ ensure
+ Mime::Type.unregister :wibble
+ end
+
+ def test_parsed_body_without_as_option
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action" => FooController
+ end
+ end
+
+ get "/foos_json.json", params: { foo: "heyo" }
+
+ assert_equal({ "foo" => "heyo" }, response.parsed_body)
+ end
+ end
+
+ def test_get_parameters_with_as_option
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action" => FooController
+ end
+ end
+
+ get "/foos_json?foo=heyo", as: :json
+
+ assert_equal({ "foo" => "heyo" }, response.parsed_body)
+ end
+ end
+
+ def test_get_request_with_json_uses_method_override_and_sends_a_post_request
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action" => FooController
+ end
+ end
+
+ get "/foos_json", params: { foo: "heyo" }, as: :json
+
+ assert_equal "POST", request.method
+ assert_equal "GET", request.headers["X-Http-Method-Override"]
+ assert_equal({ "foo" => "heyo" }, response.parsed_body)
+ end
+ end
+
+ def test_get_request_with_json_excludes_null_query_string
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":action" => FooController
+ end
+ end
+
+ get "/foos_json", as: :json
+
+ assert_equal "http://www.example.com/foos_json", request.url
+ end
+ end
+
+ private
+ def post_to_foos(as:)
+ with_routing do |routes|
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ post ":action" => FooController
+ end
+ end
+
+ post "/foos_#{as}", params: { foo: "fighters" }, as: as
+
+ yield
+ end
+ end
+end
+
+class IntegrationFileUploadTest < ActionDispatch::IntegrationTest
+ class IntegrationController < ActionController::Base
+ def test_file_upload
+ render plain: params[:file].size
+ end
+ end
+
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ def self.call(env)
+ routes.call(env)
+ end
+
+ def app
+ self.class
+ end
+
+ def self.fixture_path
+ File.expand_path("../fixtures/multipart", __dir__)
+ end
+
+ routes.draw do
+ post "test_file_upload", to: "integration_file_upload_test/integration#test_file_upload"
+ end
+
+ def test_fixture_file_upload
+ post "/test_file_upload",
+ params: {
+ file: fixture_file_upload("/ruby_on_rails.jpg", "image/jpg")
+ }
+ assert_equal "45142", @response.body
+ end
+end
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
new file mode 100644
index 0000000000..d81c43b87d
--- /dev/null
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -0,0 +1,518 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "timeout"
+require "concurrent/atomic/count_down_latch"
+Thread.abort_on_exception = true
+
+module ActionController
+ class SSETest < ActionController::TestCase
+ class SSETestController < ActionController::Base
+ include ActionController::Live
+
+ def basic_sse
+ response.headers["Content-Type"] = "text/event-stream"
+ sse = SSE.new(response.stream)
+ sse.write("{\"name\":\"John\"}")
+ sse.write(name: "Ryan")
+ ensure
+ sse.close
+ end
+
+ def sse_with_event
+ sse = SSE.new(response.stream, event: "send-name")
+ sse.write("{\"name\":\"John\"}")
+ sse.write(name: "Ryan")
+ ensure
+ sse.close
+ end
+
+ def sse_with_retry
+ sse = SSE.new(response.stream, retry: 1000)
+ sse.write("{\"name\":\"John\"}")
+ sse.write({ name: "Ryan" }, { retry: 1500 })
+ ensure
+ sse.close
+ end
+
+ def sse_with_id
+ sse = SSE.new(response.stream)
+ sse.write("{\"name\":\"John\"}", id: 1)
+ sse.write({ name: "Ryan" }, { id: 2 })
+ ensure
+ sse.close
+ end
+
+ def sse_with_multiple_line_message
+ sse = SSE.new(response.stream)
+ sse.write("first line.\nsecond line.")
+ ensure
+ sse.close
+ end
+ end
+
+ tests SSETestController
+
+ def wait_for_response_stream_close
+ response.body
+ end
+
+ def test_basic_sse
+ get :basic_sse
+
+ wait_for_response_stream_close
+ assert_match(/data: {\"name\":\"John\"}/, response.body)
+ assert_match(/data: {\"name\":\"Ryan\"}/, response.body)
+ end
+
+ def test_sse_with_event_name
+ get :sse_with_event
+
+ wait_for_response_stream_close
+ assert_match(/data: {\"name\":\"John\"}/, response.body)
+ assert_match(/data: {\"name\":\"Ryan\"}/, response.body)
+ assert_match(/event: send-name/, response.body)
+ end
+
+ def test_sse_with_retry
+ get :sse_with_retry
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n\n")
+ assert_match(/data: {\"name\":\"John\"}/, first_response)
+ assert_match(/retry: 1000/, first_response)
+
+ assert_match(/data: {\"name\":\"Ryan\"}/, second_response)
+ assert_match(/retry: 1500/, second_response)
+ end
+
+ def test_sse_with_id
+ get :sse_with_id
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n\n")
+ assert_match(/data: {\"name\":\"John\"}/, first_response)
+ assert_match(/id: 1/, first_response)
+
+ assert_match(/data: {\"name\":\"Ryan\"}/, second_response)
+ assert_match(/id: 2/, second_response)
+ end
+
+ def test_sse_with_multiple_line_message
+ get :sse_with_multiple_line_message
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n")
+ assert_match(/data: first line/, first_response)
+ assert_match(/data: second line/, second_response)
+ end
+ end
+
+ class LiveStreamTest < ActionController::TestCase
+ class Exception < StandardError
+ end
+
+ class TestController < ActionController::Base
+ include ActionController::Live
+
+ attr_accessor :latch, :tc, :error_latch
+
+ def self.controller_path
+ "test"
+ end
+
+ def set_cookie
+ cookies[:hello] = "world"
+ response.stream.write "hello world"
+ response.close
+ end
+
+ def render_text
+ render plain: "zomg"
+ end
+
+ def default_header
+ response.stream.write "<html><body>hi</body></html>"
+ response.stream.close
+ end
+
+ def basic_stream
+ response.headers["Content-Type"] = "text/event-stream"
+ %w{ hello world }.each do |word|
+ response.stream.write word
+ end
+ response.stream.close
+ end
+
+ def blocking_stream
+ response.headers["Content-Type"] = "text/event-stream"
+ %w{ hello world }.each do |word|
+ response.stream.write word
+ latch.wait
+ end
+ response.stream.close
+ end
+
+ def write_sleep_autoload
+ path = File.expand_path("../fixtures", __dir__)
+ ActiveSupport::Dependencies.autoload_paths << path
+
+ response.headers["Content-Type"] = "text/event-stream"
+ response.stream.write "before load"
+ sleep 0.01
+ silence_warning do
+ ::LoadMe
+ end
+ response.stream.close
+ latch.count_down
+
+ ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path }
+ end
+
+ def thread_locals
+ tc.assert_equal "aaron", Thread.current[:setting]
+
+ response.headers["Content-Type"] = "text/event-stream"
+ %w{ hello world }.each do |word|
+ response.stream.write word
+ end
+ response.stream.close
+ end
+
+ def with_stale
+ render plain: "stale" if stale?(etag: "123", template: false)
+ end
+
+ def exception_in_view
+ render "doesntexist"
+ end
+
+ def exception_in_view_after_commit
+ response.stream.write ""
+ render "doesntexist"
+ end
+
+ def exception_with_callback
+ response.headers["Content-Type"] = "text/event-stream"
+
+ response.stream.on_error do
+ response.stream.write %(data: "500 Internal Server Error"\n\n)
+ response.stream.close
+ end
+
+ response.stream.write "" # make sure the response is committed
+ raise "An exception occurred..."
+ end
+
+ def exception_in_controller
+ raise Exception, "Exception in controller"
+ end
+
+ def bad_request_error
+ raise ActionController::BadRequest
+ end
+
+ def exception_in_exception_callback
+ response.headers["Content-Type"] = "text/event-stream"
+ response.stream.on_error do
+ raise "We need to go deeper."
+ end
+ response.stream.write ""
+ response.stream.write params[:widget][:didnt_check_for_nil]
+ end
+
+ def overfill_buffer_and_die
+ logger = ActionController::Base.logger || Logger.new($stdout)
+ response.stream.on_error do
+ logger.warn "Error while streaming."
+ error_latch.count_down
+ end
+
+ # Write until the buffer is full. It doesn't expose that
+ # information directly, so we must hard-code its size:
+ 10.times do
+ response.stream.write "."
+ end
+ # .. plus one more, because the #each frees up a slot:
+ response.stream.write "."
+
+ latch.count_down
+
+ # This write will block, and eventually raise
+ response.stream.write "x"
+
+ 20.times do
+ response.stream.write "."
+ end
+ end
+
+ def ignore_client_disconnect
+ response.stream.ignore_disconnect = true
+
+ response.stream.write "" # commit
+
+ # These writes will be ignored
+ 15.times do
+ response.stream.write "x"
+ end
+
+ logger.info "Work complete"
+ latch.count_down
+ end
+ end
+
+ tests TestController
+
+ def assert_stream_closed
+ assert response.stream.closed?, "stream should be closed"
+ assert response.committed?, "response should be committed"
+ assert response.sent?, "response should be sent"
+ end
+
+ def capture_log_output
+ output = StringIO.new
+ old_logger, ActionController::Base.logger = ActionController::Base.logger, ActiveSupport::Logger.new(output)
+
+ begin
+ yield output
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+ end
+
+ def setup
+ super
+
+ def @controller.new_controller_thread
+ Thread.new { yield }
+ end
+ end
+
+ def test_set_cookie
+ get :set_cookie
+ assert_equal({ "hello" => "world" }, @response.cookies)
+ assert_equal "hello world", @response.body
+ end
+
+ def test_write_to_stream
+ get :basic_stream
+ assert_equal "helloworld", @response.body
+ assert_equal "text/event-stream", @response.headers["Content-Type"]
+ end
+
+ def test_delayed_autoload_after_write_within_interlock_hook
+ # Simulate InterlockHook
+ ActiveSupport::Dependencies.interlock.start_running
+ res = get :write_sleep_autoload
+ res.each { }
+ ActiveSupport::Dependencies.interlock.done_running
+ end
+
+ def test_async_stream
+ rubinius_skip "https://github.com/rubinius/rubinius/issues/2934"
+
+ @controller.latch = Concurrent::CountDownLatch.new
+ parts = ["hello", "world"]
+
+ get :blocking_stream
+
+ t = Thread.new(response) { |resp|
+ resp.await_commit
+ resp.stream.each do |part|
+ assert_equal parts.shift, part
+ ol = @controller.latch
+ @controller.latch = Concurrent::CountDownLatch.new
+ ol.count_down
+ end
+ }
+
+ assert t.join(3), "timeout expired before the thread terminated"
+ end
+
+ def test_abort_with_full_buffer
+ @controller.latch = Concurrent::CountDownLatch.new
+ @controller.error_latch = Concurrent::CountDownLatch.new
+
+ capture_log_output do |output|
+ get :overfill_buffer_and_die, format: "plain"
+
+ t = Thread.new(response) { |resp|
+ resp.await_commit
+ _, _, body = resp.to_a
+ body.each do
+ @controller.latch.wait
+ body.close
+ break
+ end
+ }
+
+ t.join
+ @controller.error_latch.wait
+ assert_match "Error while streaming", output.rewind && output.read
+ end
+ end
+
+ def test_ignore_client_disconnect
+ @controller.latch = Concurrent::CountDownLatch.new
+
+ capture_log_output do |output|
+ get :ignore_client_disconnect
+
+ t = Thread.new(response) { |resp|
+ resp.await_commit
+ _, _, body = resp.to_a
+ body.each do
+ body.close
+ break
+ end
+ }
+
+ t.join
+ Timeout.timeout(3) do
+ @controller.latch.wait
+ end
+ assert_match "Work complete", output.rewind && output.read
+ end
+ end
+
+ def test_thread_locals_get_copied
+ @controller.tc = self
+ Thread.current[:originating_thread] = Thread.current.object_id
+ Thread.current[:setting] = "aaron"
+
+ get :thread_locals
+ end
+
+ def test_live_stream_default_header
+ get :default_header
+ assert response.headers["Content-Type"]
+ end
+
+ def test_render_text
+ get :render_text
+ assert_equal "zomg", response.body
+ assert_stream_closed
+ end
+
+ def test_exception_handling_html
+ assert_raises(ActionView::MissingTemplate) do
+ get :exception_in_view
+ end
+
+ capture_log_output do |output|
+ get :exception_in_view_after_commit
+ assert_match %r((window\.location = "/500\.html"</script></html>)$), response.body
+ assert_match "Missing template test/doesntexist", output.rewind && output.read
+ assert_stream_closed
+ end
+ assert response.body
+ assert_stream_closed
+ end
+
+ def test_exception_handling_plain_text
+ assert_raises(ActionView::MissingTemplate) do
+ get :exception_in_view, format: :json
+ end
+
+ capture_log_output do |output|
+ get :exception_in_view_after_commit, format: :json
+ assert_equal "", response.body
+ assert_match "Missing template test/doesntexist", output.rewind && output.read
+ assert_stream_closed
+ end
+ end
+
+ def test_exception_callback_when_committed
+ current_threads = Thread.list
+
+ capture_log_output do |output|
+ get :exception_with_callback, format: "text/event-stream"
+
+ # Wait on the execution of all threads
+ (Thread.list - current_threads).each(&:join)
+
+ assert_equal %(data: "500 Internal Server Error"\n\n), response.body
+ assert_match "An exception occurred...", output.rewind && output.read
+ assert_stream_closed
+ end
+ end
+
+ def test_exception_in_controller_before_streaming
+ assert_raises(ActionController::LiveStreamTest::Exception) do
+ get :exception_in_controller, format: "text/event-stream"
+ end
+ end
+
+ def test_bad_request_in_controller_before_streaming
+ assert_raises(ActionController::BadRequest) do
+ get :bad_request_error, format: "text/event-stream"
+ end
+ end
+
+ def test_exceptions_raised_handling_exceptions_and_committed
+ capture_log_output do |output|
+ get :exception_in_exception_callback, format: "text/event-stream"
+ assert_equal "", response.body
+ assert_match "We need to go deeper", output.rewind && output.read
+ assert_stream_closed
+ end
+ end
+
+ def test_stale_without_etag
+ get :with_stale
+ assert_equal 200, response.status.to_i
+ end
+
+ def test_stale_with_etag
+ @request.if_none_match = %(W/"#{ActiveSupport::Digest.hexdigest('123')}")
+ get :with_stale
+ assert_equal 304, response.status.to_i
+ end
+ end
+
+ class BufferTest < ActionController::TestCase
+ def test_nil_callback
+ buf = ActionController::Live::Buffer.new nil
+ assert buf.call_on_error
+ end
+ end
+end
+
+class LiveStreamRouterTest < ActionDispatch::IntegrationTest
+ class TestController < ActionController::Base
+ include ActionController::Live
+
+ def index
+ response.headers["Content-Type"] = "text/event-stream"
+ sse = SSE.new(response.stream)
+ sse.write("{\"name\":\"John\"}")
+ sse.write(name: "Ryan")
+ ensure
+ sse.close
+ end
+ end
+
+ def self.call(env)
+ routes.call(env)
+ end
+
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ routes.draw do
+ get "/test" => "live_stream_router_test/test#index"
+ end
+
+ def app
+ self.class
+ end
+
+ test "streaming served through the router" do
+ get "/test"
+
+ assert_response :ok
+ assert_match(/data: {\"name\":\"John\"}/, response.body)
+ assert_match(/data: {\"name\":\"Ryan\"}/, response.body)
+ end
+end
diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb
new file mode 100644
index 0000000000..d84a76fb46
--- /dev/null
+++ b/actionpack/test/controller/localized_templates_test.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class LocalizedController < ActionController::Base
+ def hello_world
+ end
+end
+
+class LocalizedTemplatesTest < ActionController::TestCase
+ tests LocalizedController
+
+ setup do
+ @old_locale = I18n.locale
+ end
+
+ teardown do
+ I18n.locale = @old_locale
+ end
+
+ def test_localized_template_is_used
+ I18n.locale = :de
+ get :hello_world
+ assert_equal "Guten Tag", @response.body
+ end
+
+ def test_default_locale_template_is_used_when_locale_is_missing
+ I18n.locale = :dk
+ get :hello_world
+ assert_equal "Hello World", @response.body
+ end
+
+ def test_use_fallback_locales
+ I18n.locale = :"de-AT"
+ I18n.backend.class.include(I18n::Backend::Fallbacks)
+ I18n.fallbacks[:"de-AT"] = [:de]
+
+ get :hello_world
+ assert_equal "Guten Tag", @response.body
+ end
+
+ def test_localized_template_has_correct_header_with_no_format_in_template_name
+ I18n.locale = :it
+ get :hello_world
+ assert_equal "Ciao Mondo", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+end
diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb
new file mode 100644
index 0000000000..0562c16284
--- /dev/null
+++ b/actionpack/test/controller/log_subscriber_test.rb
@@ -0,0 +1,369 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/log_subscriber/test_helper"
+require "action_controller/log_subscriber"
+
+module Another
+ class LogSubscribersController < ActionController::Base
+ wrap_parameters :person, include: :name, format: :json
+
+ class SpecialException < Exception
+ end
+
+ rescue_from SpecialException do
+ head 406
+ end
+
+ before_action :redirector, only: :never_executed
+
+ def never_executed
+ end
+
+ def show
+ head :ok
+ end
+
+ def redirector
+ redirect_to "http://foo.bar/"
+ end
+
+ def filterable_redirector
+ redirect_to "http://secret.foo.bar/"
+ end
+
+ def data_sender
+ send_data "cool data", filename: "file.txt"
+ end
+
+ def file_sender
+ send_file File.expand_path("company.rb", FIXTURE_LOAD_PATH)
+ end
+
+ def with_fragment_cache
+ render inline: "<%= cache('foo'){ 'bar' } %>"
+ end
+
+ def with_fragment_cache_and_percent_in_key
+ render inline: "<%= cache('foo%bar'){ 'Contains % sign in key' } %>"
+ end
+
+ def with_fragment_cache_if_with_true_condition
+ render inline: "<%= cache_if(true, 'foo') { 'bar' } %>"
+ end
+
+ def with_fragment_cache_if_with_false_condition
+ render inline: "<%= cache_if(false, 'foo') { 'bar' } %>"
+ end
+
+ def with_fragment_cache_unless_with_false_condition
+ render inline: "<%= cache_unless(false, 'foo') { 'bar' } %>"
+ end
+
+ def with_fragment_cache_unless_with_true_condition
+ render inline: "<%= cache_unless(true, 'foo') { 'bar' } %>"
+ end
+
+ def with_exception
+ raise Exception
+ end
+
+ def with_rescued_exception
+ raise SpecialException
+ end
+
+ def with_action_not_found
+ raise AbstractController::ActionNotFound
+ end
+
+ def append_info_to_payload(payload)
+ super
+ payload[:test_key] = "test_value"
+ @last_payload = payload
+ end
+
+ attr_reader :last_payload
+ end
+end
+
+class ACLogSubscriberTest < ActionController::TestCase
+ tests Another::LogSubscribersController
+ include ActiveSupport::LogSubscriber::TestHelper
+
+ def setup
+ super
+ ActionController::Base.enable_fragment_cache_logging = true
+
+ @old_logger = ActionController::Base.logger
+
+ @cache_path = Dir.mktmpdir(%w[tmp cache])
+ @controller.cache_store = :file_store, @cache_path
+ ActionController::LogSubscriber.attach_to :action_controller
+ end
+
+ def teardown
+ super
+ ActiveSupport::LogSubscriber.log_subscribers.clear
+ FileUtils.rm_rf(@cache_path)
+ ActionController::Base.logger = @old_logger
+ ActionController::Base.enable_fragment_cache_logging = true
+ end
+
+ def set_logger(logger)
+ ActionController::Base.logger = logger
+ end
+
+ def test_start_processing
+ get :show
+ wait
+ assert_equal 2, logs.size
+ assert_equal "Processing by Another::LogSubscribersController#show as HTML", logs.first
+ end
+
+ def test_halted_callback
+ get :never_executed
+ wait
+ assert_equal 4, logs.size
+ assert_equal "Filter chain halted as :redirector rendered or redirected", logs.third
+ end
+
+ def test_process_action
+ get :show
+ wait
+ assert_equal 2, logs.size
+ assert_match(/Completed/, logs.last)
+ assert_match(/200 OK/, logs.last)
+ end
+
+ def test_process_action_without_parameters
+ get :show
+ wait
+ assert_nil logs.detect { |l| l =~ /Parameters/ }
+ end
+
+ def test_process_action_with_parameters
+ get :show, params: { id: "10" }
+ wait
+
+ assert_equal 3, logs.size
+ assert_equal 'Parameters: {"id"=>"10"}', logs[1]
+ end
+
+ def test_multiple_process_with_parameters
+ get :show, params: { id: "10" }
+ get :show, params: { id: "20" }
+
+ wait
+
+ assert_equal 6, logs.size
+ assert_equal 'Parameters: {"id"=>"10"}', logs[1]
+ assert_equal 'Parameters: {"id"=>"20"}', logs[4]
+ end
+
+ def test_process_action_with_wrapped_parameters
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :show, params: { id: "10", name: "jose" }
+ wait
+
+ assert_equal 3, logs.size
+ assert_match '"person"=>{"name"=>"jose"}', logs[1]
+ end
+
+ def test_process_action_with_view_runtime
+ get :show
+ wait
+ assert_match(/Completed 200 OK in \d+ms/, logs[1])
+ end
+
+ def test_append_info_to_payload_is_called_even_with_exception
+ begin
+ get :with_exception
+ wait
+ rescue Exception
+ end
+
+ assert_equal "test_value", @controller.last_payload[:test_key]
+ end
+
+ def test_process_action_headers
+ get :show
+ wait
+ assert_equal "Rails Testing", @controller.last_payload[:headers]["User-Agent"]
+ end
+
+ def test_process_action_with_filter_parameters
+ @request.env["action_dispatch.parameter_filter"] = [:lifo, :amount]
+
+ get :show, params: {
+ lifo: "Pratik", amount: "420", step: "1"
+ }
+ wait
+
+ params = logs[1]
+ assert_match(/"amount"=>"\[FILTERED\]"/, params)
+ assert_match(/"lifo"=>"\[FILTERED\]"/, params)
+ assert_match(/"step"=>"1"/, params)
+ end
+
+ def test_redirect_to
+ get :redirector
+ wait
+
+ assert_equal 3, logs.size
+ assert_equal "Redirected to http://foo.bar/", logs[1]
+ end
+
+ def test_filter_redirect_url_by_string
+ @request.env["action_dispatch.redirect_filter"] = ["secret"]
+ get :filterable_redirector
+ wait
+
+ assert_equal 3, logs.size
+ assert_equal "Redirected to [FILTERED]", logs[1]
+ end
+
+ def test_filter_redirect_url_by_regexp
+ @request.env["action_dispatch.redirect_filter"] = [/secret\.foo.+/]
+ get :filterable_redirector
+ wait
+
+ assert_equal 3, logs.size
+ assert_equal "Redirected to [FILTERED]", logs[1]
+ end
+
+ def test_send_data
+ get :data_sender
+ wait
+
+ assert_equal 3, logs.size
+ assert_match(/Sent data file\.txt/, logs[1])
+ end
+
+ def test_send_file
+ get :file_sender
+ wait
+
+ assert_equal 3, logs.size
+ assert_match(/Sent file/, logs[1])
+ assert_match(/test\/fixtures\/company\.rb/, logs[1])
+ end
+
+ def test_with_fragment_cache
+ @controller.config.perform_caching = true
+ get :with_fragment_cache
+ wait
+
+ assert_equal 4, logs.size
+ assert_match(/Read fragment views\/foo/, logs[1])
+ assert_match(/Write fragment views\/foo/, logs[2])
+ ensure
+ @controller.config.perform_caching = true
+ end
+
+ def test_with_fragment_cache_when_log_disabled
+ @controller.config.perform_caching = true
+ ActionController::Base.enable_fragment_cache_logging = false
+ get :with_fragment_cache
+ wait
+
+ assert_equal 2, logs.size
+ assert_equal "Processing by Another::LogSubscribersController#with_fragment_cache as HTML", logs[0]
+ assert_match(/Completed 200 OK in \d+ms/, logs[1])
+ ensure
+ @controller.config.perform_caching = true
+ ActionController::Base.enable_fragment_cache_logging = true
+ end
+
+ def test_with_fragment_cache_if_with_true
+ @controller.config.perform_caching = true
+ get :with_fragment_cache_if_with_true_condition
+ wait
+
+ assert_equal 4, logs.size
+ assert_match(/Read fragment views\/foo/, logs[1])
+ assert_match(/Write fragment views\/foo/, logs[2])
+ ensure
+ @controller.config.perform_caching = true
+ end
+
+ def test_with_fragment_cache_if_with_false
+ @controller.config.perform_caching = true
+ get :with_fragment_cache_if_with_false_condition
+ wait
+
+ assert_equal 2, logs.size
+ assert_no_match(/Read fragment views\/foo/, logs[1])
+ assert_no_match(/Write fragment views\/foo/, logs[2])
+ ensure
+ @controller.config.perform_caching = true
+ end
+
+ def test_with_fragment_cache_unless_with_true
+ @controller.config.perform_caching = true
+ get :with_fragment_cache_unless_with_true_condition
+ wait
+
+ assert_equal 2, logs.size
+ assert_no_match(/Read fragment views\/foo/, logs[1])
+ assert_no_match(/Write fragment views\/foo/, logs[2])
+ ensure
+ @controller.config.perform_caching = true
+ end
+
+ def test_with_fragment_cache_unless_with_false
+ @controller.config.perform_caching = true
+ get :with_fragment_cache_unless_with_false_condition
+ wait
+
+ assert_equal 4, logs.size
+ assert_match(/Read fragment views\/foo/, logs[1])
+ assert_match(/Write fragment views\/foo/, logs[2])
+ ensure
+ @controller.config.perform_caching = true
+ end
+
+ def test_with_fragment_cache_and_percent_in_key
+ @controller.config.perform_caching = true
+ get :with_fragment_cache_and_percent_in_key
+ wait
+
+ assert_equal 4, logs.size
+ assert_match(/Read fragment views\/foo/, logs[1])
+ assert_match(/Write fragment views\/foo/, logs[2])
+ ensure
+ @controller.config.perform_caching = true
+ end
+
+ def test_process_action_with_exception_includes_http_status_code
+ begin
+ get :with_exception
+ wait
+ rescue Exception
+ end
+ assert_equal 2, logs.size
+ assert_match(/Completed 500/, logs.last)
+ end
+
+ def test_process_action_with_rescued_exception_includes_http_status_code
+ get :with_rescued_exception
+ wait
+
+ assert_equal 2, logs.size
+ assert_match(/Completed 406/, logs.last)
+ end
+
+ def test_process_action_with_with_action_not_found_logs_404
+ begin
+ get :with_action_not_found
+ wait
+ rescue AbstractController::ActionNotFound
+ end
+
+ assert_equal 2, logs.size
+ assert_match(/Completed 404/, logs.last)
+ end
+
+ def logs
+ @logs ||= @logger.logged(:info)
+ end
+end
diff --git a/actionpack/test/controller/metal/renderers_test.rb b/actionpack/test/controller/metal/renderers_test.rb
new file mode 100644
index 0000000000..5f0d125128
--- /dev/null
+++ b/actionpack/test/controller/metal/renderers_test.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/hash/conversions"
+
+class MetalRenderingController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+ include ActionController::Renderers
+end
+
+class MetalRenderingJsonController < MetalRenderingController
+ class Model
+ def to_json(options = {})
+ { a: "b" }.to_json(options)
+ end
+
+ def to_xml(options = {})
+ { a: "b" }.to_xml(options)
+ end
+ end
+
+ use_renderers :json
+
+ def one
+ render json: Model.new
+ end
+
+ def two
+ render xml: Model.new
+ end
+end
+
+class RenderersMetalTest < ActionController::TestCase
+ tests MetalRenderingJsonController
+
+ def test_render_json
+ get :one
+ assert_response :success
+ assert_equal({ a: "b" }.to_json, @response.body)
+ assert_equal "application/json", @response.content_type
+ end
+
+ def test_render_xml
+ get :two
+ assert_response :success
+ assert_equal(" ", @response.body)
+ assert_equal "text/plain", @response.content_type
+ end
+end
diff --git a/actionpack/test/controller/metal_test.rb b/actionpack/test/controller/metal_test.rb
new file mode 100644
index 0000000000..7b53092266
--- /dev/null
+++ b/actionpack/test/controller/metal_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class MetalControllerInstanceTests < ActiveSupport::TestCase
+ class SimpleController < ActionController::Metal
+ def hello
+ self.response_body = "hello"
+ end
+ end
+
+ def test_response_does_not_have_default_headers
+ original_default_headers = ActionDispatch::Response.default_headers
+
+ ActionDispatch::Response.default_headers = {
+ "X-Frame-Options" => "DENY",
+ "X-Content-Type-Options" => "nosniff",
+ "X-XSS-Protection" => "1;"
+ }
+
+ response_headers = SimpleController.action("hello").call(
+ "REQUEST_METHOD" => "GET",
+ "rack.input" => -> { }
+ )[1]
+
+ assert_not response_headers.key?("X-Frame-Options")
+ assert_not response_headers.key?("X-Content-Type-Options")
+ assert_not response_headers.key?("X-XSS-Protection")
+ ensure
+ ActionDispatch::Response.default_headers = original_default_headers
+ end
+end
diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb
new file mode 100644
index 0000000000..eed671d593
--- /dev/null
+++ b/actionpack/test/controller/mime/accept_format_test.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class StarStarMimeController < ActionController::Base
+ layout nil
+
+ def index
+ render
+ end
+end
+
+class StarStarMimeControllerTest < ActionController::TestCase
+ def test_javascript_with_format
+ @request.accept = "text/javascript"
+ get :index, format: "js"
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+
+ def test_javascript_with_no_format
+ @request.accept = "text/javascript"
+ get :index
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+
+ def test_javascript_with_no_format_only_star_star
+ @request.accept = "*/*"
+ get :index
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+end
+
+class AbstractPostController < ActionController::Base
+ self.view_paths = File.expand_path("../../fixtures/post_test", __dir__)
+end
+
+# For testing layouts which are set automatically
+class PostController < AbstractPostController
+ around_action :with_iphone
+
+ def index
+ respond_to(:html, :iphone, :js)
+ end
+
+private
+
+ def with_iphone
+ request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
+ yield
+ end
+end
+
+class SuperPostController < PostController
+end
+
+class MimeControllerLayoutsTest < ActionController::TestCase
+ tests PostController
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ Mime::Type.register_alias("text/html", :iphone)
+ end
+
+ def teardown
+ super
+ Mime::Type.unregister(:iphone)
+ end
+
+ def test_missing_layout_renders_properly
+ get :index
+ assert_equal '<html><div id="html">Hello Firefox</div></html>', @response.body
+
+ @request.accept = "text/iphone"
+ get :index
+ assert_equal "Hello iPhone", @response.body
+ end
+
+ def test_format_with_inherited_layouts
+ @controller = SuperPostController.new
+
+ get :index
+ assert_equal '<html><div id="html">Super Firefox</div></html>', @response.body
+
+ @request.accept = "text/iphone"
+ get :index
+ assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body
+ end
+
+ def test_non_navigational_format_with_no_template_fallbacks_to_html_template_with_no_layout
+ get :index, format: :js
+ assert_equal "Hello Firefox", @response.body
+ end
+end
diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb
new file mode 100644
index 0000000000..00e1d5f3b3
--- /dev/null
+++ b/actionpack/test/controller/mime/respond_to_test.rb
@@ -0,0 +1,877 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/log_subscriber/test_helper"
+
+class RespondToController < ActionController::Base
+ layout :set_layout
+
+ before_action {
+ case params[:v]
+ when String then request.variant = params[:v].to_sym
+ when Array then request.variant = params[:v].map(&:to_sym)
+ end
+ }
+
+ def html_xml_or_rss
+ respond_to do |type|
+ type.html { render body: "HTML" }
+ type.xml { render body: "XML" }
+ type.rss { render body: "RSS" }
+ type.all { render body: "Nothing" }
+ end
+ end
+
+ def js_or_html
+ respond_to do |type|
+ type.html { render body: "HTML" }
+ type.js { render body: "JS" }
+ type.all { render body: "Nothing" }
+ end
+ end
+
+ def json_or_yaml
+ respond_to do |type|
+ type.json { render body: "JSON" }
+ type.yaml { render body: "YAML" }
+ end
+ end
+
+ def html_or_xml
+ respond_to do |type|
+ type.html { render body: "HTML" }
+ type.xml { render body: "XML" }
+ type.all { render body: "Nothing" }
+ end
+ end
+
+ def json_xml_or_html
+ respond_to do |type|
+ type.json { render body: "JSON" }
+ type.xml { render xml: "XML" }
+ type.html { render body: "HTML" }
+ end
+ end
+
+ def forced_xml
+ request.format = :xml
+
+ respond_to do |type|
+ type.html { render body: "HTML" }
+ type.xml { render body: "XML" }
+ end
+ end
+
+ def just_xml
+ respond_to do |type|
+ type.xml { render body: "XML" }
+ end
+ end
+
+ def using_defaults
+ respond_to do |type|
+ type.html
+ type.xml
+ end
+ end
+
+ def missing_templates
+ respond_to do |type|
+ # This test requires a block that is empty
+ type.json { }
+ type.xml
+ end
+ end
+
+ def using_defaults_with_type_list
+ respond_to(:html, :xml)
+ end
+
+ def using_defaults_with_all
+ respond_to do |type|
+ type.html
+ type.all { render body: "ALL" }
+ end
+ end
+
+ def made_for_content_type
+ respond_to do |type|
+ type.rss { render body: "RSS" }
+ type.atom { render body: "ATOM" }
+ type.all { render body: "Nothing" }
+ end
+ end
+
+ def using_conflicting_nested_js_then_html
+ respond_to do |outer_type|
+ outer_type.js do
+ respond_to do |inner_type|
+ inner_type.html { render body: "HTML" }
+ end
+ end
+ end
+ end
+
+ def using_non_conflicting_nested_js_then_js
+ respond_to do |outer_type|
+ outer_type.js do
+ respond_to do |inner_type|
+ inner_type.js { render body: "JS" }
+ end
+ end
+ end
+ end
+
+ def custom_type_handling
+ respond_to do |type|
+ type.html { render body: "HTML" }
+ type.custom("application/crazy-xml") { render body: "Crazy XML" }
+ type.all { render body: "Nothing" }
+ end
+ end
+
+ def custom_constant_handling
+ respond_to do |type|
+ type.html { render body: "HTML" }
+ type.mobile { render body: "Mobile" }
+ end
+ end
+
+ def custom_constant_handling_without_block
+ respond_to do |type|
+ type.html { render body: "HTML" }
+ type.mobile
+ end
+ end
+
+ def handle_any
+ respond_to do |type|
+ type.html { render body: "HTML" }
+ type.any(:js, :xml) { render body: "Either JS or XML" }
+ end
+ end
+
+ def handle_any_any
+ respond_to do |type|
+ type.html { render body: "HTML" }
+ type.any { render body: "Whatever you ask for, I got it" }
+ end
+ end
+
+ def all_types_with_layout
+ respond_to do |type|
+ type.html
+ end
+ end
+
+ def json_with_callback
+ respond_to do |type|
+ type.json { render json: "JS", callback: "alert" }
+ end
+ end
+
+ def iphone_with_html_response_type
+ request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone"
+
+ respond_to do |type|
+ type.html { @type = "Firefox" }
+ type.iphone { @type = "iPhone" }
+ end
+ end
+
+ def iphone_with_html_response_type_without_layout
+ request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
+
+ respond_to do |type|
+ type.html { @type = "Firefox"; render action: "iphone_with_html_response_type" }
+ type.iphone { @type = "iPhone" ; render action: "iphone_with_html_response_type" }
+ end
+ end
+
+ def variant_with_implicit_template_rendering
+ # This has exactly one variant template defined in the file system (+mobile.html.erb),
+ # which raises the regular MissingTemplate error for other variants.
+ end
+
+ def variant_without_implicit_template_rendering
+ # This differs from the above in that it does not have any templates defined in the file
+ # system, which triggers the ImplicitRender (204 No Content) behavior.
+ end
+
+ def variant_with_format_and_custom_render
+ request.variant = :mobile
+
+ respond_to do |type|
+ type.html { render body: "mobile" }
+ end
+ end
+
+ def multiple_variants_for_format
+ respond_to do |type|
+ type.html do |html|
+ html.tablet { render body: "tablet" }
+ html.phone { render body: "phone" }
+ end
+ end
+ end
+
+ def variant_plus_none_for_format
+ respond_to do |format|
+ format.html do |variant|
+ variant.phone { render body: "phone" }
+ variant.none
+ end
+ end
+ end
+
+ def variant_inline_syntax
+ respond_to do |format|
+ format.js { render body: "js" }
+ format.html.none { render body: "none" }
+ format.html.phone { render body: "phone" }
+ end
+ end
+
+ def variant_inline_syntax_without_block
+ respond_to do |format|
+ format.js
+ format.html.none
+ format.html.phone
+ end
+ end
+
+ def variant_any
+ respond_to do |format|
+ format.html do |variant|
+ variant.any(:tablet, :phablet) { render body: "any" }
+ variant.phone { render body: "phone" }
+ end
+ end
+ end
+
+ def variant_any_any
+ respond_to do |format|
+ format.html do |variant|
+ variant.any { render body: "any" }
+ variant.phone { render body: "phone" }
+ end
+ end
+ end
+
+ def variant_inline_any
+ respond_to do |format|
+ format.html.any(:tablet, :phablet) { render body: "any" }
+ format.html.phone { render body: "phone" }
+ end
+ end
+
+ def variant_inline_any_any
+ respond_to do |format|
+ format.html.phone { render body: "phone" }
+ format.html.any { render body: "any" }
+ end
+ end
+
+ def variant_any_implicit_render
+ respond_to do |format|
+ format.html.phone
+ format.html.any(:tablet, :phablet)
+ end
+ end
+
+ def variant_any_with_none
+ respond_to do |format|
+ format.html.any(:none, :phone) { render body: "none or phone" }
+ end
+ end
+
+ def format_any_variant_any
+ respond_to do |format|
+ format.html { render body: "HTML" }
+ format.any(:js, :xml) do |variant|
+ variant.phone { render body: "phone" }
+ variant.any(:tablet, :phablet) { render body: "tablet" }
+ end
+ end
+ end
+
+ private
+ def set_layout
+ case action_name
+ when "all_types_with_layout", "iphone_with_html_response_type"
+ "respond_to/layouts/standard"
+ when "iphone_with_html_response_type_without_layout"
+ "respond_to/layouts/missing"
+ end
+ end
+end
+
+class RespondToControllerTest < ActionController::TestCase
+ NO_CONTENT_WARNING = "No template found for RespondToController#variant_without_implicit_template_rendering, rendering head :no_content"
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ Mime::Type.register_alias("text/html", :iphone)
+ Mime::Type.register("text/x-mobile", :mobile)
+ end
+
+ def teardown
+ super
+ Mime::Type.unregister(:iphone)
+ Mime::Type.unregister(:mobile)
+ end
+
+ def test_html
+ @request.accept = "text/html"
+ get :js_or_html
+ assert_equal "HTML", @response.body
+
+ get :html_or_xml
+ assert_equal "HTML", @response.body
+
+ assert_raises(ActionController::UnknownFormat) do
+ get :just_xml
+ end
+ end
+
+ def test_all
+ @request.accept = "*/*"
+ get :js_or_html
+ assert_equal "HTML", @response.body # js is not part of all
+
+ get :html_or_xml
+ assert_equal "HTML", @response.body
+
+ get :just_xml
+ assert_equal "XML", @response.body
+ end
+
+ def test_xml
+ @request.accept = "application/xml"
+ get :html_xml_or_rss
+ assert_equal "XML", @response.body
+ end
+
+ def test_js_or_html
+ @request.accept = "text/javascript, text/html"
+ get :js_or_html, xhr: true
+ assert_equal "JS", @response.body
+
+ @request.accept = "text/javascript, text/html"
+ get :html_or_xml, xhr: true
+ assert_equal "HTML", @response.body
+
+ @request.accept = "text/javascript, text/html"
+
+ assert_raises(ActionController::UnknownFormat) do
+ get :just_xml, xhr: true
+ end
+ end
+
+ def test_json_or_yaml_with_leading_star_star
+ @request.accept = "*/*, application/json"
+ get :json_xml_or_html
+ assert_equal "HTML", @response.body
+
+ @request.accept = "*/* , application/json"
+ get :json_xml_or_html
+ assert_equal "HTML", @response.body
+ end
+
+ def test_json_or_yaml
+ get :json_or_yaml, xhr: true
+ assert_equal "JSON", @response.body
+
+ get :json_or_yaml, format: "json"
+ assert_equal "JSON", @response.body
+
+ get :json_or_yaml, format: "yaml"
+ assert_equal "YAML", @response.body
+
+ { "YAML" => %w(text/yaml),
+ "JSON" => %w(application/json text/x-json)
+ }.each do |body, content_types|
+ content_types.each do |content_type|
+ @request.accept = content_type
+ get :json_or_yaml
+ assert_equal body, @response.body
+ end
+ end
+ end
+
+ def test_js_or_anything
+ @request.accept = "text/javascript, */*"
+ get :js_or_html, xhr: true
+ assert_equal "JS", @response.body
+
+ get :html_or_xml, xhr: true
+ assert_equal "HTML", @response.body
+
+ get :just_xml, xhr: true
+ assert_equal "XML", @response.body
+ end
+
+ def test_using_defaults
+ @request.accept = "*/*"
+ get :using_defaults
+ assert_equal "text/html", @response.content_type
+ assert_equal "Hello world!", @response.body
+
+ @request.accept = "application/xml"
+ get :using_defaults
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<p>Hello world!</p>\n", @response.body
+ end
+
+ def test_using_defaults_with_all
+ @request.accept = "*/*"
+ get :using_defaults_with_all
+ assert_equal "HTML!", @response.body.strip
+
+ @request.accept = "text/html"
+ get :using_defaults_with_all
+ assert_equal "HTML!", @response.body.strip
+
+ @request.accept = "application/json"
+ get :using_defaults_with_all
+ assert_equal "ALL", @response.body
+ end
+
+ def test_using_defaults_with_type_list
+ @request.accept = "*/*"
+ get :using_defaults_with_type_list
+ assert_equal "text/html", @response.content_type
+ assert_equal "Hello world!", @response.body
+
+ @request.accept = "application/xml"
+ get :using_defaults_with_type_list
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<p>Hello world!</p>\n", @response.body
+ end
+
+ def test_using_conflicting_nested_js_then_html
+ @request.accept = "*/*"
+ assert_raises(ActionController::RespondToMismatchError) do
+ get :using_conflicting_nested_js_then_html
+ end
+ end
+
+ def test_using_non_conflicting_nested_js_then_js
+ @request.accept = "*/*"
+ get :using_non_conflicting_nested_js_then_js
+ assert_equal "text/javascript", @response.content_type
+ assert_equal "JS", @response.body
+ end
+
+ def test_with_atom_content_type
+ @request.accept = ""
+ @request.env["CONTENT_TYPE"] = "application/atom+xml"
+ get :made_for_content_type, xhr: true
+ assert_equal "ATOM", @response.body
+ end
+
+ def test_with_rss_content_type
+ @request.accept = ""
+ @request.env["CONTENT_TYPE"] = "application/rss+xml"
+ get :made_for_content_type, xhr: true
+ assert_equal "RSS", @response.body
+ end
+
+ def test_synonyms
+ @request.accept = "application/javascript"
+ get :js_or_html
+ assert_equal "JS", @response.body
+
+ @request.accept = "application/x-xml"
+ get :html_xml_or_rss
+ assert_equal "XML", @response.body
+ end
+
+ def test_custom_types
+ @request.accept = "application/crazy-xml"
+ get :custom_type_handling
+ assert_equal "application/crazy-xml", @response.content_type
+ assert_equal "Crazy XML", @response.body
+
+ @request.accept = "text/html"
+ get :custom_type_handling
+ assert_equal "text/html", @response.content_type
+ assert_equal "HTML", @response.body
+ end
+
+ def test_xhtml_alias
+ @request.accept = "application/xhtml+xml,application/xml"
+ get :html_or_xml
+ assert_equal "HTML", @response.body
+ end
+
+ def test_firefox_simulation
+ @request.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
+ get :html_or_xml
+ assert_equal "HTML", @response.body
+ end
+
+ def test_handle_any
+ @request.accept = "*/*"
+ get :handle_any
+ assert_equal "HTML", @response.body
+
+ @request.accept = "text/javascript"
+ get :handle_any
+ assert_equal "Either JS or XML", @response.body
+
+ @request.accept = "text/xml"
+ get :handle_any
+ assert_equal "Either JS or XML", @response.body
+ end
+
+ def test_handle_any_any
+ @request.accept = "*/*"
+ get :handle_any_any
+ assert_equal "HTML", @response.body
+ end
+
+ def test_handle_any_any_parameter_format
+ get :handle_any_any, format: "html"
+ assert_equal "HTML", @response.body
+ end
+
+ def test_handle_any_any_explicit_html
+ @request.accept = "text/html"
+ get :handle_any_any
+ assert_equal "HTML", @response.body
+ end
+
+ def test_handle_any_any_javascript
+ @request.accept = "text/javascript"
+ get :handle_any_any
+ assert_equal "Whatever you ask for, I got it", @response.body
+ end
+
+ def test_handle_any_any_xml
+ @request.accept = "text/xml"
+ get :handle_any_any
+ assert_equal "Whatever you ask for, I got it", @response.body
+ end
+
+ def test_handle_any_any_unknown_format
+ get :handle_any_any, format: "php"
+ assert_equal "Whatever you ask for, I got it", @response.body
+ end
+
+ def test_browser_check_with_any_any
+ @request.accept = "application/json, application/xml"
+ get :json_xml_or_html
+ assert_equal "JSON", @response.body
+
+ @request.accept = "application/json, application/xml, */*"
+ get :json_xml_or_html
+ assert_equal "HTML", @response.body
+ end
+
+ def test_html_type_with_layout
+ @request.accept = "text/html"
+ get :all_types_with_layout
+ assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body
+ end
+
+ def test_json_with_callback_sets_javascript_content_type
+ @request.accept = "application/json"
+ get :json_with_callback
+ assert_equal "/**/alert(JS)", @response.body
+ assert_equal "text/javascript", @response.content_type
+ end
+
+ def test_xhr
+ get :js_or_html, xhr: true
+ assert_equal "JS", @response.body
+ end
+
+ def test_custom_constant
+ get :custom_constant_handling, format: "mobile"
+ assert_equal "text/x-mobile", @response.content_type
+ assert_equal "Mobile", @response.body
+ end
+
+ def test_custom_constant_handling_without_block
+ get :custom_constant_handling_without_block, format: "mobile"
+ assert_equal "text/x-mobile", @response.content_type
+ assert_equal "Mobile", @response.body
+ end
+
+ def test_forced_format
+ get :html_xml_or_rss
+ assert_equal "HTML", @response.body
+
+ get :html_xml_or_rss, format: "html"
+ assert_equal "HTML", @response.body
+
+ get :html_xml_or_rss, format: "xml"
+ assert_equal "XML", @response.body
+
+ get :html_xml_or_rss, format: "rss"
+ assert_equal "RSS", @response.body
+ end
+
+ def test_internally_forced_format
+ get :forced_xml
+ assert_equal "XML", @response.body
+
+ get :forced_xml, format: "html"
+ assert_equal "XML", @response.body
+ end
+
+ def test_extension_synonyms
+ get :html_xml_or_rss, format: "xhtml"
+ assert_equal "HTML", @response.body
+ end
+
+ def test_render_action_for_html
+ @controller.instance_eval do
+ def render(*args)
+ @action = args.first[:action] unless args.empty?
+ @action ||= action_name
+
+ response.body = "#{@action} - #{formats}"
+ end
+ end
+
+ get :using_defaults
+ assert_equal "using_defaults - #{[:html]}", @response.body
+
+ get :using_defaults, format: "xml"
+ assert_equal "using_defaults - #{[:xml]}", @response.body
+ end
+
+ def test_format_with_custom_response_type
+ get :iphone_with_html_response_type
+ assert_equal '<html><div id="html">Hello future from Firefox!</div></html>', @response.body
+
+ get :iphone_with_html_response_type, format: "iphone"
+ assert_equal "text/html", @response.content_type
+ assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
+ end
+
+ def test_format_with_custom_response_type_and_request_headers
+ @request.accept = "text/iphone"
+ get :iphone_with_html_response_type
+ assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_invalid_format
+ assert_raises(ActionController::UnknownFormat) do
+ get :using_defaults, format: "invalidformat"
+ end
+ end
+
+ def test_missing_templates
+ get :missing_templates, format: :json
+ assert_response :no_content
+ get :missing_templates, format: :xml
+ assert_response :no_content
+ end
+
+ def test_invalid_variant
+ assert_raises(ActionController::UnknownFormat) do
+ get :variant_with_implicit_template_rendering, params: { v: :invalid }
+ end
+ end
+
+ def test_variant_not_set_regular_unknown_format
+ assert_raises(ActionController::UnknownFormat) do
+ get :variant_with_implicit_template_rendering
+ end
+ end
+
+ def test_variant_with_implicit_template_rendering
+ get :variant_with_implicit_template_rendering, params: { v: :mobile }
+ assert_equal "text/html", @response.content_type
+ assert_equal "mobile", @response.body
+ end
+
+ def test_variant_without_implicit_rendering_from_browser
+ assert_raises(ActionController::MissingExactTemplate) do
+ get :variant_without_implicit_template_rendering, params: { v: :does_not_matter }
+ end
+ end
+
+ def test_variant_variant_not_set_and_without_implicit_rendering_from_browser
+ assert_raises(ActionController::MissingExactTemplate) do
+ get :variant_without_implicit_template_rendering
+ end
+ end
+
+ def test_variant_without_implicit_rendering_from_xhr
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ old_logger, ActionController::Base.logger = ActionController::Base.logger, logger
+
+ get :variant_without_implicit_template_rendering, xhr: true, params: { v: :does_not_matter }
+ assert_response :no_content
+
+ assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged"
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+
+ def test_variant_without_implicit_rendering_from_api
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ old_logger, ActionController::Base.logger = ActionController::Base.logger, logger
+
+ get :variant_without_implicit_template_rendering, format: "json", params: { v: :does_not_matter }
+ assert_response :no_content
+
+ assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged"
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+
+ def test_variant_variant_not_set_and_without_implicit_rendering_from_xhr
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ old_logger, ActionController::Base.logger = ActionController::Base.logger, logger
+
+ get :variant_without_implicit_template_rendering, xhr: true
+ assert_response :no_content
+
+ assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged"
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+
+ def test_variant_with_format_and_custom_render
+ get :variant_with_format_and_custom_render, params: { v: :phone }
+ assert_equal "text/html", @response.content_type
+ assert_equal "mobile", @response.body
+ end
+
+ def test_multiple_variants_for_format
+ get :multiple_variants_for_format, params: { v: :tablet }
+ assert_equal "text/html", @response.content_type
+ assert_equal "tablet", @response.body
+ end
+
+ def test_no_variant_in_variant_setup
+ get :variant_plus_none_for_format
+ assert_equal "text/html", @response.content_type
+ assert_equal "none", @response.body
+ end
+
+ def test_variant_inline_syntax
+ get :variant_inline_syntax
+ assert_equal "text/html", @response.content_type
+ assert_equal "none", @response.body
+
+ get :variant_inline_syntax, params: { v: :phone }
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_inline_syntax_with_format
+ get :variant_inline_syntax, format: :js
+ assert_equal "text/javascript", @response.content_type
+ assert_equal "js", @response.body
+ end
+
+ def test_variant_inline_syntax_without_block
+ get :variant_inline_syntax_without_block, params: { v: :phone }
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_any
+ get :variant_any, params: { v: :phone }
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ get :variant_any, params: { v: :tablet }
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+
+ get :variant_any, params: { v: :phablet }
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_any_any
+ get :variant_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+
+ get :variant_any_any, params: { v: :phone }
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ get :variant_any_any, params: { v: :yolo }
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_inline_any
+ get :variant_any, params: { v: :phone }
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ get :variant_inline_any, params: { v: :tablet }
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+
+ get :variant_inline_any, params: { v: :phablet }
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_inline_any_any
+ get :variant_inline_any_any, params: { v: :phone }
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ get :variant_inline_any_any, params: { v: :yolo }
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_any_implicit_render
+ get :variant_any_implicit_render, params: { v: :tablet }
+ assert_equal "text/html", @response.content_type
+ assert_equal "tablet", @response.body
+
+ get :variant_any_implicit_render, params: { v: :phablet }
+ assert_equal "text/html", @response.content_type
+ assert_equal "phablet", @response.body
+ end
+
+ def test_variant_any_with_none
+ get :variant_any_with_none
+ assert_equal "text/html", @response.content_type
+ assert_equal "none or phone", @response.body
+
+ get :variant_any_with_none, params: { v: :phone }
+ assert_equal "text/html", @response.content_type
+ assert_equal "none or phone", @response.body
+ end
+
+ def test_format_any_variant_any
+ get :format_any_variant_any, format: :js, params: { v: :tablet }
+ assert_equal "text/javascript", @response.content_type
+ assert_equal "tablet", @response.body
+ end
+
+ def test_variant_negotiation_inline_syntax
+ get :variant_inline_syntax_without_block, params: { v: [:tablet, :phone] }
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_negotiation_block_syntax
+ get :variant_plus_none_for_format, params: { v: [:tablet, :phone] }
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_negotiation_without_block
+ get :variant_inline_syntax_without_block, params: { v: [:tablet, :phone] }
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+end
diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb
new file mode 100644
index 0000000000..7572d514fb
--- /dev/null
+++ b/actionpack/test/controller/new_base/bare_metal_test.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module BareMetalTest
+ class BareController < ActionController::Metal
+ def index
+ self.response_body = "Hello world"
+ end
+ end
+
+ class BareTest < ActiveSupport::TestCase
+ test "response body is a Rack-compatible response" do
+ status, headers, body = BareController.action(:index).call(Rack::MockRequest.env_for("/"))
+ assert_equal 200, status
+ string = +""
+
+ body.each do |part|
+ assert part.is_a?(String), "Each part of the body must be a String"
+ string << part
+ end
+
+ assert_kind_of Hash, headers, "Headers must be a Hash"
+ assert headers["Content-Type"], "Content-Type must exist"
+
+ assert_equal "Hello world", string
+ end
+
+ test "response_body value is wrapped in an array when the value is a String" do
+ controller = BareController.new
+ controller.set_request!(ActionDispatch::Request.empty)
+ controller.set_response!(BareController.make_response!(controller.request))
+ controller.index
+ assert_equal ["Hello world"], controller.response_body
+ end
+
+ test "connect a request to controller instance without dispatch" do
+ env = {}
+ controller = BareController.new
+ controller.set_request! ActionDispatch::Request.new(env)
+ assert controller.request
+ end
+ end
+
+ class BareEmptyController < ActionController::Metal
+ def index
+ self.response_body = nil
+ end
+ end
+
+ class BareEmptyTest < ActiveSupport::TestCase
+ test "response body is nil" do
+ controller = BareEmptyController.new
+ controller.set_request!(ActionDispatch::Request.empty)
+ controller.set_response!(BareController.make_response!(controller.request))
+ controller.index
+ assert_nil controller.response_body
+ end
+ end
+
+ class HeadController < ActionController::Metal
+ include ActionController::Head
+
+ def index
+ head :not_found
+ end
+
+ def continue
+ self.content_type = "text/html"
+ head 100
+ end
+
+ def switching_protocols
+ self.content_type = "text/html"
+ head 101
+ end
+
+ def processing
+ self.content_type = "text/html"
+ head 102
+ end
+
+ def no_content
+ self.content_type = "text/html"
+ head 204
+ end
+
+ def reset_content
+ self.content_type = "text/html"
+ head 205
+ end
+
+ def not_modified
+ self.content_type = "text/html"
+ head 304
+ end
+ end
+
+ class HeadTest < ActiveSupport::TestCase
+ test "head works on its own" do
+ status = HeadController.action(:index).call(Rack::MockRequest.env_for("/")).first
+ assert_equal 404, status
+ end
+
+ test "head :continue (100) does not return a content-type header" do
+ headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
+ end
+
+ test "head :switching_protocols (101) does not return a content-type header" do
+ headers = HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")).second
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
+ end
+
+ test "head :processing (102) does not return a content-type header" do
+ headers = HeadController.action(:processing).call(Rack::MockRequest.env_for("/")).second
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
+ end
+
+ test "head :no_content (204) does not return a content-type header" do
+ headers = HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")).second
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
+ end
+
+ test "head :reset_content (205) does not return a content-type header" do
+ headers = HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/")).second
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
+ end
+
+ test "head :not_modified (304) does not return a content-type header" do
+ headers = HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/")).second
+ assert_nil headers["Content-Type"]
+ assert_nil headers["Content-Length"]
+ end
+
+ test "head :no_content (204) does not return any content" do
+ content = body(HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")))
+ assert_empty content
+ end
+
+ test "head :reset_content (205) does not return any content" do
+ content = body(HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/")))
+ assert_empty content
+ end
+
+ test "head :not_modified (304) does not return any content" do
+ content = body(HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/")))
+ assert_empty content
+ end
+
+ test "head :continue (100) does not return any content" do
+ content = body(HeadController.action(:continue).call(Rack::MockRequest.env_for("/")))
+ assert_empty content
+ end
+
+ test "head :switching_protocols (101) does not return any content" do
+ content = body(HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")))
+ assert_empty content
+ end
+
+ test "head :processing (102) does not return any content" do
+ content = body(HeadController.action(:processing).call(Rack::MockRequest.env_for("/")))
+ assert_empty content
+ end
+
+ def body(rack_response)
+ buf = []
+ rack_response[2].each { |x| buf << x }
+ buf.join
+ end
+ end
+
+ class BareControllerTest < ActionController::TestCase
+ test "GET index" do
+ get :index
+ assert_equal "Hello world", @response.body
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb
new file mode 100644
index 0000000000..280134f8d2
--- /dev/null
+++ b/actionpack/test/controller/new_base/base_test.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+# Tests the controller dispatching happy path
+module Dispatching
+ class SimpleController < ActionController::Base
+ before_action :authenticate
+
+ def index
+ render body: "success"
+ end
+
+ def modify_response_body
+ self.response_body = "success"
+ end
+
+ def modify_response_body_twice
+ ret = (self.response_body = "success")
+ self.response_body = "#{ret}!"
+ end
+
+ def modify_response_headers
+ end
+
+ def show_actions
+ render body: "actions: #{action_methods.to_a.sort.join(', ')}"
+ end
+
+ private
+ def authenticate
+ end
+ end
+
+ class EmptyController < ActionController::Base ; end
+ class SubEmptyController < EmptyController ; end
+ class NonDefaultPathController < ActionController::Base
+ def self.controller_path; "i_am_not_default"; end
+ end
+
+ module Submodule
+ class ContainedEmptyController < ActionController::Base ; end
+ class ContainedSubEmptyController < ContainedEmptyController ; end
+ class ContainedNonDefaultPathController < ActionController::Base
+ def self.controller_path; "i_am_extremely_not_default"; end
+ end
+ end
+
+ class BaseTest < Rack::TestCase
+ test "simple dispatching" do
+ get "/dispatching/simple/index"
+
+ assert_body "success"
+ assert_status 200
+ assert_content_type "text/plain; charset=utf-8"
+ end
+
+ test "directly modifying response body" do
+ get "/dispatching/simple/modify_response_body"
+
+ assert_body "success"
+ end
+
+ test "directly modifying response body twice" do
+ get "/dispatching/simple/modify_response_body_twice"
+
+ assert_body "success!"
+ end
+
+ test "controller path" do
+ assert_equal "dispatching/empty", EmptyController.controller_path
+ assert_equal EmptyController.controller_path, EmptyController.new.controller_path
+ end
+
+ test "non-default controller path" do
+ assert_equal "i_am_not_default", NonDefaultPathController.controller_path
+ assert_equal NonDefaultPathController.controller_path, NonDefaultPathController.new.controller_path
+ end
+
+ test "sub controller path" do
+ assert_equal "dispatching/sub_empty", SubEmptyController.controller_path
+ assert_equal SubEmptyController.controller_path, SubEmptyController.new.controller_path
+ end
+
+ test "namespaced controller path" do
+ assert_equal "dispatching/submodule/contained_empty", Submodule::ContainedEmptyController.controller_path
+ assert_equal Submodule::ContainedEmptyController.controller_path, Submodule::ContainedEmptyController.new.controller_path
+ end
+
+ test "namespaced non-default controller path" do
+ assert_equal "i_am_extremely_not_default", Submodule::ContainedNonDefaultPathController.controller_path
+ assert_equal Submodule::ContainedNonDefaultPathController.controller_path, Submodule::ContainedNonDefaultPathController.new.controller_path
+ end
+
+ test "namespaced sub controller path" do
+ assert_equal "dispatching/submodule/contained_sub_empty", Submodule::ContainedSubEmptyController.controller_path
+ assert_equal Submodule::ContainedSubEmptyController.controller_path, Submodule::ContainedSubEmptyController.new.controller_path
+ end
+
+ test "controller name" do
+ assert_equal "empty", EmptyController.controller_name
+ assert_equal "contained_empty", Submodule::ContainedEmptyController.controller_name
+ end
+
+ test "non-default path controller name" do
+ assert_equal "non_default_path", NonDefaultPathController.controller_name
+ assert_equal "contained_non_default_path", Submodule::ContainedNonDefaultPathController.controller_name
+ end
+
+ test "sub controller name" do
+ assert_equal "sub_empty", SubEmptyController.controller_name
+ assert_equal "contained_sub_empty", Submodule::ContainedSubEmptyController.controller_name
+ end
+
+ test "action methods" do
+ assert_equal Set.new(%w(
+ index
+ modify_response_headers
+ modify_response_body_twice
+ modify_response_body
+ show_actions
+ )), SimpleController.action_methods
+
+ assert_equal Set.new, EmptyController.action_methods
+ assert_equal Set.new, Submodule::ContainedEmptyController.action_methods
+
+ get "/dispatching/simple/show_actions"
+ assert_body "actions: index, modify_response_body, modify_response_body_twice, modify_response_headers, show_actions"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/content_negotiation_test.rb b/actionpack/test/controller/new_base/content_negotiation_test.rb
new file mode 100644
index 0000000000..7205e90176
--- /dev/null
+++ b/actionpack/test/controller/new_base/content_negotiation_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ContentNegotiation
+ # This has no layout and it works
+ class BasicController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "content_negotiation/basic/hello.html.erb" => "Hello world <%= request.formats.first.to_s %>!"
+ )]
+
+ def all
+ render plain: formats.inspect
+ end
+ end
+
+ class TestContentNegotiation < Rack::TestCase
+ test "A */* Accept header will return HTML" do
+ get "/content_negotiation/basic/hello", headers: { "HTTP_ACCEPT" => "*/*" }
+ assert_body "Hello world */*!"
+ end
+
+ test "Not all mimes are converted to symbol" do
+ get "/content_negotiation/basic/all", headers: { "HTTP_ACCEPT" => "text/plain, mime/another" }
+ assert_body '[:text, "mime/another"]'
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb
new file mode 100644
index 0000000000..d3ee4a8a6f
--- /dev/null
+++ b/actionpack/test/controller/new_base/content_type_test.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ContentType
+ class BaseController < ActionController::Base
+ def index
+ render body: "Hello world!"
+ end
+
+ def set_on_response_obj
+ response.content_type = Mime[:rss]
+ render body: "Hello world!"
+ end
+
+ def set_on_render
+ render body: "Hello world!", content_type: Mime[:rss]
+ end
+ end
+
+ class ImpliedController < ActionController::Base
+ # Template's mime type is used if no content_type is specified
+
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "content_type/implied/i_am_html_erb.html.erb" => "Hello world!",
+ "content_type/implied/i_am_xml_erb.xml.erb" => "<xml>Hello world!</xml>",
+ "content_type/implied/i_am_html_builder.html.builder" => "xml.p 'Hello'",
+ "content_type/implied/i_am_xml_builder.xml.builder" => "xml.awesome 'Hello'"
+ )]
+ end
+
+ class CharsetController < ActionController::Base
+ def set_on_response_obj
+ response.charset = "utf-16"
+ render body: "Hello world!"
+ end
+
+ def set_as_nil_on_response_obj
+ response.charset = nil
+ render body: "Hello world!"
+ end
+ end
+
+ class ExplicitContentTypeTest < Rack::TestCase
+ test "default response is text/plain and UTF8" do
+ with_routing do |set|
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller", action: "index"
+ end
+ end
+
+ get "/content_type/base"
+
+ assert_body "Hello world!"
+ assert_header "Content-Type", "text/plain; charset=utf-8"
+ end
+ end
+
+ test "setting the content type of the response directly on the response object" do
+ get "/content_type/base/set_on_response_obj"
+
+ assert_body "Hello world!"
+ assert_header "Content-Type", "application/rss+xml; charset=utf-8"
+ end
+
+ test "setting the content type of the response as an option to render" do
+ get "/content_type/base/set_on_render"
+
+ assert_body "Hello world!"
+ assert_header "Content-Type", "application/rss+xml; charset=utf-8"
+ end
+ end
+
+ class ImpliedContentTypeTest < Rack::TestCase
+ test "sets Content-Type as text/html when rendering *.html.erb" do
+ get "/content_type/implied/i_am_html_erb"
+
+ assert_header "Content-Type", "text/html; charset=utf-8"
+ end
+
+ test "sets Content-Type as application/xml when rendering *.xml.erb" do
+ get "/content_type/implied/i_am_xml_erb", params: { "format" => "xml" }
+
+ assert_header "Content-Type", "application/xml; charset=utf-8"
+ end
+
+ test "sets Content-Type as text/html when rendering *.html.builder" do
+ get "/content_type/implied/i_am_html_builder"
+
+ assert_header "Content-Type", "text/html; charset=utf-8"
+ end
+
+ test "sets Content-Type as application/xml when rendering *.xml.builder" do
+ get "/content_type/implied/i_am_xml_builder", params: { "format" => "xml" }
+
+ assert_header "Content-Type", "application/xml; charset=utf-8"
+ end
+ end
+
+ class ExplicitCharsetTest < Rack::TestCase
+ test "setting the charset of the response directly on the response object" do
+ get "/content_type/charset/set_on_response_obj"
+
+ assert_body "Hello world!"
+ assert_header "Content-Type", "text/plain; charset=utf-16"
+ end
+
+ test "setting the charset of the response as nil directly on the response object" do
+ get "/content_type/charset/set_as_nil_on_response_obj"
+
+ assert_body "Hello world!"
+ assert_header "Content-Type", "text/plain; charset=utf-8"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/middleware_test.rb b/actionpack/test/controller/new_base/middleware_test.rb
new file mode 100644
index 0000000000..df69650a7b
--- /dev/null
+++ b/actionpack/test/controller/new_base/middleware_test.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module MiddlewareTest
+ class MyMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ result = @app.call(env)
+ result[1]["Middleware-Test"] = "Success"
+ result[1]["Middleware-Order"] = "First"
+ result
+ end
+ end
+
+ class ExclaimerMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ result = @app.call(env)
+ result[1]["Middleware-Order"] += "!"
+ result
+ end
+ end
+
+ class BlockMiddleware
+ attr_accessor :configurable_message
+ def initialize(app, &block)
+ @app = app
+ yield(self) if block_given?
+ end
+
+ def call(env)
+ result = @app.call(env)
+ result[1]["Configurable-Message"] = configurable_message
+ result
+ end
+ end
+
+ class MyController < ActionController::Metal
+ use BlockMiddleware do |config|
+ config.configurable_message = "Configured by block."
+ end
+ use MyMiddleware
+ middleware.insert_before MyMiddleware, ExclaimerMiddleware
+
+ def index
+ self.response_body = "Hello World"
+ end
+ end
+
+ class InheritedController < MyController
+ end
+
+ class ActionsController < ActionController::Metal
+ use MyMiddleware, only: :show
+ middleware.insert_before MyMiddleware, ExclaimerMiddleware, except: :index
+
+ def index
+ self.response_body = "index"
+ end
+
+ def show
+ self.response_body = "show"
+ end
+ end
+
+ class TestMiddleware < ActiveSupport::TestCase
+ def setup
+ @app = MyController.action(:index)
+ end
+
+ test "middleware that is 'use'd is called as part of the Rack application" do
+ result = @app.call(env_for("/"))
+ assert_equal ["Hello World"], [].tap { |a| result[2].each { |x| a << x } }
+ assert_equal "Success", result[1]["Middleware-Test"]
+ end
+
+ test "the middleware stack is exposed as 'middleware' in the controller" do
+ result = @app.call(env_for("/"))
+ assert_equal "First!", result[1]["Middleware-Order"]
+ end
+
+ test "middleware stack accepts block arguments" do
+ result = @app.call(env_for("/"))
+ assert_equal "Configured by block.", result[1]["Configurable-Message"]
+ end
+
+ test "middleware stack accepts only and except as options" do
+ result = ActionsController.action(:show).call(env_for("/"))
+ assert_equal "First!", result[1]["Middleware-Order"]
+
+ result = ActionsController.action(:index).call(env_for("/"))
+ assert_nil result[1]["Middleware-Order"]
+ end
+
+ def env_for(url)
+ Rack::MockRequest.env_for(url)
+ end
+ end
+
+ class TestInheritedMiddleware < TestMiddleware
+ def setup
+ @app = InheritedController.action(:index)
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_action_test.rb b/actionpack/test/controller/new_base/render_action_test.rb
new file mode 100644
index 0000000000..33b55dc5a8
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_action_test.rb
@@ -0,0 +1,314 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module RenderAction
+ # This has no layout and it works
+ class BasicController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render_action/basic/hello_world.html.erb" => "Hello world!"
+ )]
+
+ def hello_world
+ render action: "hello_world"
+ end
+
+ def hello_world_as_string
+ render "hello_world"
+ end
+
+ def hello_world_as_string_with_options
+ render "hello_world", status: 404
+ end
+
+ def hello_world_as_symbol
+ render :hello_world
+ end
+
+ def hello_world_with_symbol
+ render action: :hello_world
+ end
+
+ def hello_world_with_layout
+ render action: "hello_world", layout: true
+ end
+
+ def hello_world_with_layout_false
+ render action: "hello_world", layout: false
+ end
+
+ def hello_world_with_layout_nil
+ render action: "hello_world", layout: nil
+ end
+
+ def hello_world_with_custom_layout
+ render action: "hello_world", layout: "greetings"
+ end
+ end
+
+ class RenderActionTest < Rack::TestCase
+ test "rendering an action using :action => <String>" do
+ get "/render_action/basic/hello_world"
+
+ assert_body "Hello world!"
+ assert_status 200
+ end
+
+ test "rendering an action using '<action>'" do
+ get "/render_action/basic/hello_world_as_string"
+
+ assert_body "Hello world!"
+ assert_status 200
+ end
+
+ test "rendering an action using '<action>' and options" do
+ get "/render_action/basic/hello_world_as_string_with_options"
+
+ assert_body "Hello world!"
+ assert_status 404
+ end
+
+ test "rendering an action using :action" do
+ get "/render_action/basic/hello_world_as_symbol"
+
+ assert_body "Hello world!"
+ assert_status 200
+ end
+
+ test "rendering an action using :action => :hello_world" do
+ get "/render_action/basic/hello_world_with_symbol"
+
+ assert_body "Hello world!"
+ assert_status 200
+ end
+ end
+
+ class RenderLayoutTest < Rack::TestCase
+ def setup
+ end
+
+ test "rendering with layout => true" do
+ assert_raise(ArgumentError) do
+ get "/render_action/basic/hello_world_with_layout", headers: { "action_dispatch.show_exceptions" => false }
+ end
+ end
+
+ test "rendering with layout => false" do
+ get "/render_action/basic/hello_world_with_layout_false"
+
+ assert_body "Hello world!"
+ assert_status 200
+ end
+
+ test "rendering with layout => :nil" do
+ get "/render_action/basic/hello_world_with_layout_nil"
+
+ assert_body "Hello world!"
+ assert_status 200
+ end
+
+ test "rendering with layout => 'greetings'" do
+ assert_raise(ActionView::MissingTemplate) do
+ get "/render_action/basic/hello_world_with_custom_layout", headers: { "action_dispatch.show_exceptions" => false }
+ end
+ end
+ end
+end
+
+module RenderActionWithApplicationLayout
+ # # ==== Render actions with layouts ====
+ class BasicController < ::ApplicationController
+ # Set the view path to an application view structure with layouts
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render_action_with_application_layout/basic/hello_world.html.erb" => "Hello World!",
+ "render_action_with_application_layout/basic/hello.html.builder" => "xml.p 'Hello'",
+ "layouts/application.html.erb" => "Hi <%= yield %> OK, Bye",
+ "layouts/greetings.html.erb" => "Greetings <%= yield %> Bye",
+ "layouts/builder.html.builder" => "xml.html do\n xml << yield\nend"
+ )]
+
+ def hello_world
+ render action: "hello_world"
+ end
+
+ def hello_world_with_layout
+ render action: "hello_world", layout: true
+ end
+
+ def hello_world_with_layout_false
+ render action: "hello_world", layout: false
+ end
+
+ def hello_world_with_layout_nil
+ render action: "hello_world", layout: nil
+ end
+
+ def hello_world_with_custom_layout
+ render action: "hello_world", layout: "greetings"
+ end
+
+ def with_builder_and_layout
+ render action: "hello", layout: "builder"
+ end
+ end
+
+ class LayoutTest < Rack::TestCase
+ test "rendering implicit application.html.erb as layout" do
+ get "/render_action_with_application_layout/basic/hello_world"
+
+ assert_body "Hi Hello World! OK, Bye"
+ assert_status 200
+ end
+
+ test "rendering with layout => true" do
+ get "/render_action_with_application_layout/basic/hello_world_with_layout"
+
+ assert_body "Hi Hello World! OK, Bye"
+ assert_status 200
+ end
+
+ test "rendering with layout => false" do
+ get "/render_action_with_application_layout/basic/hello_world_with_layout_false"
+
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering with layout => :nil" do
+ get "/render_action_with_application_layout/basic/hello_world_with_layout_nil"
+
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering with layout => 'greetings'" do
+ get "/render_action_with_application_layout/basic/hello_world_with_custom_layout"
+
+ assert_body "Greetings Hello World! Bye"
+ assert_status 200
+ end
+ end
+
+ class TestLayout < Rack::TestCase
+ testing BasicController
+
+ test "builder works with layouts" do
+ get :with_builder_and_layout
+ assert_response "<html>\n<p>Hello</p>\n</html>\n"
+ end
+ end
+end
+
+module RenderActionWithControllerLayout
+ class BasicController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render_action_with_controller_layout/basic/hello_world.html.erb" => "Hello World!",
+ "layouts/render_action_with_controller_layout/basic.html.erb" => "With Controller Layout! <%= yield %> Bye"
+ )]
+
+ def hello_world
+ render action: "hello_world"
+ end
+
+ def hello_world_with_layout
+ render action: "hello_world", layout: true
+ end
+
+ def hello_world_with_layout_false
+ render action: "hello_world", layout: false
+ end
+
+ def hello_world_with_layout_nil
+ render action: "hello_world", layout: nil
+ end
+
+ def hello_world_with_custom_layout
+ render action: "hello_world", layout: "greetings"
+ end
+ end
+
+ class ControllerLayoutTest < Rack::TestCase
+ test "render hello_world and implicitly use <controller_path>.html.erb as a layout." do
+ get "/render_action_with_controller_layout/basic/hello_world"
+
+ assert_body "With Controller Layout! Hello World! Bye"
+ assert_status 200
+ end
+
+ test "rendering with layout => true" do
+ get "/render_action_with_controller_layout/basic/hello_world_with_layout"
+
+ assert_body "With Controller Layout! Hello World! Bye"
+ assert_status 200
+ end
+
+ test "rendering with layout => false" do
+ get "/render_action_with_controller_layout/basic/hello_world_with_layout_false"
+
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering with layout => :nil" do
+ get "/render_action_with_controller_layout/basic/hello_world_with_layout_nil"
+
+ assert_body "Hello World!"
+ assert_status 200
+ end
+ end
+end
+
+module RenderActionWithBothLayouts
+ class BasicController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render_action_with_both_layouts/basic/hello_world.html.erb" => "Hello World!",
+ "layouts/application.html.erb" => "Oh Hi <%= yield %> Bye",
+ "layouts/render_action_with_both_layouts/basic.html.erb" => "With Controller Layout! <%= yield %> Bye")]
+
+ def hello_world
+ render action: "hello_world"
+ end
+
+ def hello_world_with_layout
+ render action: "hello_world", layout: true
+ end
+
+ def hello_world_with_layout_false
+ render action: "hello_world", layout: false
+ end
+
+ def hello_world_with_layout_nil
+ render action: "hello_world", layout: nil
+ end
+ end
+
+ class ControllerLayoutTest < Rack::TestCase
+ test "rendering implicitly use <controller_path>.html.erb over application.html.erb as a layout" do
+ get "/render_action_with_both_layouts/basic/hello_world"
+
+ assert_body "With Controller Layout! Hello World! Bye"
+ assert_status 200
+ end
+
+ test "rendering with layout => true" do
+ get "/render_action_with_both_layouts/basic/hello_world_with_layout"
+
+ assert_body "With Controller Layout! Hello World! Bye"
+ assert_status 200
+ end
+
+ test "rendering with layout => false" do
+ get "/render_action_with_both_layouts/basic/hello_world_with_layout_false"
+
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering with layout => :nil" do
+ get "/render_action_with_both_layouts/basic/hello_world_with_layout_nil"
+
+ assert_body "Hello World!"
+ assert_status 200
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb
new file mode 100644
index 0000000000..d0b61f0665
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_body_test.rb
@@ -0,0 +1,172 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module RenderBody
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render body: "Hello World!"
+ end
+ end
+
+ class SimpleController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new]
+
+ def index
+ render body: "hello david"
+ end
+ end
+
+ class WithLayoutController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.erb" => "<%= yield %>, I'm here!",
+ "layouts/greetings.erb" => "<%= yield %>, I wish thee well.",
+ "layouts/ivar.erb" => "<%= yield %>, <%= @ivar %>"
+ )]
+
+ def index
+ render body: "hello david"
+ end
+
+ def custom_code
+ render body: "hello world", status: 404
+ end
+
+ def with_custom_code_as_string
+ render body: "hello world", status: "404 Not Found"
+ end
+
+ def with_nil
+ render body: nil
+ end
+
+ def with_nil_and_status
+ render body: nil, status: 403
+ end
+
+ def with_false
+ render body: false
+ end
+
+ def with_layout_true
+ render body: "hello world", layout: true
+ end
+
+ def with_layout_false
+ render body: "hello world", layout: false
+ end
+
+ def with_layout_nil
+ render body: "hello world", layout: nil
+ end
+
+ def with_custom_layout
+ render body: "hello world", layout: "greetings"
+ end
+
+ def with_custom_content_type
+ response.headers["Content-Type"] = "application/json"
+ render body: '["troll","face"]'
+ end
+
+ def with_ivar_in_layout
+ @ivar = "hello world"
+ render body: "hello world", layout: "ivar"
+ end
+ end
+
+ class RenderBodyTest < Rack::TestCase
+ test "rendering body from a minimal controller" do
+ get "/render_body/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering body from an action with default options renders the body with the layout" do
+ with_routing do |set|
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
+
+ get "/render_body/simple"
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering body from an action with default options renders the body without the layout" do
+ with_routing do |set|
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
+
+ get "/render_body/with_layout"
+
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering body, while also providing a custom status code" do
+ get "/render_body/with_layout/custom_code"
+
+ assert_body "hello world"
+ assert_status 404
+ end
+
+ test "rendering body with nil returns an empty body" do
+ get "/render_body/with_layout/with_nil"
+
+ assert_body ""
+ assert_status 200
+ end
+
+ test "Rendering body with nil and custom status code returns an empty body and the status" do
+ get "/render_body/with_layout/with_nil_and_status"
+
+ assert_body ""
+ assert_status 403
+ end
+
+ test "rendering body with false returns the string 'false'" do
+ get "/render_body/with_layout/with_false"
+
+ assert_body "false"
+ assert_status 200
+ end
+
+ test "rendering body with layout: true" do
+ get "/render_body/with_layout/with_layout_true"
+
+ assert_body "hello world, I'm here!"
+ assert_status 200
+ end
+
+ test "rendering body with layout: 'greetings'" do
+ get "/render_body/with_layout/with_custom_layout"
+
+ assert_body "hello world, I wish thee well."
+ assert_status 200
+ end
+
+ test "specified content type should not be removed" do
+ get "/render_body/with_layout/with_custom_content_type"
+
+ assert_equal %w{ troll face }, JSON.parse(response.body)
+ assert_equal "application/json", response.headers["Content-Type"]
+ end
+
+ test "rendering body with layout: false" do
+ get "/render_body/with_layout/with_layout_false"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering body with layout: nil" do
+ get "/render_body/with_layout/with_layout_nil"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_context_test.rb b/actionpack/test/controller/new_base/render_context_test.rb
new file mode 100644
index 0000000000..5e570a1d79
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_context_test.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+# This is testing the decoupling of view renderer and view context
+# by allowing the controller to be used as view context. This is
+# similar to the way sinatra renders templates.
+module RenderContext
+ class BasicController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render_context/basic/hello_world.html.erb" => "<%= @value %> from <%= self.__controller_method__ %>",
+ "layouts/basic.html.erb" => "?<%= yield %>?"
+ )]
+
+ # 1) Include ActionView::Context to bring the required dependencies
+ include ActionView::Context
+
+ # 2) Call _prepare_context that will do the required initialization
+ before_action :_prepare_context
+
+ def hello_world
+ @value = "Hello"
+ render action: "hello_world", layout: false
+ end
+
+ def with_layout
+ @value = "Hello"
+ render action: "hello_world", layout: "basic"
+ end
+
+ protected def __controller_method__
+ "controller context!"
+ end
+
+ private
+ # 3) Set view_context to self
+ def view_context
+ self
+ end
+ end
+
+ class RenderContextTest < Rack::TestCase
+ test "rendering using the controller as context" do
+ get "/render_context/basic/hello_world"
+ assert_body "Hello from controller context!"
+ assert_status 200
+ end
+
+ test "rendering using the controller as context with layout" do
+ get "/render_context/basic/with_layout"
+ assert_body "?Hello from controller context!?"
+ assert_status 200
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_file_test.rb b/actionpack/test/controller/new_base/render_file_test.rb
new file mode 100644
index 0000000000..de8af029e0
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_file_test.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module RenderFile
+ class BasicController < ActionController::Base
+ self.view_paths = __dir__
+
+ def index
+ render file: File.expand_path("../../fixtures/test/hello_world", __dir__)
+ end
+
+ def with_instance_variables
+ @secret = "in the sauce"
+ render file: File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__)
+ end
+
+ def relative_path
+ @secret = "in the sauce"
+ render file: "../../fixtures/test/render_file_with_ivar"
+ end
+
+ def relative_path_with_dot
+ @secret = "in the sauce"
+ render file: "../../fixtures/test/dot.directory/render_file_with_ivar"
+ end
+
+ def pathname
+ @secret = "in the sauce"
+ render file: Pathname.new(__dir__).join(*%w[.. .. fixtures test dot.directory render_file_with_ivar])
+ end
+
+ def with_locals
+ path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__)
+ render file: path, locals: { secret: "in the sauce" }
+ end
+ end
+
+ class TestBasic < Rack::TestCase
+ testing RenderFile::BasicController
+
+ test "rendering simple template" do
+ get :index
+ assert_response "Hello world!"
+ end
+
+ test "rendering template with ivar" do
+ get :with_instance_variables
+ assert_response "The secret is in the sauce\n"
+ end
+
+ test "rendering a relative path" do
+ get :relative_path
+ assert_response "The secret is in the sauce\n"
+ end
+
+ test "rendering a relative path with dot" do
+ get :relative_path_with_dot
+ assert_response "The secret is in the sauce\n"
+ end
+
+ test "rendering a Pathname" do
+ get :pathname
+ assert_response "The secret is in the sauce\n"
+ end
+
+ test "rendering file with locals" do
+ get :with_locals
+ assert_response "The secret is in the sauce\n"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb
new file mode 100644
index 0000000000..4bea2ba2e9
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_html_test.rb
@@ -0,0 +1,192 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module RenderHtml
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render html: "Hello World!"
+ end
+ end
+
+ class SimpleController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new]
+
+ def index
+ render html: "hello david"
+ end
+ end
+
+ class WithLayoutController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.html.erb" => "<%= yield %>, I'm here!",
+ "layouts/greetings.html.erb" => "<%= yield %>, I wish thee well.",
+ "layouts/ivar.html.erb" => "<%= yield %>, <%= @ivar %>"
+ )]
+
+ def index
+ render html: "hello david"
+ end
+
+ def custom_code
+ render html: "hello world", status: 404
+ end
+
+ def with_custom_code_as_string
+ render html: "hello world", status: "404 Not Found"
+ end
+
+ def with_nil
+ render html: nil
+ end
+
+ def with_nil_and_status
+ render html: nil, status: 403
+ end
+
+ def with_false
+ render html: false
+ end
+
+ def with_layout_true
+ render html: "hello world", layout: true
+ end
+
+ def with_layout_false
+ render html: "hello world", layout: false
+ end
+
+ def with_layout_nil
+ render html: "hello world", layout: nil
+ end
+
+ def with_custom_layout
+ render html: "hello world", layout: "greetings"
+ end
+
+ def with_ivar_in_layout
+ @ivar = "hello world"
+ render html: "hello world", layout: "ivar"
+ end
+
+ def with_unsafe_html_tag
+ render html: "<p>hello world</p>", layout: nil
+ end
+
+ def with_safe_html_tag
+ render html: "<p>hello world</p>".html_safe, layout: nil
+ end
+ end
+
+ class RenderHtmlTest < Rack::TestCase
+ test "rendering text from a minimal controller" do
+ get "/render_html/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering text from an action with default options renders the text with the layout" do
+ with_routing do |set|
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
+
+ get "/render_html/simple"
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text from an action with default options renders the text without the layout" do
+ with_routing do |set|
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
+
+ get "/render_html/with_layout"
+
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text, while also providing a custom status code" do
+ get "/render_html/with_layout/custom_code"
+
+ assert_body "hello world"
+ assert_status 404
+ end
+
+ test "rendering text with nil returns an empty body" do
+ get "/render_html/with_layout/with_nil"
+
+ assert_body ""
+ assert_status 200
+ end
+
+ test "Rendering text with nil and custom status code returns an empty body and the status" do
+ get "/render_html/with_layout/with_nil_and_status"
+
+ assert_body ""
+ assert_status 403
+ end
+
+ test "rendering text with false returns the string 'false'" do
+ get "/render_html/with_layout/with_false"
+
+ assert_body "false"
+ assert_status 200
+ end
+
+ test "rendering text with layout: true" do
+ get "/render_html/with_layout/with_layout_true"
+
+ assert_body "hello world, I'm here!"
+ assert_status 200
+ end
+
+ test "rendering text with layout: 'greetings'" do
+ get "/render_html/with_layout/with_custom_layout"
+
+ assert_body "hello world, I wish thee well."
+ assert_status 200
+ end
+
+ test "rendering text with layout: false" do
+ get "/render_html/with_layout/with_layout_false"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering text with layout: nil" do
+ get "/render_html/with_layout/with_layout_nil"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering html should escape the string if it is not html safe" do
+ get "/render_html/with_layout/with_unsafe_html_tag"
+
+ assert_body "&lt;p&gt;hello world&lt;/p&gt;"
+ assert_status 200
+ end
+
+ test "rendering html should not escape the string if it is html safe" do
+ get "/render_html/with_layout/with_safe_html_tag"
+
+ assert_body "<p>hello world</p>"
+ assert_status 200
+ end
+
+ test "rendering from minimal controller returns response with text/html content type" do
+ get "/render_html/minimal/index"
+ assert_content_type "text/html; charset=utf-8"
+ end
+
+ test "rendering from normal controller returns response with text/html content type" do
+ get "/render_html/simple/index"
+ assert_content_type "text/html; charset=utf-8"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_implicit_action_test.rb b/actionpack/test/controller/new_base/render_implicit_action_test.rb
new file mode 100644
index 0000000000..8c26d34b00
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_implicit_action_test.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module RenderImplicitAction
+ class SimpleController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render_implicit_action/simple/hello_world.html.erb" => "Hello world!",
+ "render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!",
+ "render_implicit_action/simple/not_implemented.html.erb" => "Not Implemented"
+ ), ActionView::FileSystemResolver.new(File.expand_path("../../controller", __dir__))]
+
+ def hello_world() end
+ end
+
+ class RenderImplicitActionTest < Rack::TestCase
+ test "render a simple action with new explicit call to render" do
+ get "/render_implicit_action/simple/hello_world"
+
+ assert_body "Hello world!"
+ assert_status 200
+ end
+
+ test "render an action with a missing method and has special characters" do
+ get "/render_implicit_action/simple/hyphen-ated"
+
+ assert_body "Hello hyphen-ated!"
+ assert_status 200
+ end
+
+ test "render an action called not_implemented" do
+ get "/render_implicit_action/simple/not_implemented"
+
+ assert_body "Not Implemented"
+ assert_status 200
+ end
+
+ test "render does not traverse the file system" do
+ assert_raises(AbstractController::ActionNotFound) do
+ action_name = %w(.. .. fixtures shared).join(File::SEPARATOR)
+ SimpleController.action(action_name).call(Rack::MockRequest.env_for("/"))
+ end
+ end
+
+ test "available_action? returns true for implicit actions" do
+ assert SimpleController.new.available_action?(:hello_world)
+ assert SimpleController.new.available_action?(:"hyphen-ated")
+ assert SimpleController.new.available_action?(:not_implemented)
+ end
+
+ test "available_action? does not allow File::SEPARATOR on the name" do
+ action_name = %w(evil .. .. path).join(File::SEPARATOR)
+ assert_equal false, SimpleController.new.available_action?(action_name.to_sym)
+
+ action_name = %w(evil path).join(File::SEPARATOR)
+ assert_equal false, SimpleController.new.available_action?(action_name.to_sym)
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_layout_test.rb b/actionpack/test/controller/new_base/render_layout_test.rb
new file mode 100644
index 0000000000..806c6206dc
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_layout_test.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ControllerLayouts
+ class ImplicitController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.html.erb" => "Main <%= yield %> Layout",
+ "layouts/override.html.erb" => "Override! <%= yield %>",
+ "basic.html.erb" => "Hello world!",
+ "controller_layouts/implicit/layout_false.html.erb" => "hi(layout_false.html.erb)"
+ )]
+
+ def index
+ render template: "basic"
+ end
+
+ def override
+ render template: "basic", layout: "override"
+ end
+
+ def layout_false
+ render layout: false
+ end
+
+ def builder_override
+ end
+ end
+
+ class ImplicitNameController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/controller_layouts/implicit_name.html.erb" => "Implicit <%= yield %> Layout",
+ "basic.html.erb" => "Hello world!"
+ )]
+
+ def index
+ render template: "basic"
+ end
+ end
+
+ class RenderLayoutTest < Rack::TestCase
+ test "rendering a normal template, but using the implicit layout" do
+ get "/controller_layouts/implicit/index"
+
+ assert_body "Main Hello world! Layout"
+ assert_status 200
+ end
+
+ test "rendering a normal template, but using an implicit NAMED layout" do
+ get "/controller_layouts/implicit_name/index"
+
+ assert_body "Implicit Hello world! Layout"
+ assert_status 200
+ end
+
+ test "overriding an implicit layout with render :layout option" do
+ get "/controller_layouts/implicit/override"
+ assert_body "Override! Hello world!"
+ end
+ end
+
+ class LayoutOptionsTest < Rack::TestCase
+ testing ControllerLayouts::ImplicitController
+
+ test "rendering with :layout => false leaves out the implicit layout" do
+ get :layout_false
+ assert_response "hi(layout_false.html.erb)"
+ end
+ end
+
+ class MismatchFormatController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.html.erb" => "<html><%= yield %></html>",
+ "controller_layouts/mismatch_format/index.xml.builder" => "xml.instruct!",
+ "controller_layouts/mismatch_format/implicit.builder" => "xml.instruct!",
+ "controller_layouts/mismatch_format/explicit.js.erb" => "alert('foo');"
+ )]
+
+ def explicit
+ render layout: "application"
+ end
+ end
+
+ class MismatchFormatTest < Rack::TestCase
+ testing ControllerLayouts::MismatchFormatController
+
+ XML_INSTRUCT = %Q(<?xml version="1.0" encoding="UTF-8"?>\n)
+
+ test "if XML is selected, an HTML template is not also selected" do
+ get :index, params: { format: "xml" }
+ assert_response XML_INSTRUCT
+ end
+
+ test "if XML is implicitly selected, an HTML template is not also selected" do
+ get :implicit
+ assert_response XML_INSTRUCT
+ end
+
+ test "a layout for JS is ignored even if explicitly provided for HTML" do
+ get :explicit, params: { format: "js" }
+ assert_response "alert('foo');"
+ end
+ end
+
+ class FalseLayoutMethodController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "controller_layouts/false_layout_method/index.js.erb" => "alert('foo');"
+ )]
+
+ layout :which_layout?
+
+ def which_layout?
+ false
+ end
+
+ def index
+ end
+ end
+
+ class FalseLayoutMethodTest < Rack::TestCase
+ testing ControllerLayouts::FalseLayoutMethodController
+
+ test "access false layout returned by a method/proc" do
+ get :index, params: { format: "js" }
+ assert_response "alert('foo');"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_partial_test.rb b/actionpack/test/controller/new_base/render_partial_test.rb
new file mode 100644
index 0000000000..a0c7cbc686
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_partial_test.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module RenderPartial
+ class BasicController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render_partial/basic/_basic.html.erb" => "BasicPartial!",
+ "render_partial/basic/basic.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'basic' %><%= @test_unchanged %>",
+ "render_partial/basic/with_json.html.erb" => "<%= render :partial => 'with_json', :formats => [:json] %>",
+ "render_partial/basic/_with_json.json.erb" => "<%= render :partial => 'final', :formats => [:json] %>",
+ "render_partial/basic/_final.json.erb" => "{ final: json }",
+ "render_partial/basic/overridden.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'overridden' %><%= @test_unchanged %>",
+ "render_partial/basic/_overridden.html.erb" => "ParentPartial!",
+ "render_partial/child/_overridden.html.erb" => "OverriddenPartial!"
+ )]
+
+ def html_with_json_inside_json
+ render action: "with_json"
+ end
+
+ def changing
+ @test_unchanged = "hello"
+ render action: "basic"
+ end
+
+ def overridden
+ @test_unchanged = "hello"
+ end
+ end
+
+ class ChildController < BasicController; end
+
+ class TestPartial < Rack::TestCase
+ testing BasicController
+
+ test "rendering a partial in ActionView doesn't pull the ivars again from the controller" do
+ get :changing
+ assert_response("goodbyeBasicPartial!goodbye")
+ end
+
+ test "rendering a template with renders another partial with other format that renders other partial in the same format" do
+ get :html_with_json_inside_json
+ assert_content_type "text/html; charset=utf-8"
+ assert_response "{ final: json }"
+ end
+ end
+
+ class TestInheritedPartial < Rack::TestCase
+ testing ChildController
+
+ test "partial from parent controller gets picked if missing in child one" do
+ get :changing
+ assert_response("goodbyeBasicPartial!goodbye")
+ end
+
+ test "partial from child controller gets picked" do
+ get :overridden
+ assert_response("goodbyeOverriddenPartial!goodbye")
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb
new file mode 100644
index 0000000000..640979e4f5
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_plain_test.rb
@@ -0,0 +1,170 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module RenderPlain
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render plain: "Hello World!"
+ end
+ end
+
+ class SimpleController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new]
+
+ def index
+ render plain: "hello david"
+ end
+ end
+
+ class WithLayoutController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.text.erb" => "<%= yield %>, I'm here!",
+ "layouts/greetings.text.erb" => "<%= yield %>, I wish thee well.",
+ "layouts/ivar.text.erb" => "<%= yield %>, <%= @ivar %>"
+ )]
+
+ def index
+ render plain: "hello david"
+ end
+
+ def custom_code
+ render plain: "hello world", status: 404
+ end
+
+ def with_custom_code_as_string
+ render plain: "hello world", status: "404 Not Found"
+ end
+
+ def with_nil
+ render plain: nil
+ end
+
+ def with_nil_and_status
+ render plain: nil, status: 403
+ end
+
+ def with_false
+ render plain: false
+ end
+
+ def with_layout_true
+ render plain: "hello world", layout: true
+ end
+
+ def with_layout_false
+ render plain: "hello world", layout: false
+ end
+
+ def with_layout_nil
+ render plain: "hello world", layout: nil
+ end
+
+ def with_custom_layout
+ render plain: "hello world", layout: "greetings"
+ end
+
+ def with_ivar_in_layout
+ @ivar = "hello world"
+ render plain: "hello world", layout: "ivar"
+ end
+ end
+
+ class RenderPlainTest < Rack::TestCase
+ test "rendering text from a minimal controller" do
+ get "/render_plain/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering text from an action with default options renders the text with the layout" do
+ with_routing do |set|
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
+
+ get "/render_plain/simple"
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text from an action with default options renders the text without the layout" do
+ with_routing do |set|
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
+
+ get "/render_plain/with_layout"
+
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text, while also providing a custom status code" do
+ get "/render_plain/with_layout/custom_code"
+
+ assert_body "hello world"
+ assert_status 404
+ end
+
+ test "rendering text with nil returns an empty body" do
+ get "/render_plain/with_layout/with_nil"
+
+ assert_body ""
+ assert_status 200
+ end
+
+ test "Rendering text with nil and custom status code returns an empty body and the status" do
+ get "/render_plain/with_layout/with_nil_and_status"
+
+ assert_body ""
+ assert_status 403
+ end
+
+ test "rendering text with false returns the string 'false'" do
+ get "/render_plain/with_layout/with_false"
+
+ assert_body "false"
+ assert_status 200
+ end
+
+ test "rendering text with layout: true" do
+ get "/render_plain/with_layout/with_layout_true"
+
+ assert_body "hello world, I'm here!"
+ assert_status 200
+ end
+
+ test "rendering text with layout: 'greetings'" do
+ get "/render_plain/with_layout/with_custom_layout"
+
+ assert_body "hello world, I wish thee well."
+ assert_status 200
+ end
+
+ test "rendering text with layout: false" do
+ get "/render_plain/with_layout/with_layout_false"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering text with layout: nil" do
+ get "/render_plain/with_layout/with_layout_nil"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering from minimal controller returns response with text/plain content type" do
+ get "/render_plain/minimal/index"
+ assert_content_type "text/plain; charset=utf-8"
+ end
+
+ test "rendering from normal controller returns response with text/plain content type" do
+ get "/render_plain/simple/index"
+ assert_content_type "text/plain; charset=utf-8"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_streaming_test.rb b/actionpack/test/controller/new_base/render_streaming_test.rb
new file mode 100644
index 0000000000..23dc6bca40
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_streaming_test.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module RenderStreaming
+ class BasicController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render_streaming/basic/hello_world.html.erb" => "Hello world",
+ "render_streaming/basic/boom.html.erb" => "<%= raise 'Ruby was here!' %>",
+ "layouts/application.html.erb" => "<%= yield %>, I'm here!",
+ "layouts/boom.html.erb" => "<body class=\"<%= nil.invalid! %>\"<%= yield %></body>"
+ )]
+
+ layout "application"
+
+ def hello_world
+ render stream: true
+ end
+
+ def layout_exception
+ render action: "hello_world", stream: true, layout: "boom"
+ end
+
+ def template_exception
+ render action: "boom", stream: true
+ end
+
+ def skip
+ render action: "hello_world", stream: false
+ end
+
+ def explicit
+ render action: "hello_world", stream: true
+ end
+
+ def no_layout
+ render action: "hello_world", stream: true, layout: false
+ end
+
+ def explicit_cache
+ headers["Cache-Control"] = "private"
+ render action: "hello_world", stream: true
+ end
+ end
+
+ class StreamingTest < Rack::TestCase
+ test "rendering with streaming enabled at the class level" do
+ get "/render_streaming/basic/hello_world"
+ assert_body "b\r\nHello world\r\nb\r\n, I'm here!\r\n0\r\n\r\n"
+ assert_streaming!
+ end
+
+ test "rendering with streaming given to render" do
+ get "/render_streaming/basic/explicit"
+ assert_body "b\r\nHello world\r\nb\r\n, I'm here!\r\n0\r\n\r\n"
+ assert_streaming!
+ end
+
+ test "rendering with streaming do not override explicit cache control given to render" do
+ get "/render_streaming/basic/explicit_cache"
+ assert_body "b\r\nHello world\r\nb\r\n, I'm here!\r\n0\r\n\r\n"
+ assert_streaming! "private"
+ end
+
+ test "rendering with streaming no layout" do
+ get "/render_streaming/basic/no_layout"
+ assert_body "b\r\nHello world\r\n0\r\n\r\n"
+ assert_streaming!
+ end
+
+ test "skip rendering with streaming at render level" do
+ get "/render_streaming/basic/skip"
+ assert_body "Hello world, I'm here!"
+ end
+
+ test "rendering with layout exception" do
+ get "/render_streaming/basic/layout_exception"
+ assert_body "d\r\n<body class=\"\r\n37\r\n\"><script>window.location = \"/500.html\"</script></html>\r\n0\r\n\r\n"
+ assert_streaming!
+ end
+
+ test "rendering with template exception" do
+ get "/render_streaming/basic/template_exception"
+ assert_body "37\r\n\"><script>window.location = \"/500.html\"</script></html>\r\n0\r\n\r\n"
+ assert_streaming!
+ end
+
+ test "rendering with template exception logs the exception" do
+ io = StringIO.new
+ _old, ActionView::Base.logger = ActionView::Base.logger, ActiveSupport::Logger.new(io)
+
+ begin
+ get "/render_streaming/basic/template_exception"
+ io.rewind
+ assert_match "Ruby was here!", io.read
+ ensure
+ ActionView::Base.logger = _old
+ end
+ end
+
+ test "do not stream on HTTP/1.0" do
+ get "/render_streaming/basic/hello_world", headers: { "HTTP_VERSION" => "HTTP/1.0" }
+ assert_body "Hello world, I'm here!"
+ assert_status 200
+ assert_equal "22", headers["Content-Length"]
+ assert_nil headers["Transfer-Encoding"]
+ end
+
+ def assert_streaming!(cache = "no-cache")
+ assert_status 200
+ assert_nil headers["Content-Length"]
+ assert_equal "chunked", headers["Transfer-Encoding"]
+ assert_equal cache, headers["Cache-Control"]
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb
new file mode 100644
index 0000000000..14dc958475
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_template_test.rb
@@ -0,0 +1,240 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module RenderTemplate
+ class WithoutLayoutController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "test/basic.html.erb" => "Hello from basic.html.erb",
+ "shared.html.erb" => "Elastica",
+ "locals.html.erb" => "The secret is <%= secret %>",
+ "xml_template.xml.builder" => "xml.html do\n xml.p 'Hello'\nend",
+ "with_raw.html.erb" => "Hello <%=raw '<strong>this is raw</strong>' %>",
+ "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in an html template",
+ "with_implicit_raw.text.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a text template",
+ "test/with_json.html.erb" => "<%= render :template => 'test/with_json', :formats => [:json] %>",
+ "test/with_json.json.erb" => "<%= render :template => 'test/final', :formats => [:json] %>",
+ "test/final.json.erb" => "{ final: json }",
+ "test/with_error.html.erb" => "<%= raise 'i do not exist' %>"
+ )]
+
+ def index
+ render template: "test/basic"
+ end
+
+ def html_with_json_inside_json
+ render template: "test/with_json"
+ end
+
+ def index_without_key
+ render "test/basic"
+ end
+
+ def in_top_directory
+ render template: "shared"
+ end
+
+ def in_top_directory_with_slash
+ render template: "/shared"
+ end
+
+ def in_top_directory_with_slash_without_key
+ render "/shared"
+ end
+
+ def with_locals
+ render template: "locals", locals: { secret: "area51" }
+ end
+
+ def with_locals_without_key
+ render "locals", locals: { secret: "area51" }
+ end
+
+ def builder_template
+ render template: "xml_template"
+ end
+
+ def with_raw
+ render template: "with_raw"
+ end
+
+ def with_implicit_raw
+ render template: "with_implicit_raw"
+ end
+
+ def with_error
+ render template: "test/with_error"
+ end
+
+ private
+
+ def show_detailed_exceptions?
+ request.local?
+ end
+ end
+
+ class TestWithoutLayout < Rack::TestCase
+ testing RenderTemplate::WithoutLayoutController
+
+ test "rendering a normal template with full path without layout" do
+ get :index
+ assert_response "Hello from basic.html.erb"
+ end
+
+ test "rendering a normal template with full path without layout without key" do
+ get :index_without_key
+ assert_response "Hello from basic.html.erb"
+ end
+
+ test "rendering a template not in a subdirectory" do
+ get :in_top_directory
+ assert_response "Elastica"
+ end
+
+ test "rendering a template not in a subdirectory with a leading slash" do
+ get :in_top_directory_with_slash
+ assert_response "Elastica"
+ end
+
+ test "rendering a template not in a subdirectory with a leading slash without key" do
+ get :in_top_directory_with_slash_without_key
+ assert_response "Elastica"
+ end
+
+ test "rendering a template with local variables" do
+ get :with_locals
+ assert_response "The secret is area51"
+ end
+
+ test "rendering a template with local variables without key" do
+ get :with_locals
+ assert_response "The secret is area51"
+ end
+
+ test "rendering a builder template" do
+ get :builder_template, params: { "format" => "xml" }
+ assert_response "<html>\n <p>Hello</p>\n</html>\n"
+ end
+
+ test "rendering a template with <%=raw stuff %>" do
+ get :with_raw
+
+ assert_body "Hello <strong>this is raw</strong>"
+ assert_status 200
+
+ get :with_implicit_raw
+
+ assert_body "Hello <strong>this is also raw</strong> in an html template"
+ assert_status 200
+
+ get :with_implicit_raw, params: { format: "text" }
+
+ assert_body "Hello <strong>this is also raw</strong> in a text template"
+ assert_status 200
+ end
+
+ test "rendering a template with renders another template with other format that renders other template in the same format" do
+ get :html_with_json_inside_json
+ assert_content_type "text/html; charset=utf-8"
+ assert_response "{ final: json }"
+ end
+
+ test "rendering a template with error properly excerts the code" do
+ get :with_error
+ assert_status 500
+ assert_match "i do not exist", response.body
+ end
+ end
+
+ class WithLayoutController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "test/basic.html.erb" => "Hello from basic.html.erb",
+ "shared.html.erb" => "Elastica",
+ "layouts/application.html.erb" => "<%= yield %>, I'm here!",
+ "layouts/greetings.html.erb" => "<%= yield %>, I wish thee well."
+ )]
+
+ def index
+ render template: "test/basic"
+ end
+
+ def with_layout
+ render template: "test/basic", layout: true
+ end
+
+ def with_layout_false
+ render template: "test/basic", layout: false
+ end
+
+ def with_layout_nil
+ render template: "test/basic", layout: nil
+ end
+
+ def with_custom_layout
+ render template: "test/basic", layout: "greetings"
+ end
+ end
+
+ class TestWithLayout < Rack::TestCase
+ test "rendering with implicit layout" do
+ with_routing do |set|
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: :index } }
+
+ get "/render_template/with_layout"
+
+ assert_body "Hello from basic.html.erb, I'm here!"
+ assert_status 200
+ end
+ end
+
+ test "rendering with layout => true" do
+ get "/render_template/with_layout/with_layout"
+
+ assert_body "Hello from basic.html.erb, I'm here!"
+ assert_status 200
+ end
+
+ test "rendering with layout => false" do
+ get "/render_template/with_layout/with_layout_false"
+
+ assert_body "Hello from basic.html.erb"
+ assert_status 200
+ end
+
+ test "rendering with layout => nil" do
+ get "/render_template/with_layout/with_layout_nil"
+
+ assert_body "Hello from basic.html.erb"
+ assert_status 200
+ end
+
+ test "rendering layout => 'greetings'" do
+ get "/render_template/with_layout/with_custom_layout"
+
+ assert_body "Hello from basic.html.erb, I wish thee well."
+ assert_status 200
+ end
+ end
+
+ module Compatibility
+ class WithoutLayoutController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "test/basic.html.erb" => "Hello from basic.html.erb",
+ "shared.html.erb" => "Elastica"
+ )]
+
+ def with_forward_slash
+ render template: "/test/basic"
+ end
+ end
+
+ class TestTemplateRenderWithForwardSlash < Rack::TestCase
+ test "rendering a normal template with full path starting with a leading slash" do
+ get "/render_template/compatibility/without_layout/with_forward_slash"
+
+ assert_body "Hello from basic.html.erb"
+ assert_status 200
+ end
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_test.rb b/actionpack/test/controller/new_base/render_test.rb
new file mode 100644
index 0000000000..eb29203f59
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_test.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module Render
+ class BlankRenderController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render/blank_render/index.html.erb" => "Hello world!",
+ "render/blank_render/access_request.html.erb" => "The request: <%= request.method.to_s.upcase %>",
+ "render/blank_render/access_action_name.html.erb" => "Action Name: <%= action_name %>",
+ "render/blank_render/access_controller_name.html.erb" => "Controller Name: <%= controller_name %>",
+ "render/blank_render/overridden_with_own_view_paths_appended.html.erb" => "parent content",
+ "render/blank_render/overridden_with_own_view_paths_prepended.html.erb" => "parent content",
+ "render/blank_render/overridden.html.erb" => "parent content",
+ "render/child_render/overridden.html.erb" => "child content"
+ )]
+
+ def index
+ render
+ end
+
+ def access_request
+ render action: "access_request"
+ end
+
+ def render_action_name
+ render action: "access_action_name"
+ end
+
+ def overridden_with_own_view_paths_appended
+ end
+
+ def overridden_with_own_view_paths_prepended
+ end
+
+ def overridden
+ end
+
+ private
+
+ def secretz
+ render plain: "FAIL WHALE!"
+ end
+ end
+
+ class DoubleRenderController < ActionController::Base
+ def index
+ render plain: "hello"
+ render plain: "world"
+ end
+ end
+
+ class ChildRenderController < BlankRenderController
+ append_view_path ActionView::FixtureResolver.new("render/child_render/overridden_with_own_view_paths_appended.html.erb" => "child content")
+ prepend_view_path ActionView::FixtureResolver.new("render/child_render/overridden_with_own_view_paths_prepended.html.erb" => "child content")
+ end
+
+ class RenderTest < Rack::TestCase
+ test "render with blank" do
+ with_routing do |set|
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller", action: "index"
+ end
+ end
+
+ get "/render/blank_render"
+
+ assert_body "Hello world!"
+ assert_status 200
+ end
+ end
+
+ test "rendering more than once raises an exception" do
+ with_routing do |set|
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller", action: "index"
+ end
+ end
+
+ assert_raises(AbstractController::DoubleRenderError) do
+ get "/render/double_render", headers: { "action_dispatch.show_exceptions" => false }
+ end
+ end
+ end
+ end
+
+ class TestOnlyRenderPublicActions < Rack::TestCase
+ # Only public methods on actual controllers are callable actions
+ test "raises an exception when a method of Object is called" do
+ assert_raises(AbstractController::ActionNotFound) do
+ get "/render/blank_render/clone", headers: { "action_dispatch.show_exceptions" => false }
+ end
+ end
+
+ test "raises an exception when a private method is called" do
+ assert_raises(AbstractController::ActionNotFound) do
+ get "/render/blank_render/secretz", headers: { "action_dispatch.show_exceptions" => false }
+ end
+ end
+ end
+
+ class TestVariousObjectsAvailableInView < Rack::TestCase
+ test "The request object is accessible in the view" do
+ get "/render/blank_render/access_request"
+ assert_body "The request: GET"
+ end
+
+ test "The action_name is accessible in the view" do
+ get "/render/blank_render/render_action_name"
+ assert_body "Action Name: render_action_name"
+ end
+
+ test "The controller_name is accessible in the view" do
+ get "/render/blank_render/access_controller_name"
+ assert_body "Controller Name: blank_render"
+ end
+ end
+
+ class TestViewInheritance < Rack::TestCase
+ test "Template from child controller gets picked over parent one" do
+ get "/render/child_render/overridden"
+ assert_body "child content"
+ end
+
+ test "Template from child controller with custom view_paths prepended gets picked over parent one" do
+ get "/render/child_render/overridden_with_own_view_paths_prepended"
+ assert_body "child content"
+ end
+
+ test "Template from child controller with custom view_paths appended gets picked over parent one" do
+ get "/render/child_render/overridden_with_own_view_paths_appended"
+ assert_body "child content"
+ end
+
+ test "Template from parent controller gets picked if missing in child controller" do
+ get "/render/child_render/index"
+ assert_body "Hello world!"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_xml_test.rb b/actionpack/test/controller/new_base/render_xml_test.rb
new file mode 100644
index 0000000000..0dc16d64e2
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_xml_test.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module RenderXml
+ # This has no layout and it works
+ class BasicController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "render_xml/basic/with_render_erb" => "Hello world!"
+ )]
+ end
+end
diff --git a/actionpack/test/controller/output_escaping_test.rb b/actionpack/test/controller/output_escaping_test.rb
new file mode 100644
index 0000000000..d683bc73e6
--- /dev/null
+++ b/actionpack/test/controller/output_escaping_test.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class OutputEscapingTest < ActiveSupport::TestCase
+ test "escape_html shouldn't die when passed nil" do
+ assert_predicate ERB::Util.h(nil), :blank?
+ end
+
+ test "escapeHTML should escape strings" do
+ assert_equal "&lt;&gt;&quot;", ERB::Util.h("<>\"")
+ end
+
+ test "escapeHTML shouldn't touch explicitly safe strings" do
+ assert_equal "<", ERB::Util.h("<".html_safe)
+ end
+end
diff --git a/actionpack/test/controller/parameter_encoding_test.rb b/actionpack/test/controller/parameter_encoding_test.rb
new file mode 100644
index 0000000000..e2194e8974
--- /dev/null
+++ b/actionpack/test/controller/parameter_encoding_test.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ParameterEncodingController < ActionController::Base
+ skip_parameter_encoding :test_bar
+ skip_parameter_encoding :test_all_values_encoding
+
+ def test_foo
+ render body: params[:foo].encoding
+ end
+
+ def test_bar
+ render body: params[:bar].encoding
+ end
+
+ def test_all_values_encoding
+ render body: ::JSON.dump(params.values.map(&:encoding).map(&:name))
+ end
+end
+
+class ParameterEncodingTest < ActionController::TestCase
+ tests ParameterEncodingController
+
+ test "properly transcodes UTF8 parameters into declared encodings" do
+ post :test_foo, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" }
+
+ assert_response :success
+ assert_equal "UTF-8", @response.body
+ end
+
+ test "properly encodes ASCII_8BIT parameters into binary" do
+ post :test_bar, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" }
+
+ assert_response :success
+ assert_equal "ASCII-8BIT", @response.body
+ end
+
+ test "properly encodes all ASCII_8BIT parameters into binary" do
+ post :test_all_values_encoding, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" }
+
+ assert_response :success
+ assert_equal ["ASCII-8BIT"], JSON.parse(@response.body).uniq
+ end
+
+ test "does not raise an error when passed a param declared as ASCII-8BIT that contains invalid bytes" do
+ get :test_bar, params: { "bar" => URI.parser.escape("bar\xE2baz".b) }
+
+ assert_response :success
+ assert_equal "ASCII-8BIT", @response.body
+ end
+end
diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb
new file mode 100644
index 0000000000..7789e654d5
--- /dev/null
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -0,0 +1,338 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+
+class ParametersAccessorsTest < ActiveSupport::TestCase
+ setup do
+ ActionController::Parameters.permit_all_parameters = false
+
+ @params = ActionController::Parameters.new(
+ person: {
+ age: "32",
+ name: {
+ first: "David",
+ last: "Heinemeier Hansson"
+ },
+ addresses: [{ city: "Chicago", state: "Illinois" }]
+ }
+ )
+ end
+
+ test "[] retains permitted status" do
+ @params.permit!
+ assert_predicate @params[:person], :permitted?
+ assert_predicate @params[:person][:name], :permitted?
+ end
+
+ test "[] retains unpermitted status" do
+ assert_not_predicate @params[:person], :permitted?
+ assert_not_predicate @params[:person][:name], :permitted?
+ end
+
+ test "as_json returns the JSON representation of the parameters hash" do
+ assert_not @params.as_json.key? "parameters"
+ assert_not @params.as_json.key? "permitted"
+ assert @params.as_json.key? "person"
+ end
+
+ test "to_s returns the string representation of the parameters hash" do
+ assert_equal '{"person"=>{"age"=>"32", "name"=>{"first"=>"David", "last"=>"Heinemeier Hansson"}, ' \
+ '"addresses"=>[{"city"=>"Chicago", "state"=>"Illinois"}]}}', @params.to_s
+ end
+
+ test "each carries permitted status" do
+ @params.permit!
+ @params.each { |key, value| assert(value.permitted?) if key == "person" }
+ end
+
+ test "each carries unpermitted status" do
+ @params.each { |key, value| assert_not(value.permitted?) if key == "person" }
+ end
+
+ test "each returns key,value array for block with arity 1" do
+ @params.each do |arg|
+ assert_kind_of Array, arg
+ assert_equal "person", arg[0]
+ assert_kind_of ActionController::Parameters, arg[1]
+ end
+ end
+
+ test "each_pair carries permitted status" do
+ @params.permit!
+ @params.each_pair { |key, value| assert(value.permitted?) if key == "person" }
+ end
+
+ test "each_pair carries unpermitted status" do
+ @params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" }
+ end
+
+ test "each_pair returns key,value array for block with arity 1" do
+ @params.each_pair do |arg|
+ assert_kind_of Array, arg
+ assert_equal "person", arg[0]
+ assert_kind_of ActionController::Parameters, arg[1]
+ end
+ end
+
+ test "each_value carries permitted status" do
+ @params.permit!
+ @params.each_value do |value|
+ assert_predicate(value, :permitted?)
+ end
+ end
+
+ test "each_value carries unpermitted status" do
+ @params.each_value do |value|
+ assert_not_predicate(value, :permitted?)
+ end
+ end
+
+ test "each_key converts to hash for permitted" do
+ @params.permit!
+ @params.each_key { |key| assert_kind_of(String, key) if key == "person" }
+ end
+
+ test "each_key converts to hash for unpermitted" do
+ @params.each_key { |key| assert_kind_of(String, key) if key == "person" }
+ end
+
+ test "empty? returns true when params contains no key/value pairs" do
+ params = ActionController::Parameters.new
+ assert_empty params
+ end
+
+ test "empty? returns false when any params are present" do
+ assert_not_empty @params
+ end
+
+ test "except retains permitted status" do
+ @params.permit!
+ assert_predicate @params.except(:person), :permitted?
+ assert_predicate @params[:person].except(:name), :permitted?
+ end
+
+ test "except retains unpermitted status" do
+ assert_not_predicate @params.except(:person), :permitted?
+ assert_not_predicate @params[:person].except(:name), :permitted?
+ end
+
+ test "fetch retains permitted status" do
+ @params.permit!
+ assert_predicate @params.fetch(:person), :permitted?
+ assert_predicate @params[:person].fetch(:name), :permitted?
+ end
+
+ test "fetch retains unpermitted status" do
+ assert_not_predicate @params.fetch(:person), :permitted?
+ assert_not_predicate @params[:person].fetch(:name), :permitted?
+ end
+
+ test "has_key? returns true if the given key is present in the params" do
+ assert @params.has_key?(:person)
+ end
+
+ test "has_key? returns false if the given key is not present in the params" do
+ assert_not @params.has_key?(:address)
+ end
+
+ test "has_value? returns true if the given value is present in the params" do
+ params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
+ assert params.has_value?("Chicago")
+ end
+
+ test "has_value? returns false if the given value is not present in the params" do
+ params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
+ assert_not params.has_value?("New York")
+ end
+
+ test "include? returns true if the given key is present in the params" do
+ assert @params.include?(:person)
+ end
+
+ test "include? returns false if the given key is not present in the params" do
+ assert_not @params.include?(:address)
+ end
+
+ test "key? returns true if the given key is present in the params" do
+ assert @params.key?(:person)
+ end
+
+ test "key? returns false if the given key is not present in the params" do
+ assert_not @params.key?(:address)
+ end
+
+ test "keys returns an array of the keys of the params" do
+ assert_equal ["person"], @params.keys
+ assert_equal ["age", "name", "addresses"], @params[:person].keys
+ end
+
+ test "reject retains permitted status" do
+ assert_not_predicate @params.reject { |k| k == "person" }, :permitted?
+ end
+
+ test "reject retains unpermitted status" do
+ @params.permit!
+ assert_predicate @params.reject { |k| k == "person" }, :permitted?
+ end
+
+ test "select retains permitted status" do
+ @params.permit!
+ assert_predicate @params.select { |k| k == "person" }, :permitted?
+ end
+
+ test "select retains unpermitted status" do
+ assert_not_predicate @params.select { |k| k == "person" }, :permitted?
+ end
+
+ test "slice retains permitted status" do
+ @params.permit!
+ assert_predicate @params.slice(:person), :permitted?
+ end
+
+ test "slice retains unpermitted status" do
+ assert_not_predicate @params.slice(:person), :permitted?
+ end
+
+ test "transform_keys retains permitted status" do
+ @params.permit!
+ assert_predicate @params.transform_keys { |k| k }, :permitted?
+ end
+
+ test "transform_keys retains unpermitted status" do
+ assert_not_predicate @params.transform_keys { |k| k }, :permitted?
+ end
+
+ test "transform_values retains permitted status" do
+ @params.permit!
+ assert_predicate @params.transform_values { |v| v }, :permitted?
+ end
+
+ test "transform_values retains unpermitted status" do
+ assert_not_predicate @params.transform_values { |v| v }, :permitted?
+ end
+
+ test "transform_values converts hashes to parameters" do
+ @params.transform_values do |value|
+ assert_kind_of ActionController::Parameters, value
+ value
+ end
+ end
+
+ test "transform_values without block yieds an enumerator" do
+ assert_kind_of Enumerator, @params.transform_values
+ end
+
+ test "transform_values! converts hashes to parameters" do
+ @params.transform_values! do |value|
+ assert_kind_of ActionController::Parameters, value
+ end
+ end
+
+ test "transform_values! without block yields an enumerator" do
+ assert_kind_of Enumerator, @params.transform_values!
+ end
+
+ test "value? returns true if the given value is present in the params" do
+ params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
+ assert params.value?("Chicago")
+ end
+
+ test "value? returns false if the given value is not present in the params" do
+ params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
+ assert_not params.value?("New York")
+ end
+
+ test "values returns an array of the values of the params" do
+ params = ActionController::Parameters.new(city: "Chicago", state: "Illinois")
+ assert_equal ["Chicago", "Illinois"], params.values
+ end
+
+ test "values_at retains permitted status" do
+ @params.permit!
+ assert_predicate @params.values_at(:person).first, :permitted?
+ assert_predicate @params[:person].values_at(:name).first, :permitted?
+ end
+
+ test "values_at retains unpermitted status" do
+ assert_not_predicate @params.values_at(:person).first, :permitted?
+ assert_not_predicate @params[:person].values_at(:name).first, :permitted?
+ end
+
+ test "is equal to Parameters instance with same params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2)
+ params2 = ActionController::Parameters.new(a: 1, b: 2)
+ assert(params1 == params2)
+ end
+
+ test "is equal to Parameters instance with same permitted params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ assert(params1 == params2)
+ end
+
+ test "is equal to Parameters instance with same different source params, but same permitted params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ params2 = ActionController::Parameters.new(a: 1, c: 3).permit(:a)
+ assert(params1 == params2)
+ assert(params2 == params1)
+ end
+
+ test "is not equal to an unpermitted Parameters instance with same params" do
+ params1 = ActionController::Parameters.new(a: 1).permit(:a)
+ params2 = ActionController::Parameters.new(a: 1)
+ assert(params1 != params2)
+ assert(params2 != params1)
+ end
+
+ test "is not equal to Parameters instance with different permitted params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a, :b)
+ params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ assert(params1 != params2)
+ assert(params2 != params1)
+ end
+
+ test "equality with simple types works" do
+ assert(@params != "Hello")
+ assert(@params != 42)
+ assert(@params != false)
+ end
+
+ test "inspect shows both class name, parameters and permitted flag" do
+ assert_equal(
+ '<ActionController::Parameters {"person"=>{"age"=>"32", '\
+ '"name"=>{"first"=>"David", "last"=>"Heinemeier Hansson"}, ' \
+ '"addresses"=>[{"city"=>"Chicago", "state"=>"Illinois"}]}} permitted: false>',
+ @params.inspect
+ )
+ end
+
+ test "inspect prints updated permitted flag in the output" do
+ assert_match(/permitted: false/, @params.inspect)
+
+ @params.permit!
+
+ assert_match(/permitted: true/, @params.inspect)
+ end
+
+ test "#dig delegates the dig method to its values" do
+ assert_equal "David", @params.dig(:person, :name, :first)
+ assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city)
+ end
+
+ test "#dig converts hashes to parameters" do
+ assert_kind_of ActionController::Parameters, @params.dig(:person)
+ assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0)
+ assert @params.dig(:person, :addresses).all? do |value|
+ value.is_a?(ActionController::Parameters)
+ end
+ end
+
+ test "mutating #dig return value mutates underlying parameters" do
+ @params.dig(:person, :name)[:first] = "Bill"
+ assert_equal "Bill", @params.dig(:person, :name, :first)
+
+ @params.dig(:person, :addresses)[0] = { city: "Boston", state: "Massachusetts" }
+ assert_equal "Boston", @params.dig(:person, :addresses, 0, :city)
+ end
+end
diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
new file mode 100644
index 0000000000..974612fb7b
--- /dev/null
+++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+
+class AlwaysPermittedParametersTest < ActiveSupport::TestCase
+ def setup
+ ActionController::Parameters.action_on_unpermitted_parameters = :raise
+ ActionController::Parameters.always_permitted_parameters = %w( controller action format )
+ end
+
+ def teardown
+ ActionController::Parameters.action_on_unpermitted_parameters = false
+ ActionController::Parameters.always_permitted_parameters = %w( controller action )
+ end
+
+ test "returns super on missing constant other than NEVER_UNPERMITTED_PARAMS" do
+ ActionController::Parameters.superclass.stub :const_missing, "super" do
+ assert_equal "super", ActionController::Parameters::NON_EXISTING_CONSTANT
+ end
+ end
+
+ test "allows both explicitly listed and always-permitted parameters" do
+ params = ActionController::Parameters.new(
+ book: { pages: 65 },
+ format: "json")
+ permitted = params.permit book: [:pages]
+ assert_predicate permitted, :permitted?
+ end
+end
diff --git a/actionpack/test/controller/parameters/dup_test.rb b/actionpack/test/controller/parameters/dup_test.rb
new file mode 100644
index 0000000000..5403fc6d93
--- /dev/null
+++ b/actionpack/test/controller/parameters/dup_test.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+require "active_support/core_ext/object/deep_dup"
+
+class ParametersDupTest < ActiveSupport::TestCase
+ setup do
+ ActionController::Parameters.permit_all_parameters = false
+
+ @params = ActionController::Parameters.new(
+ person: {
+ age: "32",
+ name: {
+ first: "David",
+ last: "Heinemeier Hansson"
+ },
+ addresses: [{ city: "Chicago", state: "Illinois" }]
+ }
+ )
+ end
+
+ test "a duplicate maintains the original's permitted status" do
+ @params.permit!
+ dupped_params = @params.dup
+ assert_predicate dupped_params, :permitted?
+ end
+
+ test "a duplicate maintains the original's parameters" do
+ @params.permit!
+ dupped_params = @params.dup
+ assert_equal @params.to_h, dupped_params.to_h
+ end
+
+ test "changes to a duplicate's parameters do not affect the original" do
+ dupped_params = @params.dup
+ dupped_params.delete(:person)
+ assert_not_equal @params, dupped_params
+ end
+
+ test "changes to a duplicate's permitted status do not affect the original" do
+ dupped_params = @params.dup
+ dupped_params.permit!
+ assert_not_equal @params, dupped_params
+ end
+
+ test "deep_dup content" do
+ dupped_params = @params.deep_dup
+ dupped_params[:person][:age] = "45"
+ dupped_params[:person][:addresses].clear
+
+ assert_not_equal @params[:person][:age], dupped_params[:person][:age]
+ assert_not_equal @params[:person][:addresses], dupped_params[:person][:addresses]
+ end
+
+ test "deep_dup @permitted" do
+ dupped_params = @params.deep_dup
+ dupped_params.permit!
+
+ assert_not_predicate @params, :permitted?
+ end
+
+ test "deep_dup @permitted is being copied" do
+ @params.permit!
+ assert_predicate @params.deep_dup, :permitted?
+ end
+end
diff --git a/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
new file mode 100644
index 0000000000..fc9229ca1d
--- /dev/null
+++ b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+
+class LogOnUnpermittedParamsTest < ActiveSupport::TestCase
+ def setup
+ ActionController::Parameters.action_on_unpermitted_parameters = :log
+ end
+
+ def teardown
+ ActionController::Parameters.action_on_unpermitted_parameters = false
+ end
+
+ test "logs on unexpected param" do
+ params = ActionController::Parameters.new(
+ book: { pages: 65 },
+ fishing: "Turnips")
+
+ assert_logged("Unpermitted parameter: :fishing") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ test "logs on unexpected params" do
+ params = ActionController::Parameters.new(
+ book: { pages: 65 },
+ fishing: "Turnips",
+ car: "Mersedes")
+
+ assert_logged("Unpermitted parameters: :fishing, :car") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ test "logs on unexpected nested param" do
+ params = ActionController::Parameters.new(
+ book: { pages: 65, title: "Green Cats and where to find then." })
+
+ assert_logged("Unpermitted parameter: :title") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ test "logs on unexpected nested params" do
+ params = ActionController::Parameters.new(
+ book: { pages: 65, title: "Green Cats and where to find then.", author: "G. A. Dog" })
+
+ assert_logged("Unpermitted parameters: :title, :author") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ private
+
+ def assert_logged(message)
+ old_logger = ActionController::Base.logger
+ log = StringIO.new
+ ActionController::Base.logger = Logger.new(log)
+
+ begin
+ yield
+
+ log.rewind
+ assert_match message, log.read
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+ end
+end
diff --git a/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
new file mode 100644
index 0000000000..c890839727
--- /dev/null
+++ b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+
+class MultiParameterAttributesTest < ActiveSupport::TestCase
+ test "permitted multi-parameter attribute keys" do
+ params = ActionController::Parameters.new(
+ book: {
+ "shipped_at(1i)" => "2012",
+ "shipped_at(2i)" => "3",
+ "shipped_at(3i)" => "25",
+ "shipped_at(4i)" => "10",
+ "shipped_at(5i)" => "15",
+ "published_at(1i)" => "1999",
+ "published_at(2i)" => "2",
+ "published_at(3i)" => "5",
+ "price(1)" => "R$",
+ "price(2f)" => "2.02"
+ })
+
+ permitted = params.permit book: [ :shipped_at, :price ]
+
+ assert_predicate permitted, :permitted?
+
+ assert_equal "2012", permitted[:book]["shipped_at(1i)"]
+ assert_equal "3", permitted[:book]["shipped_at(2i)"]
+ assert_equal "25", permitted[:book]["shipped_at(3i)"]
+ assert_equal "10", permitted[:book]["shipped_at(4i)"]
+ assert_equal "15", permitted[:book]["shipped_at(5i)"]
+
+ assert_equal "R$", permitted[:book]["price(1)"]
+ assert_equal "2.02", permitted[:book]["price(2f)"]
+
+ assert_nil permitted[:book]["published_at(1i)"]
+ assert_nil permitted[:book]["published_at(2i)"]
+ assert_nil permitted[:book]["published_at(3i)"]
+ end
+end
diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb
new file mode 100644
index 0000000000..312b1e5b27
--- /dev/null
+++ b/actionpack/test/controller/parameters/mutators_test.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+
+class ParametersMutatorsTest < ActiveSupport::TestCase
+ setup do
+ @params = ActionController::Parameters.new(
+ person: {
+ age: "32",
+ name: {
+ first: "David",
+ last: "Heinemeier Hansson"
+ },
+ addresses: [{ city: "Chicago", state: "Illinois" }]
+ }
+ )
+ end
+
+ test "delete retains permitted status" do
+ @params.permit!
+ assert_predicate @params.delete(:person), :permitted?
+ end
+
+ test "delete retains unpermitted status" do
+ assert_not_predicate @params.delete(:person), :permitted?
+ end
+
+ test "delete returns the value when the key is present" do
+ assert_equal "32", @params[:person].delete(:age)
+ end
+
+ test "delete removes the entry when the key present" do
+ @params[:person].delete(:age)
+ assert_not @params[:person].key?(:age)
+ end
+
+ test "delete returns nil when the key is not present" do
+ assert_nil @params[:person].delete(:first_name)
+ end
+
+ test "delete returns the value of the given block when the key is not present" do
+ assert_equal "David", @params[:person].delete(:first_name) { "David" }
+ end
+
+ test "delete yields the key to the given block when the key is not present" do
+ assert_equal "first_name: David", @params[:person].delete(:first_name) { |k| "#{k}: David" }
+ end
+
+ test "delete_if retains permitted status" do
+ @params.permit!
+ assert_predicate @params.delete_if { |k| k == "person" }, :permitted?
+ end
+
+ test "delete_if retains unpermitted status" do
+ assert_not_predicate @params.delete_if { |k| k == "person" }, :permitted?
+ end
+
+ test "extract! retains permitted status" do
+ @params.permit!
+ assert_predicate @params.extract!(:person), :permitted?
+ end
+
+ test "extract! retains unpermitted status" do
+ assert_not_predicate @params.extract!(:person), :permitted?
+ end
+
+ test "keep_if retains permitted status" do
+ @params.permit!
+ assert_predicate @params.keep_if { |k, v| k == "person" }, :permitted?
+ end
+
+ test "keep_if retains unpermitted status" do
+ assert_not_predicate @params.keep_if { |k, v| k == "person" }, :permitted?
+ end
+
+ test "reject! retains permitted status" do
+ @params.permit!
+ assert_predicate @params.reject! { |k| k == "person" }, :permitted?
+ end
+
+ test "reject! retains unpermitted status" do
+ assert_not_predicate @params.reject! { |k| k == "person" }, :permitted?
+ end
+
+ test "select! retains permitted status" do
+ @params.permit!
+ assert_predicate @params.select! { |k| k != "person" }, :permitted?
+ end
+
+ test "select! retains unpermitted status" do
+ assert_not_predicate @params.select! { |k| k != "person" }, :permitted?
+ end
+
+ test "slice! retains permitted status" do
+ @params.permit!
+ assert_predicate @params.slice!(:person), :permitted?
+ end
+
+ test "slice! retains unpermitted status" do
+ assert_not_predicate @params.slice!(:person), :permitted?
+ end
+
+ test "transform_keys! retains permitted status" do
+ @params.permit!
+ assert_predicate @params.transform_keys! { |k| k }, :permitted?
+ end
+
+ test "transform_keys! retains unpermitted status" do
+ assert_not_predicate @params.transform_keys! { |k| k }, :permitted?
+ end
+
+ test "transform_values! retains permitted status" do
+ @params.permit!
+ assert_predicate @params.transform_values! { |v| v }, :permitted?
+ end
+
+ test "transform_values! retains unpermitted status" do
+ assert_not_predicate @params.transform_values! { |v| v }, :permitted?
+ end
+end
diff --git a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb
new file mode 100644
index 0000000000..1403e224c0
--- /dev/null
+++ b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+
+class NestedParametersPermitTest < ActiveSupport::TestCase
+ def assert_filtered_out(params, key)
+ assert_not params.has_key?(key), "key #{key.inspect} has not been filtered out"
+ end
+
+ test "permitted nested parameters" do
+ params = ActionController::Parameters.new(
+ book: {
+ title: "Romeo and Juliet",
+ authors: [{
+ name: "William Shakespeare",
+ born: "1564-04-26"
+ }, {
+ name: "Christopher Marlowe"
+ }, {
+ name: %w(malicious injected names)
+ }],
+ details: {
+ pages: 200,
+ genre: "Tragedy"
+ },
+ id: {
+ isbn: "x"
+ }
+ },
+ magazine: "Mjallo!")
+
+ permitted = params.permit book: [ :title, { authors: [ :name ] }, { details: :pages }, :id ]
+
+ assert_predicate permitted, :permitted?
+ assert_equal "Romeo and Juliet", permitted[:book][:title]
+ assert_equal "William Shakespeare", permitted[:book][:authors][0][:name]
+ assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name]
+ assert_equal 200, permitted[:book][:details][:pages]
+
+ assert_filtered_out permitted, :magazine
+ assert_filtered_out permitted[:book], :id
+ assert_filtered_out permitted[:book][:details], :genre
+ assert_filtered_out permitted[:book][:authors][0], :born
+ assert_filtered_out permitted[:book][:authors][2], :name
+ end
+
+ test "permitted nested parameters with a string or a symbol as a key" do
+ params = ActionController::Parameters.new(
+ book: {
+ "authors" => [
+ { name: "William Shakespeare", born: "1564-04-26" },
+ { name: "Christopher Marlowe" }
+ ]
+ })
+
+ permitted = params.permit book: [ { "authors" => [ :name ] } ]
+
+ assert_equal "William Shakespeare", permitted[:book]["authors"][0][:name]
+ assert_equal "William Shakespeare", permitted[:book][:authors][0][:name]
+ assert_equal "Christopher Marlowe", permitted[:book]["authors"][1][:name]
+ assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name]
+
+ permitted = params.permit book: [ { authors: [ :name ] } ]
+
+ assert_equal "William Shakespeare", permitted[:book]["authors"][0][:name]
+ assert_equal "William Shakespeare", permitted[:book][:authors][0][:name]
+ assert_equal "Christopher Marlowe", permitted[:book]["authors"][1][:name]
+ assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name]
+ end
+
+ test "nested arrays with strings" do
+ params = ActionController::Parameters.new(
+ book: {
+ genres: ["Tragedy"]
+ })
+
+ permitted = params.permit book: { genres: [] }
+ assert_equal ["Tragedy"], permitted[:book][:genres]
+ end
+
+ test "permit may specify symbols or strings" do
+ params = ActionController::Parameters.new(
+ book: {
+ title: "Romeo and Juliet",
+ author: "William Shakespeare"
+ },
+ magazine: "Shakespeare Today")
+
+ permitted = params.permit({ book: ["title", :author] }, "magazine")
+ assert_equal "Romeo and Juliet", permitted[:book][:title]
+ assert_equal "William Shakespeare", permitted[:book][:author]
+ assert_equal "Shakespeare Today", permitted[:magazine]
+ end
+
+ test "nested array with strings that should be hashes" do
+ params = ActionController::Parameters.new(
+ book: {
+ genres: ["Tragedy"]
+ })
+
+ permitted = params.permit book: { genres: :type }
+ assert_empty permitted[:book][:genres]
+ end
+
+ test "nested array with strings that should be hashes and additional values" do
+ params = ActionController::Parameters.new(
+ book: {
+ title: "Romeo and Juliet",
+ genres: ["Tragedy"]
+ })
+
+ permitted = params.permit book: [ :title, { genres: :type } ]
+ assert_equal "Romeo and Juliet", permitted[:book][:title]
+ assert_empty permitted[:book][:genres]
+ end
+
+ test "nested string that should be a hash" do
+ params = ActionController::Parameters.new(
+ book: {
+ genre: "Tragedy"
+ })
+
+ permitted = params.permit book: { genre: :type }
+ assert_nil permitted[:book][:genre]
+ end
+
+ test "fields_for-style nested params" do
+ params = ActionController::Parameters.new(
+ book: {
+ authors_attributes: {
+ '0': { name: "William Shakespeare", age_of_death: "52" },
+ '1': { name: "Unattributed Assistant" },
+ '2': { name: %w(injected names) }
+ }
+ })
+ permitted = params.permit book: { authors_attributes: [ :name ] }
+
+ assert_not_nil permitted[:book][:authors_attributes]["0"]
+ assert_not_nil permitted[:book][:authors_attributes]["1"]
+ assert_empty permitted[:book][:authors_attributes]["2"]
+ assert_equal "William Shakespeare", permitted[:book][:authors_attributes]["0"][:name]
+ assert_equal "Unattributed Assistant", permitted[:book][:authors_attributes]["1"][:name]
+
+ assert_equal(
+ { "book" => { "authors_attributes" => { "0" => { "name" => "William Shakespeare" }, "1" => { "name" => "Unattributed Assistant" }, "2" => {} } } },
+ permitted.to_h
+ )
+
+ assert_filtered_out permitted[:book][:authors_attributes]["0"], :age_of_death
+ end
+
+ test "fields_for-style nested params with negative numbers" do
+ params = ActionController::Parameters.new(
+ book: {
+ authors_attributes: {
+ '-1': { name: "William Shakespeare", age_of_death: "52" },
+ '-2': { name: "Unattributed Assistant" }
+ }
+ })
+ permitted = params.permit book: { authors_attributes: [:name] }
+
+ assert_not_nil permitted[:book][:authors_attributes]["-1"]
+ assert_not_nil permitted[:book][:authors_attributes]["-2"]
+ assert_equal "William Shakespeare", permitted[:book][:authors_attributes]["-1"][:name]
+ assert_equal "Unattributed Assistant", permitted[:book][:authors_attributes]["-2"][:name]
+
+ assert_filtered_out permitted[:book][:authors_attributes]["-1"], :age_of_death
+ end
+
+ test "nested number as key" do
+ params = ActionController::Parameters.new(
+ product: {
+ properties: {
+ "0" => "prop0",
+ "1" => "prop1"
+ }
+ })
+ params = params.require(:product).permit(properties: ["0"])
+ assert_not_nil params[:properties]["0"]
+ assert_nil params[:properties]["1"]
+ assert_equal "prop0", params[:properties]["0"]
+ end
+end
diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb
new file mode 100644
index 0000000000..fbfe24059b
--- /dev/null
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -0,0 +1,510 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch/http/upload"
+require "action_controller/metal/strong_parameters"
+
+class ParametersPermitTest < ActiveSupport::TestCase
+ def assert_filtered_out(params, key)
+ assert_not params.has_key?(key), "key #{key.inspect} has not been filtered out"
+ end
+
+ setup do
+ @params = ActionController::Parameters.new(
+ person: {
+ age: "32",
+ name: {
+ first: "David",
+ last: "Heinemeier Hansson"
+ },
+ addresses: [{ city: "Chicago", state: "Illinois" }]
+ }
+ )
+
+ @struct_fields = []
+ %w(0 1 12).each do |number|
+ ["", "i", "f"].each do |suffix|
+ @struct_fields << "sf(#{number}#{suffix})"
+ end
+ end
+ end
+
+ def walk_permitted(params)
+ params.each do |k, v|
+ case v
+ when ActionController::Parameters
+ walk_permitted v
+ when Array
+ v.each { |x| walk_permitted v }
+ end
+ end
+ end
+
+ test "iteration should not impact permit" do
+ hash = { "foo" => { "bar" => { "0" => { "baz" => "hello", "zot" => "1" } } } }
+ params = ActionController::Parameters.new(hash)
+
+ walk_permitted params
+
+ sanitized = params[:foo].permit(bar: [:baz])
+ assert_equal({ "0" => { "baz" => "hello" } }, sanitized[:bar].to_unsafe_h)
+ end
+
+ test "if nothing is permitted, the hash becomes empty" do
+ params = ActionController::Parameters.new(id: "1234")
+ permitted = params.permit
+ assert_predicate permitted, :permitted?
+ assert_empty permitted
+ end
+
+ test "key: permitted scalar values" do
+ values = ["a", :a, nil]
+ values += [0, 1.0, 2**128, BigDecimal(1)]
+ values += [true, false]
+ values += [Date.today, Time.now, DateTime.now]
+ values += [STDOUT, StringIO.new, ActionDispatch::Http::UploadedFile.new(tempfile: __FILE__),
+ Rack::Test::UploadedFile.new(__FILE__)]
+
+ values.each do |value|
+ params = ActionController::Parameters.new(id: value)
+ permitted = params.permit(:id)
+ if value.nil?
+ assert_nil permitted[:id]
+ else
+ assert_equal value, permitted[:id]
+ end
+
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => value)
+ permitted = params.permit(:sf)
+ if value.nil?
+ assert_nil permitted[sf]
+ else
+ assert_equal value, permitted[sf]
+ end
+ end
+ end
+ end
+
+ test "key: unknown keys are filtered out" do
+ params = ActionController::Parameters.new(id: "1234", injected: "injected")
+ permitted = params.permit(:id)
+ assert_equal "1234", permitted[:id]
+ assert_filtered_out permitted, :injected
+ end
+
+ test "key: arrays are filtered out" do
+ [[], [1], ["1"]].each do |array|
+ params = ActionController::Parameters.new(id: array)
+ permitted = params.permit(:id)
+ assert_filtered_out permitted, :id
+
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => array)
+ permitted = params.permit(:sf)
+ assert_filtered_out permitted, sf
+ end
+ end
+ end
+
+ test "key: hashes are filtered out" do
+ [{}, { foo: 1 }, { foo: "bar" }].each do |hash|
+ params = ActionController::Parameters.new(id: hash)
+ permitted = params.permit(:id)
+ assert_filtered_out permitted, :id
+
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => hash)
+ permitted = params.permit(:sf)
+ assert_filtered_out permitted, sf
+ end
+ end
+ end
+
+ test "key: non-permitted scalar values are filtered out" do
+ params = ActionController::Parameters.new(id: Object.new)
+ permitted = params.permit(:id)
+ assert_filtered_out permitted, :id
+
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => Object.new)
+ permitted = params.permit(:sf)
+ assert_filtered_out permitted, sf
+ end
+ end
+
+ test "key: it is not assigned if not present in params" do
+ params = ActionController::Parameters.new(name: "Joe")
+ permitted = params.permit(:id)
+ assert_not permitted.has_key?(:id)
+ end
+
+ test "key to empty array: empty arrays pass" do
+ params = ActionController::Parameters.new(id: [])
+ permitted = params.permit(id: [])
+ assert_equal [], permitted[:id]
+ end
+
+ test "do not break params filtering on nil values" do
+ params = ActionController::Parameters.new(a: 1, b: [1, 2, 3], c: nil)
+
+ permitted = params.permit(:a, c: [], b: [])
+ assert_equal 1, permitted[:a]
+ assert_equal [1, 2, 3], permitted[:b]
+ assert_nil permitted[:c]
+ end
+
+ test "key to empty array: arrays of permitted scalars pass" do
+ [["foo"], [1], ["foo", "bar"], [1, 2, 3]].each do |array|
+ params = ActionController::Parameters.new(id: array)
+ permitted = params.permit(id: [])
+ assert_equal array, permitted[:id]
+ end
+ end
+
+ test "key to empty array: permitted scalar values do not pass" do
+ ["foo", 1].each do |permitted_scalar|
+ params = ActionController::Parameters.new(id: permitted_scalar)
+ permitted = params.permit(id: [])
+ assert_filtered_out permitted, :id
+ end
+ end
+
+ test "key to empty array: arrays of non-permitted scalar do not pass" do
+ [[Object.new], [[]], [[1]], [{}], [{ id: "1" }]].each do |non_permitted_scalar|
+ params = ActionController::Parameters.new(id: non_permitted_scalar)
+ permitted = params.permit(id: [])
+ assert_filtered_out permitted, :id
+ end
+ end
+
+ test "key to empty hash: arbitrary hashes are permitted" do
+ params = ActionController::Parameters.new(
+ username: "fxn",
+ preferences: {
+ scheme: "Marazul",
+ font: {
+ name: "Source Code Pro",
+ size: 12
+ },
+ tabstops: [4, 8, 12, 16],
+ suspicious: [true, Object.new, false, /yo!/],
+ dubious: [{ a: :a, b: /wtf!/ }, { c: :c }],
+ injected: Object.new
+ },
+ hacked: 1 # not a hash
+ )
+
+ permitted = params.permit(:username, preferences: {}, hacked: {})
+
+ assert_equal "fxn", permitted[:username]
+ assert_equal "Marazul", permitted[:preferences][:scheme]
+ assert_equal "Source Code Pro", permitted[:preferences][:font][:name]
+ assert_equal 12, permitted[:preferences][:font][:size]
+ assert_equal [4, 8, 12, 16], permitted[:preferences][:tabstops]
+ assert_equal [true, false], permitted[:preferences][:suspicious]
+ assert_equal :a, permitted[:preferences][:dubious][0][:a]
+ assert_equal :c, permitted[:preferences][:dubious][1][:c]
+
+ assert_filtered_out permitted[:preferences][:dubious][0], :b
+ assert_filtered_out permitted[:preferences], :injected
+ assert_filtered_out permitted, :hacked
+ end
+
+ test "fetch raises ParameterMissing exception" do
+ e = assert_raises(ActionController::ParameterMissing) do
+ @params.fetch :foo
+ end
+ assert_equal :foo, e.param
+ end
+
+ test "fetch with a default value of a hash does not mutate the object" do
+ params = ActionController::Parameters.new({})
+ params.fetch :foo, {}
+ assert_nil params[:foo]
+ end
+
+ test "hashes in array values get wrapped" do
+ params = ActionController::Parameters.new(foo: [{}, {}])
+ params[:foo].each do |hash|
+ assert_not_predicate hash, :permitted?
+ end
+ end
+
+ # Strong params has an optimization to avoid looping every time you read
+ # a key whose value is an array and building a new object. We check that
+ # optimization here.
+ test "arrays are converted at most once" do
+ params = ActionController::Parameters.new(foo: [{}])
+ assert_same params[:foo], params[:foo]
+ end
+
+ # Strong params has an internal cache to avoid duplicated loops in the most
+ # common usage pattern. See the docs of the method `converted_arrays`.
+ #
+ # This test checks that if we push a hash to an array (in-place modification)
+ # the cache does not get fooled, the hash is still wrapped as strong params,
+ # and not permitted.
+ test "mutated arrays are detected" do
+ params = ActionController::Parameters.new(users: [{ id: 1 }])
+
+ permitted = params.permit(users: [:id])
+ permitted[:users] << { injected: 1 }
+ assert_not_predicate permitted[:users].last, :permitted?
+ end
+
+ test "fetch doesnt raise ParameterMissing exception if there is a default" do
+ assert_equal "monkey", @params.fetch(:foo, "monkey")
+ assert_equal "monkey", @params.fetch(:foo) { "monkey" }
+ end
+
+ test "fetch doesnt raise ParameterMissing exception if there is a default that is nil" do
+ assert_nil @params.fetch(:foo, nil)
+ assert_nil @params.fetch(:foo) { nil }
+ end
+
+ test "KeyError in fetch block should not be covered up" do
+ params = ActionController::Parameters.new
+ e = assert_raises(KeyError) do
+ params.fetch(:missing_key) { {}.fetch(:also_missing) }
+ end
+ assert_match(/:also_missing$/, e.message)
+ end
+
+ test "not permitted is sticky beyond merges" do
+ assert_not_predicate @params.merge(a: "b"), :permitted?
+ end
+
+ test "permitted is sticky beyond merges" do
+ @params.permit!
+ assert_predicate @params.merge(a: "b"), :permitted?
+ end
+
+ test "merge with parameters" do
+ other_params = ActionController::Parameters.new(id: "1234").permit!
+ merged_params = @params.merge(other_params)
+
+ assert merged_params[:id]
+ end
+
+ test "not permitted is sticky beyond merge!" do
+ assert_not_predicate @params.merge!(a: "b"), :permitted?
+ end
+
+ test "permitted is sticky beyond merge!" do
+ @params.permit!
+ assert_predicate @params.merge!(a: "b"), :permitted?
+ end
+
+ test "merge! with parameters" do
+ other_params = ActionController::Parameters.new(id: "1234").permit!
+ @params.merge!(other_params)
+
+ assert_equal "1234", @params[:id]
+ assert_equal "32", @params[:person][:age]
+ end
+
+ test "#reverse_merge with parameters" do
+ default_params = ActionController::Parameters.new(id: "1234", person: {}).permit!
+ merged_params = @params.reverse_merge(default_params)
+
+ assert_equal "1234", merged_params[:id]
+ assert_not_predicate merged_params[:person], :empty?
+ end
+
+ test "#with_defaults is an alias of reverse_merge" do
+ default_params = ActionController::Parameters.new(id: "1234", person: {}).permit!
+ merged_params = @params.with_defaults(default_params)
+
+ assert_equal "1234", merged_params[:id]
+ assert_not_predicate merged_params[:person], :empty?
+ end
+
+ test "not permitted is sticky beyond reverse_merge" do
+ assert_not_predicate @params.reverse_merge(a: "b"), :permitted?
+ end
+
+ test "permitted is sticky beyond reverse_merge" do
+ @params.permit!
+ assert_predicate @params.reverse_merge(a: "b"), :permitted?
+ end
+
+ test "#reverse_merge! with parameters" do
+ default_params = ActionController::Parameters.new(id: "1234", person: {}).permit!
+ @params.reverse_merge!(default_params)
+
+ assert_equal "1234", @params[:id]
+ assert_not_predicate @params[:person], :empty?
+ end
+
+ test "#with_defaults! is an alias of reverse_merge!" do
+ default_params = ActionController::Parameters.new(id: "1234", person: {}).permit!
+ @params.with_defaults!(default_params)
+
+ assert_equal "1234", @params[:id]
+ assert_not_predicate @params[:person], :empty?
+ end
+
+ test "modifying the parameters" do
+ @params[:person][:hometown] = "Chicago"
+ @params[:person][:family] = { brother: "Jonas" }
+
+ assert_equal "Chicago", @params[:person][:hometown]
+ assert_equal "Jonas", @params[:person][:family][:brother]
+ end
+
+ test "permit! is recursive" do
+ @params[:nested_array] = [[{ x: 2, y: 3 }, { x: 21, y: 42 }]]
+ @params.permit!
+ assert_predicate @params, :permitted?
+ assert_predicate @params[:person], :permitted?
+ assert_predicate @params[:person][:name], :permitted?
+ assert_predicate @params[:person][:addresses][0], :permitted?
+ assert_predicate @params[:nested_array][0][0], :permitted?
+ assert_predicate @params[:nested_array][0][1], :permitted?
+ end
+
+ test "permitted takes a default value when Parameters.permit_all_parameters is set" do
+ ActionController::Parameters.permit_all_parameters = true
+ params = ActionController::Parameters.new(person: {
+ age: "32", name: { first: "David", last: "Heinemeier Hansson" }
+ })
+
+ assert_predicate params.slice(:person), :permitted?
+ assert_predicate params[:person][:name], :permitted?
+ ensure
+ ActionController::Parameters.permit_all_parameters = false
+ end
+
+ test "permitting parameters as an array" do
+ assert_equal "32", @params[:person].permit([ :age ])[:age]
+ end
+
+ test "to_h raises UnfilteredParameters on unfiltered params" do
+ assert_raises(ActionController::UnfilteredParameters) do
+ @params.to_h
+ end
+ end
+
+ test "to_h returns converted hash on permitted params" do
+ @params.permit!
+
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, @params.to_h
+ assert_not_kind_of ActionController::Parameters, @params.to_h
+ end
+
+ test "to_h returns converted hash when .permit_all_parameters is set" do
+ ActionController::Parameters.permit_all_parameters = true
+ params = ActionController::Parameters.new(crab: "Senjougahara Hitagi")
+
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, params.to_h
+ assert_not_kind_of ActionController::Parameters, params.to_h
+ assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_h)
+ ensure
+ ActionController::Parameters.permit_all_parameters = false
+ end
+
+ test "to_hash raises UnfilteredParameters on unfiltered params" do
+ assert_raises(ActionController::UnfilteredParameters) do
+ @params.to_hash
+ end
+ end
+
+ test "to_hash returns converted hash on permitted params" do
+ @params.permit!
+
+ assert_instance_of Hash, @params.to_hash
+ assert_not_kind_of ActionController::Parameters, @params.to_hash
+ end
+
+ test "parameters can be implicit converted to Hash" do
+ params = ActionController::Parameters.new
+ params.permit!
+
+ assert_equal({ a: 1 }, { a: 1 }.merge!(params))
+ end
+
+ test "to_hash returns converted hash when .permit_all_parameters is set" do
+ ActionController::Parameters.permit_all_parameters = true
+ params = ActionController::Parameters.new(crab: "Senjougahara Hitagi")
+
+ assert_instance_of Hash, params.to_hash
+ assert_not_kind_of ActionController::Parameters, params.to_hash
+ assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_hash)
+ assert_equal({ "crab" => "Senjougahara Hitagi" }, params)
+ ensure
+ ActionController::Parameters.permit_all_parameters = false
+ end
+
+ test "to_unsafe_h returns unfiltered params" do
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, @params.to_unsafe_h
+ assert_not_kind_of ActionController::Parameters, @params.to_unsafe_h
+ end
+
+ test "to_unsafe_h returns unfiltered params even after accessing few keys" do
+ params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] })
+ expected = { "f" => { "language_facet" => ["Tibetan"] } }
+
+ assert_instance_of ActionController::Parameters, params["f"]
+ assert_equal expected, params.to_unsafe_h
+ end
+
+ test "to_unsafe_h does not mutate the parameters" do
+ params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] })
+ params[:f]
+
+ params.to_unsafe_h
+
+ assert_not_predicate params, :permitted?
+ assert_not_predicate params[:f], :permitted?
+ end
+
+ test "to_h only deep dups Ruby collections" do
+ company = Class.new do
+ attr_reader :dupped
+ def dup; @dupped = true; end
+ end.new
+
+ params = ActionController::Parameters.new(prem: { likes: %i( dancing ) })
+ assert_equal({ "prem" => { "likes" => %i( dancing ) } }, params.permit!.to_h)
+
+ params = ActionController::Parameters.new(companies: [ company, :acme ])
+ assert_equal({ "companies" => [ company, :acme ] }, params.permit!.to_h)
+ assert_not company.dupped
+ end
+
+ test "to_unsafe_h only deep dups Ruby collections" do
+ company = Class.new do
+ attr_reader :dupped
+ def dup; @dupped = true; end
+ end.new
+
+ params = ActionController::Parameters.new(prem: { likes: %i( dancing ) })
+ assert_equal({ "prem" => { "likes" => %i( dancing ) } }, params.to_unsafe_h)
+
+ params = ActionController::Parameters.new(companies: [ company, :acme ])
+ assert_equal({ "companies" => [ company, :acme ] }, params.to_unsafe_h)
+ assert_not company.dupped
+ end
+
+ test "include? returns true when the key is present" do
+ assert @params.include? :person
+ assert @params.include? "person"
+ assert_not @params.include? :gorilla
+ end
+
+ test "scalar values should be filtered when array or hash is specified" do
+ params = ActionController::Parameters.new(foo: "bar")
+
+ assert params.permit(:foo).has_key?(:foo)
+ assert_not params.permit(foo: []).has_key?(:foo)
+ assert_not params.permit(foo: [:bar]).has_key?(:foo)
+ assert_not params.permit(foo: :bar).has_key?(:foo)
+ end
+
+ test "#permitted? is false by default" do
+ params = ActionController::Parameters.new
+
+ assert_equal false, params.permitted?
+ end
+end
diff --git a/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb
new file mode 100644
index 0000000000..4afd3da593
--- /dev/null
+++ b/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+
+class RaiseOnUnpermittedParamsTest < ActiveSupport::TestCase
+ def setup
+ ActionController::Parameters.action_on_unpermitted_parameters = :raise
+ end
+
+ def teardown
+ ActionController::Parameters.action_on_unpermitted_parameters = false
+ end
+
+ test "raises on unexpected params" do
+ params = ActionController::Parameters.new(
+ book: { pages: 65 },
+ fishing: "Turnips")
+
+ assert_raises(ActionController::UnpermittedParameters) do
+ params.permit(book: [:pages])
+ end
+ end
+
+ test "raises on unexpected nested params" do
+ params = ActionController::Parameters.new(
+ book: { pages: 65, title: "Green Cats and where to find then." })
+
+ assert_raises(ActionController::UnpermittedParameters) do
+ params.permit(book: [:pages])
+ end
+ end
+end
diff --git a/actionpack/test/controller/parameters/serialization_test.rb b/actionpack/test/controller/parameters/serialization_test.rb
new file mode 100644
index 0000000000..7708c8e4fe
--- /dev/null
+++ b/actionpack/test/controller/parameters/serialization_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_controller/metal/strong_parameters"
+
+class ParametersSerializationTest < ActiveSupport::TestCase
+ setup do
+ @old_permitted_parameters = ActionController::Parameters.permit_all_parameters
+ ActionController::Parameters.permit_all_parameters = false
+ end
+
+ teardown do
+ ActionController::Parameters.permit_all_parameters = @old_permitted_parameters
+ end
+
+ test "yaml serialization" do
+ params = ActionController::Parameters.new(key: :value)
+ yaml_dump = YAML.dump(params)
+ assert_match("--- !ruby/object:ActionController::Parameters", yaml_dump)
+ assert_match(/parameters: !ruby\/hash:ActiveSupport::HashWithIndifferentAccess\n\s+key: :value/, yaml_dump)
+ assert_match("permitted: false", yaml_dump)
+ end
+
+ test "yaml deserialization" do
+ params = ActionController::Parameters.new(key: :value)
+ roundtripped = YAML.load(YAML.dump(params))
+
+ assert_equal params, roundtripped
+ assert_not_predicate roundtripped, :permitted?
+ end
+
+ test "yaml backwardscompatible with psych 2.0.8 format" do
+ params = YAML.load <<~end_of_yaml
+ --- !ruby/hash:ActionController::Parameters
+ key: :value
+ end_of_yaml
+
+ assert_equal :value, params[:key]
+ assert_not_predicate params, :permitted?
+ end
+
+ test "yaml backwardscompatible with psych 2.0.9+ format" do
+ params = YAML.load(<<~end_of_yaml)
+ --- !ruby/hash-with-ivars:ActionController::Parameters
+ elements:
+ key: :value
+ ivars:
+ :@permitted: false
+ end_of_yaml
+
+ assert_equal :value, params[:key]
+ assert_not_predicate params, :permitted?
+ end
+end
diff --git a/actionpack/test/controller/params_parse_test.rb b/actionpack/test/controller/params_parse_test.rb
new file mode 100644
index 0000000000..440ab06fd7
--- /dev/null
+++ b/actionpack/test/controller/params_parse_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ParamsParseTest < ActionController::TestCase
+ class UsersController < ActionController::Base
+ def create
+ head :ok
+ end
+ end
+
+ tests UsersController
+
+ def test_parse_error_logged_once
+ log_output = capture_log_output do
+ post :create, body: "{", as: :json
+ end
+ assert_equal <<~LOG, log_output
+ Error occurred while parsing request parameters.
+ Contents:
+
+ {
+ LOG
+ end
+
+ private
+
+ def capture_log_output
+ output = StringIO.new
+ request.set_header "action_dispatch.logger", ActiveSupport::Logger.new(output)
+ yield
+ output.string
+ end
+end
diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb
new file mode 100644
index 0000000000..c4c74e8f2b
--- /dev/null
+++ b/actionpack/test/controller/params_wrapper_test.rb
@@ -0,0 +1,422 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module Admin; class User; end; end
+
+module ParamsWrapperTestHelp
+ def with_default_wrapper_options(&block)
+ @controller.class._set_wrapper_options(format: [:json])
+ @controller.class.inherited(@controller.class)
+ yield
+ end
+
+ def assert_parameters(expected)
+ assert_equal expected, self.class.controller_class.last_parameters
+ end
+end
+
+class ParamsWrapperTest < ActionController::TestCase
+ include ParamsWrapperTestHelp
+
+ class UsersController < ActionController::Base
+ class << self
+ attr_accessor :last_parameters
+ end
+
+ def parse
+ self.class.last_parameters = request.params.except(:controller, :action)
+ head :ok
+ end
+ end
+
+ class User
+ def self.attribute_names
+ []
+ end
+
+ def self.stored_attributes
+ { settings: [:color, :size] }
+ end
+ end
+
+ class Person
+ def self.attribute_names
+ []
+ end
+ end
+
+ tests UsersController
+
+ def teardown
+ UsersController.last_parameters = nil
+ end
+
+ def test_filtered_parameters
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_equal({ "controller" => "params_wrapper_test/users", "action" => "parse", "username" => "sikachu", "user" => { "username" => "sikachu" } }, @request.filtered_parameters)
+ end
+ end
+
+ def test_derived_name_from_controller
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu", "user" => { "username" => "sikachu" })
+ end
+ end
+
+ def test_store_accessors_wrapped
+ assert_called(User, :attribute_names, times: 2, returns: ["username"]) do
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "color" => "blue", "size" => "large" }
+ assert_parameters("username" => "sikachu", "color" => "blue", "size" => "large",
+ "user" => { "username" => "sikachu", "color" => "blue", "size" => "large" })
+ end
+ end
+ end
+
+ def test_specify_wrapper_name
+ with_default_wrapper_options do
+ UsersController.wrap_parameters :person
+
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu", "person" => { "username" => "sikachu" })
+ end
+ end
+
+ def test_specify_wrapper_model
+ with_default_wrapper_options do
+ UsersController.wrap_parameters Person
+
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu", "person" => { "username" => "sikachu" })
+ end
+ end
+
+ def test_specify_include_option
+ with_default_wrapper_options do
+ UsersController.wrap_parameters include: :username
+
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" })
+ end
+ end
+
+ def test_specify_exclude_option
+ with_default_wrapper_options do
+ UsersController.wrap_parameters exclude: :title
+
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" })
+ end
+ end
+
+ def test_specify_both_wrapper_name_and_include_option
+ with_default_wrapper_options do
+ UsersController.wrap_parameters :person, include: :username
+
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "person" => { "username" => "sikachu" })
+ end
+ end
+
+ def test_not_enabled_format
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/xml"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer")
+ end
+ end
+
+ def test_wrap_parameters_false
+ with_default_wrapper_options do
+ UsersController.wrap_parameters false
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer")
+ end
+ end
+
+ def test_specify_format
+ with_default_wrapper_options do
+ UsersController.wrap_parameters format: :xml
+
+ @request.env["CONTENT_TYPE"] = "application/xml"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu", "title" => "Developer" })
+ end
+ end
+
+ def test_not_wrap_reserved_parameters
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "authenticity_token" => "pwned", "_method" => "put", "utf8" => "&#9731;", "username" => "sikachu" }
+ assert_parameters("authenticity_token" => "pwned", "_method" => "put", "utf8" => "&#9731;", "username" => "sikachu", "user" => { "username" => "sikachu" })
+ end
+ end
+
+ def test_no_double_wrap_if_key_exists
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "user" => { "username" => "sikachu" } }
+ assert_parameters("user" => { "username" => "sikachu" })
+ end
+ end
+
+ def test_no_double_wrap_if_key_exists_and_value_is_nil
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "user" => nil }
+ assert_parameters("user" => nil)
+ end
+ end
+
+ def test_nested_params
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "person" => { "username" => "sikachu" } }
+ assert_parameters("person" => { "username" => "sikachu" }, "user" => { "person" => { "username" => "sikachu" } })
+ end
+ end
+
+ def test_derived_wrapped_keys_from_matching_model
+ assert_called(User, :attribute_names, times: 2, returns: ["username"]) do
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" })
+ end
+ end
+ end
+
+ def test_derived_wrapped_keys_from_specified_model
+ with_default_wrapper_options do
+ assert_called(Person, :attribute_names, times: 2, returns: ["username"]) do
+ UsersController.wrap_parameters Person
+
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "person" => { "username" => "sikachu" })
+ end
+ end
+ end
+
+ def test_not_wrapping_abstract_model
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu", "title" => "Developer" })
+ end
+ end
+
+ def test_preserves_query_string_params
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ get :parse, params: { "user" => { "username" => "nixon" } }
+ assert_parameters(
+ "user" => { "username" => "nixon" }
+ )
+ end
+ end
+
+ def test_preserves_query_string_params_in_filtered_params
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ get :parse, params: { "user" => { "username" => "nixon" } }
+ assert_equal({ "controller" => "params_wrapper_test/users", "action" => "parse", "user" => { "username" => "nixon" } }, @request.filtered_parameters)
+ end
+ end
+
+ def test_empty_parameter_set
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: {}
+ assert_parameters(
+ "user" => {}
+ )
+ end
+ end
+
+ def test_handles_empty_content_type
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = nil
+ _controller_class.dispatch(:parse, @request, @response)
+
+ assert_equal 200, @response.status
+ assert_equal "", @response.body
+ end
+ end
+
+ def test_derived_wrapped_keys_from_nested_attributes
+ def User.nested_attributes_options
+ { person: {} }
+ end
+
+ assert_called(User, :attribute_names, times: 2, returns: ["username"]) do
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "person_attributes" => { "title" => "Developer" } }
+ assert_parameters("username" => "sikachu", "person_attributes" => { "title" => "Developer" }, "user" => { "username" => "sikachu", "person_attributes" => { "title" => "Developer" } })
+ end
+ end
+ end
+end
+
+class NamespacedParamsWrapperTest < ActionController::TestCase
+ include ParamsWrapperTestHelp
+
+ module Admin
+ module Users
+ class UsersController < ActionController::Base
+ class << self
+ attr_accessor :last_parameters
+ end
+
+ def parse
+ self.class.last_parameters = request.params.except(:controller, :action)
+ head :ok
+ end
+ end
+ end
+ end
+
+ class SampleOne
+ def self.attribute_names
+ ["username"]
+ end
+ end
+
+ class SampleTwo
+ def self.attribute_names
+ ["title"]
+ end
+ end
+
+ tests Admin::Users::UsersController
+
+ def teardown
+ Admin::Users::UsersController.last_parameters = nil
+ end
+
+ def test_derived_name_from_controller
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu", "user" => { "username" => "sikachu" })
+ end
+ end
+
+ def test_namespace_lookup_from_model
+ Admin.const_set(:User, Class.new(SampleOne))
+ begin
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" })
+ end
+ ensure
+ Admin.send :remove_const, :User
+ end
+ end
+
+ def test_hierarchy_namespace_lookup_from_model
+ Object.const_set(:User, Class.new(SampleTwo))
+ begin
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "title" => "Developer" }
+ assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "title" => "Developer" })
+ end
+ ensure
+ Object.send :remove_const, :User
+ end
+ end
+end
+
+class AnonymousControllerParamsWrapperTest < ActionController::TestCase
+ include ParamsWrapperTestHelp
+
+ tests(Class.new(ActionController::Base) do
+ class << self
+ attr_accessor :last_parameters
+ end
+
+ def parse
+ self.class.last_parameters = request.params.except(:controller, :action)
+ head :ok
+ end
+ end)
+
+ def test_does_not_implicitly_wrap_params
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu")
+ end
+ end
+
+ def test_does_wrap_params_if_name_provided
+ with_default_wrapper_options do
+ @controller.class.wrap_parameters(name: "guest")
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu" }
+ assert_parameters("username" => "sikachu", "guest" => { "username" => "sikachu" })
+ end
+ end
+end
+
+class IrregularInflectionParamsWrapperTest < ActionController::TestCase
+ include ParamsWrapperTestHelp
+
+ class ParamswrappernewsItem
+ def self.attribute_names
+ ["test_attr"]
+ end
+ end
+
+ class ParamswrappernewsController < ActionController::Base
+ class << self
+ attr_accessor :last_parameters
+ end
+
+ def parse
+ self.class.last_parameters = request.params.except(:controller, :action)
+ head :ok
+ end
+ end
+
+ tests ParamswrappernewsController
+
+ def test_uses_model_attribute_names_with_irregular_inflection
+ with_dup do
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular "paramswrappernews_item", "paramswrappernews"
+ end
+
+ with_default_wrapper_options do
+ @request.env["CONTENT_TYPE"] = "application/json"
+ post :parse, params: { "username" => "sikachu", "test_attr" => "test_value" }
+ assert_parameters("username" => "sikachu", "test_attr" => "test_value", "paramswrappernews_item" => { "test_attr" => "test_value" })
+ end
+ end
+ end
+
+ private
+
+ def with_dup
+ original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en]
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup)
+ yield
+ ensure
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original)
+ end
+end
diff --git a/actionpack/test/controller/permitted_params_test.rb b/actionpack/test/controller/permitted_params_test.rb
new file mode 100644
index 0000000000..caac88ffb2
--- /dev/null
+++ b/actionpack/test/controller/permitted_params_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class PeopleController < ActionController::Base
+ def create
+ render plain: params[:person].permitted? ? "permitted" : "forbidden"
+ end
+
+ def create_with_permit
+ render plain: params[:person].permit(:name).permitted? ? "permitted" : "forbidden"
+ end
+end
+
+class ActionControllerPermittedParamsTest < ActionController::TestCase
+ tests PeopleController
+
+ test "parameters are forbidden" do
+ post :create, params: { person: { name: "Mjallo!" } }
+ assert_equal "forbidden", response.body
+ end
+
+ test "parameters can be permitted and are then not forbidden" do
+ post :create_with_permit, params: { person: { name: "Mjallo!" } }
+ assert_equal "permitted", response.body
+ end
+end
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
new file mode 100644
index 0000000000..998498e1b2
--- /dev/null
+++ b/actionpack/test/controller/redirect_test.rb
@@ -0,0 +1,402 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class Workshop
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ OUT_OF_SCOPE_BLOCK = proc do
+ raise "Not executed in controller's context" unless RedirectController === self
+ request.original_url
+ end
+
+ attr_accessor :id
+
+ def initialize(id)
+ @id = id
+ end
+
+ def persisted?
+ id.present?
+ end
+
+ def to_s
+ id.to_s
+ end
+end
+
+class RedirectController < ActionController::Base
+ # empty method not used anywhere to ensure methods like
+ # `status` and `location` aren't called on `redirect_to` calls
+ def status; raise "Should not be called!"; end
+ def location; raise "Should not be called!"; end
+
+ def simple_redirect
+ redirect_to action: "hello_world"
+ end
+
+ def redirect_with_status
+ redirect_to(action: "hello_world", status: 301)
+ end
+
+ def redirect_with_status_hash
+ redirect_to({ action: "hello_world" }, { status: 301 })
+ end
+
+ def redirect_with_protocol
+ redirect_to action: "hello_world", protocol: "https"
+ end
+
+ def url_redirect_with_status
+ redirect_to("http://www.example.com", status: :moved_permanently)
+ end
+
+ def url_redirect_with_status_hash
+ redirect_to("http://www.example.com", status: 301)
+ end
+
+ def relative_url_redirect_with_status
+ redirect_to("/things/stuff", status: :found)
+ end
+
+ def relative_url_redirect_with_status_hash
+ redirect_to("/things/stuff", status: 301)
+ end
+
+ def redirect_back_with_status
+ redirect_back(fallback_location: "/things/stuff", status: 307)
+ end
+
+ def safe_redirect_back_with_status
+ redirect_back(fallback_location: "/things/stuff", status: 307, allow_other_host: false)
+ end
+
+ def host_redirect
+ redirect_to action: "other_host", only_path: false, host: "other.test.host"
+ end
+
+ def module_redirect
+ redirect_to controller: "module_test/module_redirect", action: "hello_world"
+ end
+
+ def redirect_to_url
+ redirect_to "http://www.rubyonrails.org/"
+ end
+
+ def redirect_to_url_with_unescaped_query_string
+ redirect_to "http://example.com/query?status=new"
+ end
+
+ def redirect_to_url_with_complex_scheme
+ redirect_to "x-test+scheme.complex:redirect"
+ end
+
+ def redirect_to_url_with_network_path_reference
+ redirect_to "//www.rubyonrails.org/"
+ end
+
+ def redirect_to_existing_record
+ redirect_to Workshop.new(5)
+ end
+
+ def redirect_to_new_record
+ redirect_to Workshop.new(nil)
+ end
+
+ def redirect_to_nil
+ redirect_to nil
+ end
+
+ def redirect_to_params
+ redirect_to ActionController::Parameters.new(status: 200, protocol: "javascript", f: "%0Aeval(name)")
+ end
+
+ def redirect_to_with_block
+ redirect_to proc { "http://www.rubyonrails.org/" }
+ end
+
+ def redirect_to_with_block_and_assigns
+ @url = "http://www.rubyonrails.org/"
+ redirect_to proc { @url }
+ end
+
+ def redirect_to_with_block_and_options
+ redirect_to proc { { action: "hello_world" } }
+ end
+
+ def redirect_to_out_of_scope_block
+ redirect_to Workshop::OUT_OF_SCOPE_BLOCK
+ end
+
+ def redirect_with_header_break
+ redirect_to "/lol\r\nwat"
+ end
+
+ def redirect_with_null_bytes
+ redirect_to "\000/lol\r\nwat"
+ end
+
+ def rescue_errors(e) raise e end
+
+ private
+ def dashbord_url(id, message)
+ url_for action: "dashboard", params: { "id" => id, "message" => message }
+ end
+end
+
+class RedirectTest < ActionController::TestCase
+ tests RedirectController
+
+ def test_simple_redirect
+ get :simple_redirect
+ assert_response :redirect
+ assert_equal "http://test.host/redirect/hello_world", redirect_to_url
+ end
+
+ def test_redirect_with_header_break
+ get :redirect_with_header_break
+ assert_response :redirect
+ assert_equal "http://test.host/lolwat", redirect_to_url
+ end
+
+ def test_redirect_with_null_bytes
+ get :redirect_with_null_bytes
+ assert_response :redirect
+ assert_equal "http://test.host/lolwat", redirect_to_url
+ end
+
+ def test_redirect_with_no_status
+ get :simple_redirect
+ assert_response 302
+ assert_equal "http://test.host/redirect/hello_world", redirect_to_url
+ end
+
+ def test_redirect_with_status
+ get :redirect_with_status
+ assert_response 301
+ assert_equal "http://test.host/redirect/hello_world", redirect_to_url
+ end
+
+ def test_redirect_with_status_hash
+ get :redirect_with_status_hash
+ assert_response 301
+ assert_equal "http://test.host/redirect/hello_world", redirect_to_url
+ end
+
+ def test_redirect_with_protocol
+ get :redirect_with_protocol
+ assert_response 302
+ assert_equal "https://test.host/redirect/hello_world", redirect_to_url
+ end
+
+ def test_url_redirect_with_status
+ get :url_redirect_with_status
+ assert_response 301
+ assert_equal "http://www.example.com", redirect_to_url
+ end
+
+ def test_url_redirect_with_status_hash
+ get :url_redirect_with_status_hash
+ assert_response 301
+ assert_equal "http://www.example.com", redirect_to_url
+ end
+
+ def test_relative_url_redirect_with_status
+ get :relative_url_redirect_with_status
+ assert_response 302
+ assert_equal "http://test.host/things/stuff", redirect_to_url
+ end
+
+ def test_relative_url_redirect_with_status_hash
+ get :relative_url_redirect_with_status_hash
+ assert_response 301
+ assert_equal "http://test.host/things/stuff", redirect_to_url
+ end
+
+ def test_relative_url_redirect_host_with_port
+ request.host = "test.host:1234"
+ get :relative_url_redirect_with_status
+ assert_response 302
+ assert_equal "http://test.host:1234/things/stuff", redirect_to_url
+ end
+
+ def test_simple_redirect_using_options
+ get :host_redirect
+ assert_response :redirect
+ assert_redirected_to action: "other_host", only_path: false, host: "other.test.host"
+ end
+
+ def test_module_redirect
+ get :module_redirect
+ assert_response :redirect
+ assert_redirected_to "http://test.host/module_test/module_redirect/hello_world"
+ end
+
+ def test_module_redirect_using_options
+ get :module_redirect
+ assert_response :redirect
+ assert_redirected_to controller: "module_test/module_redirect", action: "hello_world"
+ end
+
+ def test_redirect_to_url
+ get :redirect_to_url
+ assert_response :redirect
+ assert_redirected_to "http://www.rubyonrails.org/"
+ end
+
+ def test_redirect_to_url_with_unescaped_query_string
+ get :redirect_to_url_with_unescaped_query_string
+ assert_response :redirect
+ assert_redirected_to "http://example.com/query?status=new"
+ end
+
+ def test_redirect_to_url_with_complex_scheme
+ get :redirect_to_url_with_complex_scheme
+ assert_response :redirect
+ assert_equal "x-test+scheme.complex:redirect", redirect_to_url
+ end
+
+ def test_redirect_to_url_with_network_path_reference
+ get :redirect_to_url_with_network_path_reference
+ assert_response :redirect
+ assert_equal "//www.rubyonrails.org/", redirect_to_url
+ end
+
+ def test_redirect_back
+ referer = "http://www.example.com/coming/from"
+ @request.env["HTTP_REFERER"] = referer
+
+ get :redirect_back_with_status
+
+ assert_response 307
+ assert_equal referer, redirect_to_url
+ end
+
+ def test_redirect_back_with_no_referer
+ get :redirect_back_with_status
+
+ assert_response 307
+ assert_equal "http://test.host/things/stuff", redirect_to_url
+ end
+
+ def test_safe_redirect_back_from_other_host
+ @request.env["HTTP_REFERER"] = "http://another.host/coming/from"
+ get :safe_redirect_back_with_status
+
+ assert_response 307
+ assert_equal "http://test.host/things/stuff", redirect_to_url
+ end
+
+ def test_safe_redirect_back_from_the_same_host
+ referer = "http://test.host/coming/from"
+ @request.env["HTTP_REFERER"] = referer
+ get :safe_redirect_back_with_status
+
+ assert_response 307
+ assert_equal referer, redirect_to_url
+ end
+
+ def test_redirect_to_record
+ with_routing do |set|
+ set.draw do
+ resources :workshops
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+
+ get :redirect_to_existing_record
+ assert_equal "http://test.host/workshops/5", redirect_to_url
+ assert_redirected_to Workshop.new(5)
+
+ get :redirect_to_new_record
+ assert_equal "http://test.host/workshops", redirect_to_url
+ assert_redirected_to Workshop.new(nil)
+ end
+ end
+
+ def test_redirect_to_nil
+ error = assert_raise(ActionController::ActionControllerError) do
+ get :redirect_to_nil
+ end
+ assert_equal "Cannot redirect to nil!", error.message
+ end
+
+ def test_redirect_to_params
+ error = assert_raise(ActionController::UnfilteredParameters) do
+ get :redirect_to_params
+ end
+ assert_equal "unable to convert unpermitted parameters to hash", error.message
+ end
+
+ def test_redirect_to_with_block
+ get :redirect_to_with_block
+ assert_response :redirect
+ assert_redirected_to "http://www.rubyonrails.org/"
+ end
+
+ def test_redirect_to_with_block_and_assigns
+ get :redirect_to_with_block_and_assigns
+ assert_response :redirect
+ assert_redirected_to "http://www.rubyonrails.org/"
+ end
+
+ def test_redirect_to_out_of_scope_block
+ get :redirect_to_out_of_scope_block
+ assert_response :redirect
+ assert_redirected_to "http://test.host/redirect/redirect_to_out_of_scope_block"
+ end
+
+ def test_redirect_to_with_block_and_accepted_options
+ with_routing do |set|
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+
+ get :redirect_to_with_block_and_options
+
+ assert_response :redirect
+ assert_redirected_to "http://test.host/redirect/hello_world"
+ end
+ end
+end
+
+module ModuleTest
+ class ModuleRedirectController < ::RedirectController
+ def module_redirect
+ redirect_to controller: "/redirect", action: "hello_world"
+ end
+ end
+
+ class ModuleRedirectTest < ActionController::TestCase
+ tests ModuleRedirectController
+
+ def test_simple_redirect
+ get :simple_redirect
+ assert_response :redirect
+ assert_equal "http://test.host/module_test/module_redirect/hello_world", redirect_to_url
+ end
+
+ def test_simple_redirect_using_options
+ get :host_redirect
+ assert_response :redirect
+ assert_redirected_to action: "other_host", only_path: false, host: "other.test.host"
+ end
+
+ def test_module_redirect
+ get :module_redirect
+ assert_response :redirect
+ assert_equal "http://test.host/redirect/hello_world", redirect_to_url
+ end
+
+ def test_module_redirect_using_options
+ get :module_redirect
+ assert_response :redirect
+ assert_redirected_to controller: "/redirect", action: "hello_world"
+ end
+ end
+end
diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb
new file mode 100644
index 0000000000..1efc0b9de1
--- /dev/null
+++ b/actionpack/test/controller/render_js_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+require "pathname"
+
+class RenderJSTest < ActionController::TestCase
+ class TestController < ActionController::Base
+ protect_from_forgery
+
+ def self.controller_path
+ "test"
+ end
+
+ def render_vanilla_js_hello
+ render js: "alert('hello')"
+ end
+
+ def show_partial
+ render partial: "partial"
+ end
+ end
+
+ tests TestController
+
+ def test_render_vanilla_js
+ get :render_vanilla_js_hello, xhr: true
+ assert_equal "alert('hello')", @response.body
+ assert_equal "text/javascript", @response.content_type
+ end
+
+ def test_should_render_js_partial
+ get :show_partial, format: "js", xhr: true
+ assert_equal "partial js", @response.body
+ end
+end
diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb
new file mode 100644
index 0000000000..82c1ba26cb
--- /dev/null
+++ b/actionpack/test/controller/render_json_test.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+require "active_support/logger"
+require "pathname"
+
+class RenderJsonTest < ActionController::TestCase
+ class JsonRenderable
+ def as_json(options = {})
+ hash = { a: :b, c: :d, e: :f }
+ hash.except!(*options[:except]) if options[:except]
+ hash
+ end
+
+ def to_json(options = {})
+ super except: [:c, :e]
+ end
+ end
+
+ class TestController < ActionController::Base
+ protect_from_forgery
+
+ def self.controller_path
+ "test"
+ end
+
+ def render_json_nil
+ render json: nil
+ end
+
+ def render_json_render_to_string
+ render plain: render_to_string(json: "[]")
+ end
+
+ def render_json_hello_world
+ render json: ActiveSupport::JSON.encode(hello: "world")
+ end
+
+ def render_json_hello_world_with_status
+ render json: ActiveSupport::JSON.encode(hello: "world"), status: 401
+ end
+
+ def render_json_hello_world_with_callback
+ render json: ActiveSupport::JSON.encode(hello: "world"), callback: "alert"
+ end
+
+ def render_json_with_custom_content_type
+ render json: ActiveSupport::JSON.encode(hello: "world"), content_type: "text/javascript"
+ end
+
+ def render_symbol_json
+ render json: ActiveSupport::JSON.encode(hello: "world")
+ end
+
+ def render_json_with_render_to_string
+ render json: { hello: render_to_string(partial: "partial") }
+ end
+
+ def render_json_with_extra_options
+ render json: JsonRenderable.new, except: [:c, :e]
+ end
+
+ def render_json_without_options
+ render json: JsonRenderable.new
+ end
+ end
+
+ tests TestController
+
+ def setup
+ # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
+ # a more accurate simulation of what happens in "real life".
+ super
+ @controller.logger = ActiveSupport::Logger.new(nil)
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_render_json_nil
+ get :render_json_nil
+ assert_equal "null", @response.body
+ assert_equal "application/json", @response.content_type
+ end
+
+ def test_render_json_render_to_string
+ get :render_json_render_to_string
+ assert_equal "[]", @response.body
+ end
+
+ def test_render_json
+ get :render_json_hello_world
+ assert_equal '{"hello":"world"}', @response.body
+ assert_equal "application/json", @response.content_type
+ end
+
+ def test_render_json_with_status
+ get :render_json_hello_world_with_status
+ assert_equal '{"hello":"world"}', @response.body
+ assert_equal 401, @response.status
+ end
+
+ def test_render_json_with_callback
+ get :render_json_hello_world_with_callback, xhr: true
+ assert_equal '/**/alert({"hello":"world"})', @response.body
+ assert_equal "text/javascript", @response.content_type
+ end
+
+ def test_render_json_with_custom_content_type
+ get :render_json_with_custom_content_type, xhr: true
+ assert_equal '{"hello":"world"}', @response.body
+ assert_equal "text/javascript", @response.content_type
+ end
+
+ def test_render_symbol_json
+ get :render_symbol_json
+ assert_equal '{"hello":"world"}', @response.body
+ assert_equal "application/json", @response.content_type
+ end
+
+ def test_render_json_with_render_to_string
+ get :render_json_with_render_to_string
+ assert_equal '{"hello":"partial html"}', @response.body
+ assert_equal "application/json", @response.content_type
+ end
+
+ def test_render_json_forwards_extra_options
+ get :render_json_with_extra_options
+ assert_equal '{"a":"b"}', @response.body
+ assert_equal "application/json", @response.content_type
+ end
+
+ def test_render_json_calls_to_json_from_object
+ get :render_json_without_options
+ assert_equal '{"a":"b"}', @response.body
+ end
+end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
new file mode 100644
index 0000000000..306b245bd1
--- /dev/null
+++ b/actionpack/test/controller/render_test.rb
@@ -0,0 +1,877 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+
+class TestControllerWithExtraEtags < ActionController::Base
+ def self.controller_name; "test"; end
+ def self.controller_path; "test"; end
+
+ etag { nil }
+ etag { "ab" }
+ etag { :cde }
+ etag { [:f] }
+ etag { nil }
+
+ def fresh
+ render plain: "stale" if stale?(etag: "123", template: false)
+ end
+
+ def array
+ render plain: "stale" if stale?(etag: %w(1 2 3), template: false)
+ end
+
+ def strong
+ render plain: "stale" if stale?(strong_etag: "strong", template: false)
+ end
+
+ def with_template
+ if stale? template: "test/hello_world"
+ render plain: "stale"
+ end
+ end
+
+ def with_implicit_template
+ fresh_when(etag: "123")
+ end
+end
+
+class ImplicitRenderTestController < ActionController::Base
+ def empty_action
+ end
+
+ def empty_action_with_template
+ end
+end
+
+module Namespaced
+ class ImplicitRenderTestController < ActionController::Base
+ def hello_world
+ fresh_when(etag: "abc")
+ end
+ end
+end
+
+class TestController < ActionController::Base
+ protect_from_forgery
+
+ before_action :set_variable_for_layout
+
+ class LabellingFormBuilder < ActionView::Helpers::FormBuilder
+ end
+
+ layout :determine_layout
+
+ def name
+ nil
+ end
+
+ private :name
+ helper_method :name
+
+ def hello_world
+ end
+
+ def conditional_hello
+ if stale?(last_modified: Time.now.utc.beginning_of_day, etag: [:foo, 123])
+ render action: "hello_world"
+ end
+ end
+
+ def conditional_hello_with_record
+ record = Struct.new(:updated_at, :cache_key).new(Time.now.utc.beginning_of_day, "foo/123")
+
+ if stale?(record)
+ render action: "hello_world"
+ end
+ end
+
+ def dynamic_render
+ render params[:id] # => String, AC::Params
+ end
+
+ def dynamic_render_permit
+ render params[:id].permit(:file)
+ end
+
+ def dynamic_render_with_file
+ # This is extremely bad, but should be possible to do.
+ file = params[:id] # => String, AC::Params
+ render file: file
+ end
+
+ class Collection
+ def initialize(records)
+ @records = records
+ end
+
+ def maximum(attribute)
+ @records.max_by(&attribute).public_send(attribute)
+ end
+ end
+
+ def conditional_hello_with_collection_of_records
+ ts = Time.now.utc.beginning_of_day
+
+ record = Struct.new(:updated_at, :cache_key).new(ts, "foo/123")
+ old_record = Struct.new(:updated_at, :cache_key).new(ts - 1.day, "bar/123")
+
+ if stale?(Collection.new([record, old_record]))
+ render action: "hello_world"
+ end
+ end
+
+ def conditional_hello_with_expires_in
+ expires_in 60.1.seconds
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_in_with_public
+ expires_in 1.minute, public: true
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_in_with_must_revalidate
+ expires_in 1.minute, must_revalidate: true
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_in_with_public_and_must_revalidate
+ expires_in 1.minute, public: true, must_revalidate: true
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_in_with_stale_while_revalidate
+ expires_in 1.minute, public: true, stale_while_revalidate: 5.minutes
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_in_with_stale_if_error
+ expires_in 1.minute, public: true, stale_if_error: 5.minutes
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_in_with_public_with_more_keys
+ expires_in 1.minute, :public => true, "s-maxage" => 5.hours
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax
+ expires_in 1.minute, :public => true, :private => nil, "s-maxage" => 5.hours
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_now
+ expires_now
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_cache_control_headers
+ response.headers["Cache-Control"] = "no-transform"
+ expires_now
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_expires_and_confliciting_cache_control_headers
+ response.headers["Cache-Control"] = "no-cache, must-revalidate"
+ expires_now
+ render action: "hello_world"
+ end
+
+ def conditional_hello_without_expires_and_confliciting_cache_control_headers
+ response.headers["Cache-Control"] = "no-cache, must-revalidate"
+ render action: "hello_world"
+ end
+
+ def conditional_hello_with_bangs
+ render action: "hello_world"
+ end
+ before_action :handle_last_modified_and_etags, only: :conditional_hello_with_bangs
+
+ def handle_last_modified_and_etags
+ fresh_when(last_modified: Time.now.utc.beginning_of_day, etag: [ :foo, 123 ])
+ end
+
+ def head_created
+ head :created
+ end
+
+ def head_created_with_application_json_content_type
+ head :created, content_type: "application/json"
+ end
+
+ def head_ok_with_image_png_content_type
+ head :ok, content_type: "image/png"
+ end
+
+ def head_with_location_header
+ head :ok, location: "/foo"
+ end
+
+ def head_with_location_object
+ head :ok, location: Customer.new("david", 1)
+ end
+
+ def head_with_symbolic_status
+ head params[:status].intern
+ end
+
+ def head_with_integer_status
+ head params[:status].to_i
+ end
+
+ def head_with_string_status
+ head params[:status]
+ end
+
+ def head_with_custom_header
+ head :ok, x_custom_header: "something"
+ end
+
+ def head_with_www_authenticate_header
+ head :ok, "WWW-Authenticate" => "something"
+ end
+
+ def head_with_status_code_first
+ head :forbidden, x_custom_header: "something"
+ end
+
+ def head_and_return
+ head(:ok) && return
+ raise "should not reach this line"
+ end
+
+ def head_with_no_content
+ # Fill in the headers with dummy data to make
+ # sure they get removed during the testing
+ response.headers["Content-Type"] = "dummy"
+ response.headers["Content-Length"] = 42
+
+ head 204
+ end
+
+ def head_default_content_type
+ # simulating path like "/1.foobar"
+ request.formats = []
+
+ respond_to do |format|
+ format.any { head 200 }
+ end
+ end
+
+ private
+
+ def set_variable_for_layout
+ @variable_for_layout = nil
+ end
+
+ def determine_layout
+ case action_name
+ when "hello_world", "layout_test", "rendering_without_layout",
+ "rendering_nothing_on_layout", "render_text_hello_world",
+ "render_text_hello_world_with_layout",
+ "hello_world_with_layout_false",
+ "partial_only", "accessing_params_in_template",
+ "accessing_params_in_template_with_layout",
+ "render_with_explicit_template",
+ "render_with_explicit_string_template",
+ "update_page", "update_page_with_instance_variables"
+
+ "layouts/standard"
+ when "action_talk_to_layout", "layout_overriding_layout"
+ "layouts/talk_from_action"
+ when "render_implicit_html_template_from_xhr_request"
+ (request.xhr? ? "layouts/xhr" : "layouts/standard")
+ end
+ end
+end
+
+module TemplateModificationHelper
+ private
+ def modify_template(name)
+ path = File.expand_path("../fixtures/#{name}.erb", __dir__)
+ original = File.read(path)
+ File.write(path, "#{original} Modified!")
+ ActionView::LookupContext::DetailsKey.clear
+ yield
+ ensure
+ File.write(path, original)
+ end
+end
+
+class MetalTestController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionView::Rendering
+ include ActionController::Rendering
+
+ def accessing_logger_in_template
+ render inline: "<%= logger.class %>"
+ end
+end
+
+class ExpiresInRenderTest < ActionController::TestCase
+ tests TestController
+
+ def setup
+ super
+ ActionController::Base.view_paths.paths.each(&:clear_cache)
+ end
+
+ def test_dynamic_render_with_file
+ # This is extremely bad, but should be possible to do.
+ assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
+ response = get :dynamic_render_with_file, params: { id: '../\\../test/abstract_unit.rb' }
+ assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)),
+ response.body
+ end
+
+ def test_dynamic_render_with_absolute_path
+ file = Tempfile.new("name")
+ file.write "secrets!"
+ file.flush
+ assert_raises ActionView::MissingTemplate do
+ get :dynamic_render, params: { id: file.path }
+ end
+ ensure
+ file.close
+ file.unlink
+ end
+
+ def test_dynamic_render
+ assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
+ assert_raises ActionView::MissingTemplate do
+ get :dynamic_render, params: { id: '../\\../test/abstract_unit.rb' }
+ end
+ end
+
+ def test_permitted_dynamic_render_file_hash
+ assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
+ response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } }
+ assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)),
+ response.body
+ end
+
+ def test_dynamic_render_file_hash
+ assert_raises ArgumentError do
+ get :dynamic_render, params: { id: { file: '../\\../test/abstract_unit.rb' } }
+ end
+ end
+
+ def test_expires_in_header
+ get :conditional_hello_with_expires_in
+ assert_equal "max-age=60, private", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_header_with_public
+ get :conditional_hello_with_expires_in_with_public
+ assert_equal "max-age=60, public", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_header_with_must_revalidate
+ get :conditional_hello_with_expires_in_with_must_revalidate
+ assert_equal "max-age=60, private, must-revalidate", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_header_with_public_and_must_revalidate
+ get :conditional_hello_with_expires_in_with_public_and_must_revalidate
+ assert_equal "max-age=60, public, must-revalidate", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_header_with_stale_while_revalidate
+ get :conditional_hello_with_expires_in_with_stale_while_revalidate
+ assert_equal "max-age=60, public, stale-while-revalidate=300", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_header_with_stale_if_error
+ get :conditional_hello_with_expires_in_with_stale_if_error
+ assert_equal "max-age=60, public, stale-if-error=300", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_header_with_additional_headers
+ get :conditional_hello_with_expires_in_with_public_with_more_keys
+ assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_in_old_syntax
+ get :conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax
+ assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_now
+ get :conditional_hello_with_expires_now
+ assert_equal "no-cache", @response.headers["Cache-Control"]
+ end
+
+ def test_expires_now_with_cache_control_headers
+ get :conditional_hello_with_cache_control_headers
+ assert_match(/no-cache/, @response.headers["Cache-Control"])
+ assert_match(/no-transform/, @response.headers["Cache-Control"])
+ end
+
+ def test_expires_now_with_conflicting_cache_control_headers
+ get :conditional_hello_with_expires_and_confliciting_cache_control_headers
+ assert_equal "no-cache", @response.headers["Cache-Control"]
+ end
+
+ def test_no_expires_now_with_conflicting_cache_control_headers
+ get :conditional_hello_without_expires_and_confliciting_cache_control_headers
+ assert_equal "no-cache", @response.headers["Cache-Control"]
+ end
+
+ def test_date_header_when_expires_in
+ time = Time.mktime(2011, 10, 30)
+ Time.stub :now, time do
+ get :conditional_hello_with_expires_in
+ assert_equal Time.now.httpdate, @response.headers["Date"]
+ end
+ end
+end
+
+class LastModifiedRenderTest < ActionController::TestCase
+ tests TestController
+
+ def setup
+ super
+ @last_modified = Time.now.utc.beginning_of_day.httpdate
+ end
+
+ def test_responds_with_last_modified
+ get :conditional_hello
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ end
+
+ def test_request_not_modified
+ @request.if_modified_since = @last_modified
+ get :conditional_hello
+ assert_equal 304, @response.status.to_i
+ assert_predicate @response.body, :blank?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ end
+
+ def test_request_not_modified_but_etag_differs
+ @request.if_modified_since = @last_modified
+ @request.if_none_match = '"234"'
+ get :conditional_hello
+ assert_response :success
+ end
+
+ def test_request_modified
+ @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT"
+ get :conditional_hello
+ assert_equal 200, @response.status.to_i
+ assert_predicate @response.body, :present?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ end
+
+ def test_responds_with_last_modified_with_record
+ get :conditional_hello_with_record
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ end
+
+ def test_request_not_modified_with_record
+ @request.if_modified_since = @last_modified
+ get :conditional_hello_with_record
+ assert_equal 304, @response.status.to_i
+ assert_predicate @response.body, :blank?
+ assert_not_nil @response.etag
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ end
+
+ def test_request_not_modified_but_etag_differs_with_record
+ @request.if_modified_since = @last_modified
+ @request.if_none_match = '"234"'
+ get :conditional_hello_with_record
+ assert_response :success
+ end
+
+ def test_request_modified_with_record
+ @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT"
+ get :conditional_hello_with_record
+ assert_equal 200, @response.status.to_i
+ assert_predicate @response.body, :present?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ end
+
+ def test_responds_with_last_modified_with_collection_of_records
+ get :conditional_hello_with_collection_of_records
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ end
+
+ def test_request_not_modified_with_collection_of_records
+ @request.if_modified_since = @last_modified
+ get :conditional_hello_with_collection_of_records
+ assert_equal 304, @response.status.to_i
+ assert_predicate @response.body, :blank?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ end
+
+ def test_request_not_modified_but_etag_differs_with_collection_of_records
+ @request.if_modified_since = @last_modified
+ @request.if_none_match = '"234"'
+ get :conditional_hello_with_collection_of_records
+ assert_response :success
+ end
+
+ def test_request_modified_with_collection_of_records
+ @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT"
+ get :conditional_hello_with_collection_of_records
+ assert_equal 200, @response.status.to_i
+ assert_predicate @response.body, :present?
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ end
+
+ def test_request_with_bang_gets_last_modified
+ get :conditional_hello_with_bangs
+ assert_equal @last_modified, @response.headers["Last-Modified"]
+ assert_response :success
+ end
+
+ def test_request_with_bang_obeys_last_modified
+ @request.if_modified_since = @last_modified
+ get :conditional_hello_with_bangs
+ assert_response :not_modified
+ end
+
+ def test_last_modified_works_with_less_than_too
+ @request.if_modified_since = 5.years.ago.httpdate
+ get :conditional_hello_with_bangs
+ assert_response :success
+ end
+end
+
+class EtagRenderTest < ActionController::TestCase
+ tests TestControllerWithExtraEtags
+ include TemplateModificationHelper
+
+ def test_strong_etag
+ @request.if_none_match = strong_etag(["strong", "ab", :cde, [:f]])
+ get :strong
+ assert_response :not_modified
+
+ @request.if_none_match = "*"
+ get :strong
+ assert_response :not_modified
+
+ @request.if_none_match = '"strong"'
+ get :strong
+ assert_response :ok
+
+ @request.if_none_match = weak_etag(["strong", "ab", :cde, [:f]])
+ get :strong
+ assert_response :ok
+ end
+
+ def test_multiple_etags
+ @request.if_none_match = weak_etag(["123", "ab", :cde, [:f]])
+ get :fresh
+ assert_response :not_modified
+
+ @request.if_none_match = %("nomatch")
+ get :fresh
+ assert_response :success
+ end
+
+ def test_array
+ @request.if_none_match = weak_etag([%w(1 2 3), "ab", :cde, [:f]])
+ get :array
+ assert_response :not_modified
+
+ @request.if_none_match = %("nomatch")
+ get :array
+ assert_response :success
+ end
+
+ def test_etag_reflects_template_digest
+ get :with_template
+ assert_response :ok
+ assert_not_nil etag = @response.etag
+
+ request.if_none_match = etag
+ get :with_template
+ assert_response :not_modified
+
+ modify_template("test/hello_world") do
+ request.if_none_match = etag
+ get :with_template
+ assert_response :ok
+ assert_not_equal etag, @response.etag
+ end
+ end
+
+ def test_etag_reflects_implicit_template_digest
+ get :with_implicit_template
+ assert_response :ok
+ assert_not_nil etag = @response.etag
+
+ request.if_none_match = etag
+ get :with_implicit_template
+ assert_response :not_modified
+
+ modify_template("test/with_implicit_template") do
+ request.if_none_match = etag
+ get :with_implicit_template
+ assert_response :ok
+ assert_not_equal etag, @response.etag
+ end
+ end
+
+ private
+ def weak_etag(record)
+ "W/#{strong_etag record}"
+ end
+
+ def strong_etag(record)
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}")
+ end
+end
+
+class NamespacedEtagRenderTest < ActionController::TestCase
+ tests Namespaced::ImplicitRenderTestController
+ include TemplateModificationHelper
+
+ def test_etag_reflects_template_digest
+ get :hello_world
+ assert_response :ok
+ assert_not_nil etag = @response.etag
+
+ request.if_none_match = etag
+ get :hello_world
+ assert_response :not_modified
+
+ modify_template("namespaced/implicit_render_test/hello_world") do
+ request.if_none_match = etag
+ get :hello_world
+ assert_response :ok
+ assert_not_equal etag, @response.etag
+ end
+ end
+end
+
+class MetalRenderTest < ActionController::TestCase
+ tests MetalTestController
+
+ def test_access_to_logger_in_view
+ get :accessing_logger_in_template
+ assert_equal "NilClass", @response.body
+ end
+end
+
+class ActionControllerRenderTest < ActionController::TestCase
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+ end
+
+ def test_direct_render_to_string_with_body
+ mc = MinimalController.new
+ assert_equal "Hello world!", mc.render_to_string(body: ["Hello world!"])
+ end
+end
+
+class ActionControllerBaseRenderTest < ActionController::TestCase
+ def test_direct_render_to_string
+ ac = ActionController::Base.new()
+ assert_equal "Hello world!", ac.render_to_string(template: "test/hello_world")
+ end
+end
+
+class ImplicitRenderTest < ActionController::TestCase
+ tests ImplicitRenderTestController
+
+ def test_implicit_no_content_response_as_browser
+ assert_raises(ActionController::MissingExactTemplate) do
+ get :empty_action
+ end
+ end
+
+ def test_implicit_no_content_response_as_xhr
+ get :empty_action, xhr: true
+ assert_response :no_content
+ end
+
+ def test_implicit_success_response_with_right_format
+ get :empty_action_with_template
+ assert_equal "<h1>Empty action rendered this implicitly.</h1>\n", @response.body
+ assert_response :success
+ end
+
+ def test_implicit_unknown_format_response
+ assert_raises(ActionController::UnknownFormat) do
+ get :empty_action_with_template, format: "json"
+ end
+ end
+end
+
+class HeadRenderTest < ActionController::TestCase
+ tests TestController
+
+ def setup
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_head_created
+ post :head_created
+ assert_predicate @response.body, :blank?
+ assert_response :created
+ end
+
+ def test_head_created_with_application_json_content_type
+ post :head_created_with_application_json_content_type
+ assert_predicate @response.body, :blank?
+ assert_equal "application/json", @response.header["Content-Type"]
+ assert_response :created
+ end
+
+ def test_head_ok_with_image_png_content_type
+ post :head_ok_with_image_png_content_type
+ assert_predicate @response.body, :blank?
+ assert_equal "image/png", @response.header["Content-Type"]
+ assert_response :ok
+ end
+
+ def test_head_with_location_header
+ get :head_with_location_header
+ assert_predicate @response.body, :blank?
+ assert_equal "/foo", @response.headers["Location"]
+ assert_response :ok
+ end
+
+ def test_head_with_location_object
+ with_routing do |set|
+ set.draw do
+ resources :customers
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+
+ get :head_with_location_object
+ assert_predicate @response.body, :blank?
+ assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"]
+ assert_response :ok
+ end
+ end
+
+ def test_head_with_custom_header
+ get :head_with_custom_header
+ assert_predicate @response.body, :blank?
+ assert_equal "something", @response.headers["X-Custom-Header"]
+ assert_response :ok
+ end
+
+ def test_head_with_www_authenticate_header
+ get :head_with_www_authenticate_header
+ assert_predicate @response.body, :blank?
+ assert_equal "something", @response.headers["WWW-Authenticate"]
+ assert_response :ok
+ end
+
+ def test_head_with_symbolic_status
+ get :head_with_symbolic_status, params: { status: "ok" }
+ assert_equal 200, @response.status
+ assert_response :ok
+
+ get :head_with_symbolic_status, params: { status: "not_found" }
+ assert_equal 404, @response.status
+ assert_response :not_found
+
+ get :head_with_symbolic_status, params: { status: "no_content" }
+ assert_equal 204, @response.status
+ assert_not_includes @response.headers, "Content-Length"
+ assert_response :no_content
+
+ Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |status, code|
+ get :head_with_symbolic_status, params: { status: status.to_s }
+ assert_equal code, @response.response_code
+ assert_response status
+ end
+ end
+
+ def test_head_with_integer_status
+ Rack::Utils::HTTP_STATUS_CODES.each do |code, message|
+ get :head_with_integer_status, params: { status: code.to_s }
+ assert_equal message, @response.message
+ end
+ end
+
+ def test_head_with_no_content
+ get :head_with_no_content
+
+ assert_equal 204, @response.status
+ assert_nil @response.headers["Content-Type"]
+ assert_nil @response.headers["Content-Length"]
+ end
+
+ def test_head_with_string_status
+ get :head_with_string_status, params: { status: "404 Eat Dirt" }
+ assert_equal 404, @response.response_code
+ assert_equal "Not Found", @response.message
+ assert_response :not_found
+ end
+
+ def test_head_with_status_code_first
+ get :head_with_status_code_first
+ assert_equal 403, @response.response_code
+ assert_equal "Forbidden", @response.message
+ assert_equal "something", @response.headers["X-Custom-Header"]
+ assert_response :forbidden
+ end
+
+ def test_head_returns_truthy_value
+ assert_nothing_raised do
+ get :head_and_return
+ end
+ end
+
+ def test_head_default_content_type
+ post :head_default_content_type
+ assert_equal "text/html", @response.header["Content-Type"]
+ end
+end
+
+class HttpCacheForeverTest < ActionController::TestCase
+ class HttpCacheForeverController < ActionController::Base
+ def cache_me_forever
+ http_cache_forever(public: params[:public]) do
+ render plain: "hello"
+ end
+ end
+ end
+
+ tests HttpCacheForeverController
+
+ def test_cache_with_public
+ get :cache_me_forever, params: { public: true }
+ assert_response :ok
+ assert_equal "max-age=#{100.years}, public", @response.headers["Cache-Control"]
+ assert_not_nil @response.etag
+ assert_predicate @response, :weak_etag?
+ end
+
+ def test_cache_with_private
+ get :cache_me_forever
+ assert_response :ok
+ assert_equal "max-age=#{100.years}, private", @response.headers["Cache-Control"]
+ assert_not_nil @response.etag
+ assert_predicate @response, :weak_etag?
+ end
+
+ def test_cache_response_code_with_if_modified_since
+ get :cache_me_forever
+ assert_response :ok
+
+ @request.if_modified_since = @response.headers["Last-Modified"]
+ get :cache_me_forever
+ assert_response :not_modified
+ end
+
+ def test_cache_response_code_with_etag
+ get :cache_me_forever
+ assert_response :ok
+
+ @request.if_none_match = @response.etag
+ get :cache_me_forever
+ assert_response :not_modified
+ end
+end
diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb
new file mode 100644
index 0000000000..a72d14e4bb
--- /dev/null
+++ b/actionpack/test/controller/render_xml_test.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+require "pathname"
+
+class RenderXmlTest < ActionController::TestCase
+ class XmlRenderable
+ def to_xml(options)
+ options[:root] ||= "i-am-xml"
+ "<#{options[:root]}/>"
+ end
+ end
+
+ class TestController < ActionController::Base
+ protect_from_forgery
+
+ def self.controller_path
+ "test"
+ end
+
+ def render_with_location
+ render xml: "<hello/>", location: "http://example.com", status: 201
+ end
+
+ def render_with_object_location
+ customer = Customer.new("Some guy", 1)
+ render xml: "<customer/>", location: customer, status: :created
+ end
+
+ def render_with_to_xml
+ render xml: XmlRenderable.new
+ end
+
+ def formatted_xml_erb
+ end
+
+ def render_xml_with_custom_content_type
+ render xml: "<blah/>", content_type: "application/atomsvc+xml"
+ end
+
+ def render_xml_with_custom_options
+ render xml: XmlRenderable.new, root: "i-am-THE-xml"
+ end
+ end
+
+ tests TestController
+
+ def setup
+ # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
+ # a more accurate simulation of what happens in "real life".
+ super
+ @controller.logger = ActiveSupport::Logger.new(nil)
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_rendering_with_location_should_set_header
+ get :render_with_location
+ assert_equal "http://example.com", @response.headers["Location"]
+ end
+
+ def test_rendering_xml_should_call_to_xml_if_possible
+ get :render_with_to_xml
+ assert_equal "<i-am-xml/>", @response.body
+ end
+
+ def test_rendering_xml_should_call_to_xml_with_extra_options
+ get :render_xml_with_custom_options
+ assert_equal "<i-am-THE-xml/>", @response.body
+ end
+
+ def test_rendering_with_object_location_should_set_header_with_url_for
+ with_routing do |set|
+ set.draw do
+ resources :customers
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+
+ get :render_with_object_location
+ assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"]
+ end
+ end
+
+ def test_should_render_formatted_xml_erb_template
+ get :formatted_xml_erb, format: :xml
+ assert_equal "<test>passed formatted xml erb</test>", @response.body
+ end
+
+ def test_should_render_xml_but_keep_custom_content_type
+ get :render_xml_with_custom_content_type
+ assert_equal "application/atomsvc+xml", @response.content_type
+ end
+
+ def test_should_use_implicit_content_type
+ get :implicit_content_type, format: "atom"
+ assert_equal Mime[:atom], @response.content_type
+ end
+end
diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb
new file mode 100644
index 0000000000..ae8330e029
--- /dev/null
+++ b/actionpack/test/controller/renderer_test.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class RendererTest < ActiveSupport::TestCase
+ test "action controller base has a renderer" do
+ assert ActionController::Base.renderer
+ end
+
+ test "creating with a controller" do
+ controller = CommentsController
+ renderer = ActionController::Renderer.for controller
+
+ assert_equal controller, renderer.controller
+ end
+
+ test "creating from a controller" do
+ controller = AccountsController
+ renderer = controller.renderer
+
+ assert_equal controller, renderer.controller
+ end
+
+ test "creating with new defaults" do
+ renderer = ApplicationController.renderer
+
+ new_defaults = { https: true }
+ new_renderer = renderer.with_defaults(new_defaults).new
+ content = new_renderer.render(inline: "<%= request.ssl? %>")
+
+ assert_equal "true", content
+ end
+
+ test "rendering with a class renderer" do
+ renderer = ApplicationController.renderer
+ content = renderer.render template: "ruby_template"
+
+ assert_equal "Hello from Ruby code", content
+ end
+
+ test "rendering with an instance renderer" do
+ renderer = ApplicationController.renderer.new
+ content = renderer.render file: "test/hello_world"
+
+ assert_equal "Hello world!", content
+ end
+
+ test "rendering with a controller class" do
+ assert_equal "Hello world!", ApplicationController.render("test/hello_world")
+ end
+
+ test "rendering with locals" do
+ renderer = ApplicationController.renderer
+ content = renderer.render template: "test/render_file_with_locals",
+ locals: { secret: "bar" }
+
+ assert_equal "The secret is bar\n", content
+ end
+
+ test "rendering with assigns" do
+ renderer = ApplicationController.renderer
+ content = renderer.render template: "test/render_file_with_ivar",
+ assigns: { secret: "foo" }
+
+ assert_equal "The secret is foo\n", content
+ end
+
+ test "rendering with custom env" do
+ renderer = ApplicationController.renderer.new method: "post"
+ content = renderer.render inline: "<%= request.post? %>"
+
+ assert_equal "true", content
+ end
+
+ test "rendering with custom env using a key that is not in RACK_KEY_TRANSLATION" do
+ value = "warden is here"
+ renderer = ApplicationController.renderer.new warden: value
+ content = renderer.render inline: "<%= request.env['warden'] %>"
+
+ assert_equal value, content
+ end
+
+ test "rendering with defaults" do
+ renderer = ApplicationController.renderer.new https: true
+ content = renderer.render inline: "<%= request.ssl? %>"
+
+ assert_equal "true", content
+ end
+
+ test "same defaults from the same controller" do
+ renderer_defaults = ->(controller) { controller.renderer.defaults }
+
+ assert_equal renderer_defaults[AccountsController], renderer_defaults[AccountsController]
+ assert_equal renderer_defaults[AccountsController], renderer_defaults[CommentsController]
+ end
+
+ test "rendering with different formats" do
+ html = "Hello world!"
+ xml = "<p>Hello world!</p>\n"
+
+ assert_equal html, render["respond_to/using_defaults"]
+ assert_equal xml, render["respond_to/using_defaults.xml.builder"]
+ assert_equal xml, render["respond_to/using_defaults", formats: :xml]
+ end
+
+ test "rendering with helpers" do
+ assert_equal "<p>1\n<br />2</p>", render[inline: '<%= simple_format "1\n2" %>']
+ end
+
+ test "rendering with user specified defaults" do
+ ApplicationController.renderer.defaults.merge!(hello: "hello", https: true)
+ renderer = ApplicationController.renderer.new
+ content = renderer.render inline: "<%= request.ssl? %>"
+
+ assert_equal "true", content
+ end
+
+ test "return valid asset url with defaults" do
+ renderer = ApplicationController.renderer
+ content = renderer.render inline: "<%= asset_url 'asset.jpg' %>"
+
+ assert_equal "http://example.org/asset.jpg", content
+ end
+
+ test "return valid asset url when https is true" do
+ renderer = ApplicationController.renderer.new https: true
+ content = renderer.render inline: "<%= asset_url 'asset.jpg' %>"
+
+ assert_equal "https://example.org/asset.jpg", content
+ end
+
+ private
+ def render
+ @render ||= ApplicationController.renderer.method(:render)
+ end
+end
diff --git a/actionpack/test/controller/renderers_test.rb b/actionpack/test/controller/renderers_test.rb
new file mode 100644
index 0000000000..d92de6f5d5
--- /dev/null
+++ b/actionpack/test/controller/renderers_test.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+require "active_support/logger"
+
+class RenderersTest < ActionController::TestCase
+ class XmlRenderable
+ def to_xml(options)
+ options[:root] ||= "i-am-xml"
+ "<#{options[:root]}/>"
+ end
+ end
+ class JsonRenderable
+ def as_json(options = {})
+ hash = { a: :b, c: :d, e: :f }
+ hash.except!(*options[:except]) if options[:except]
+ hash
+ end
+
+ def to_json(options = {})
+ super except: [:c, :e]
+ end
+ end
+ class CsvRenderable
+ def to_csv
+ "c,s,v"
+ end
+ end
+ class TestController < ActionController::Base
+ def render_simon_says
+ render simon: "foo"
+ end
+
+ def respond_to_mime
+ respond_to do |type|
+ type.json do
+ render json: JsonRenderable.new
+ end
+ type.js { render json: "JS", callback: "alert" }
+ type.csv { render csv: CsvRenderable.new }
+ type.xml { render xml: XmlRenderable.new }
+ type.html { render body: "HTML" }
+ type.rss { render body: "RSS" }
+ type.all { render body: "Nothing" }
+ type.any(:js, :xml) { render body: "Either JS or XML" }
+ end
+ end
+ end
+
+ tests TestController
+
+ def setup
+ # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
+ # a more accurate simulation of what happens in "real life".
+ super
+ @controller.logger = ActiveSupport::Logger.new(nil)
+ end
+
+ def test_using_custom_render_option
+ ActionController.add_renderer :simon do |says, options|
+ self.content_type = Mime[:text]
+ self.response_body = "Simon says: #{says}"
+ end
+
+ get :render_simon_says
+ assert_equal "Simon says: foo", @response.body
+ ensure
+ ActionController.remove_renderer :simon
+ end
+
+ def test_raises_missing_template_no_renderer
+ assert_raise ActionView::MissingTemplate do
+ get :respond_to_mime, format: "csv"
+ end
+ assert_equal Mime[:csv], @response.content_type
+ assert_equal "", @response.body
+ end
+
+ def test_adding_csv_rendering_via_renderers_add
+ ActionController::Renderers.add :csv do |value, options|
+ send_data value.to_csv, type: Mime[:csv]
+ end
+ @request.accept = "text/csv"
+ get :respond_to_mime, format: "csv"
+ assert_equal Mime[:csv], @response.content_type
+ assert_equal "c,s,v", @response.body
+ ensure
+ ActionController::Renderers.remove :csv
+ end
+end
diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb
new file mode 100644
index 0000000000..b8d86696de
--- /dev/null
+++ b/actionpack/test/controller/request/test_request_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "stringio"
+
+class ActionController::TestRequestTest < ActionController::TestCase
+ def test_test_request_has_session_options_initialized
+ assert @request.session_options
+ end
+
+ def test_mutating_session_options_does_not_affect_default_options
+ @request.session_options[:myparam] = 123
+ assert_nil ActionController::TestSession::DEFAULT_OPTIONS[:myparam]
+ end
+
+ def test_content_length_has_bytes_count_value
+ non_ascii_parameters = { data: { content: "Latin + Кириллица" } }
+ @request.set_header "REQUEST_METHOD", "POST"
+ @request.set_header "CONTENT_TYPE", "application/json"
+ @request.assign_parameters(@routes, "test", "create", non_ascii_parameters,
+ "/test", [:data, :controller, :action])
+ assert_equal(StringIO.new(non_ascii_parameters.to_json).length.to_s,
+ @request.get_header("CONTENT_LENGTH"))
+ end
+
+ ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS.each_pair do |key, value|
+ test "rack default session options #{key} exists in session options and is default" do
+ if value.nil?
+ assert_nil(@request.session_options[key],
+ "Missing rack session default option #{key} in request.session_options")
+ else
+ assert_equal(value, @request.session_options[key],
+ "Missing rack session default option #{key} in request.session_options")
+ end
+ end
+
+ test "rack default session options #{key} exists in session options" do
+ assert(@request.session_options.has_key?(key),
+ "Missing rack session option #{key} in request.session_options")
+ end
+ end
+end
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
new file mode 100644
index 0000000000..ea94a3e048
--- /dev/null
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -0,0 +1,1018 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/log_subscriber/test_helper"
+require "active_support/messages/rotation_configuration"
+
+# common controller actions
+module RequestForgeryProtectionActions
+ def index
+ render inline: "<%= form_tag('/') {} %>"
+ end
+
+ def show_button
+ render inline: "<%= button_to('New', '/') %>"
+ end
+
+ def unsafe
+ render plain: "pwn"
+ end
+
+ def meta
+ render inline: "<%= csrf_meta_tags %>"
+ end
+
+ def form_for_remote
+ render inline: "<%= form_for(:some_resource, :remote => true ) {} %>"
+ end
+
+ def form_for_remote_with_token
+ render inline: "<%= form_for(:some_resource, :remote => true, :authenticity_token => true ) {} %>"
+ end
+
+ def form_for_with_token
+ render inline: "<%= form_for(:some_resource, :authenticity_token => true ) {} %>"
+ end
+
+ def form_for_remote_with_external_token
+ render inline: "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>"
+ end
+
+ def form_with_remote
+ render inline: "<%= form_with(scope: :some_resource) {} %>"
+ end
+
+ def form_with_remote_with_token
+ render inline: "<%= form_with(scope: :some_resource, authenticity_token: true) {} %>"
+ end
+
+ def form_with_local_with_token
+ render inline: "<%= form_with(scope: :some_resource, local: true, authenticity_token: true) {} %>"
+ end
+
+ def form_with_remote_with_external_token
+ render inline: "<%= form_with(scope: :some_resource, authenticity_token: 'external_token') {} %>"
+ end
+
+ def same_origin_js
+ render js: "foo();"
+ end
+
+ def negotiate_same_origin
+ respond_to do |format|
+ format.js { same_origin_js }
+ end
+ end
+
+ def cross_origin_js
+ same_origin_js
+ end
+
+ def negotiate_cross_origin
+ negotiate_same_origin
+ end
+end
+
+# sample controllers
+class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base
+ include RequestForgeryProtectionActions
+ protect_from_forgery only: %w(index meta same_origin_js negotiate_same_origin), with: :reset_session
+end
+
+class RequestForgeryProtectionControllerUsingException < ActionController::Base
+ include RequestForgeryProtectionActions
+ protect_from_forgery only: %w(index meta same_origin_js negotiate_same_origin), with: :exception
+end
+
+class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base
+ protect_from_forgery with: :null_session
+
+ def signed
+ cookies.signed[:foo] = "bar"
+ head :ok
+ end
+
+ def encrypted
+ cookies.encrypted[:foo] = "bar"
+ head :ok
+ end
+
+ def try_to_reset_session
+ reset_session
+ head :ok
+ end
+end
+
+class PrependProtectForgeryBaseController < ActionController::Base
+ before_action :custom_action
+ attr_accessor :called_callbacks
+
+ def index
+ render inline: "OK"
+ end
+
+ private
+
+ def add_called_callback(name)
+ @called_callbacks ||= []
+ @called_callbacks << name
+ end
+
+ def custom_action
+ add_called_callback("custom_action")
+ end
+
+ def verify_authenticity_token
+ add_called_callback("verify_authenticity_token")
+ end
+end
+
+class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession
+ self.allow_forgery_protection = false
+
+ def index
+ render inline: "<%= form_tag('/') {} %>"
+ end
+
+ def show_button
+ render inline: "<%= button_to('New', '/') %>"
+ end
+end
+
+class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession
+ def form_authenticity_param
+ "foobar"
+ end
+end
+
+class PerFormTokensController < ActionController::Base
+ protect_from_forgery with: :exception
+ self.per_form_csrf_tokens = true
+
+ def index
+ render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>"
+ end
+
+ def button_to
+ render inline: "<%= button_to 'Button', (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>"
+ end
+
+ def post_one
+ render plain: ""
+ end
+
+ def post_two
+ render plain: ""
+ end
+end
+
+class SkipProtectionController < ActionController::Base
+ include RequestForgeryProtectionActions
+ protect_from_forgery with: :exception
+ skip_forgery_protection if: :skip_requested
+ attr_accessor :skip_requested
+end
+
+# common test methods
+module RequestForgeryProtectionTests
+ def setup
+ @token = Base64.strict_encode64("railstestrailstestrailstestrails")
+ @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
+ ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
+ end
+
+ def teardown
+ ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
+ end
+
+ def test_should_render_form_with_token_tag
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :index
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ end
+ end
+
+ def test_should_render_button_to_with_token_tag
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :show_button
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ end
+ end
+
+ def test_should_render_form_without_token_tag_if_remote
+ assert_not_blocked do
+ get :form_for_remote
+ end
+ assert_no_match(/authenticity_token/, response.body)
+ end
+
+ def test_should_render_form_with_token_tag_if_remote_and_embedding_token_is_on
+ original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
+ begin
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
+ assert_not_blocked do
+ get :form_for_remote
+ end
+ assert_match(/authenticity_token/, response.body)
+ ensure
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
+ end
+ end
+
+ def test_should_render_form_with_token_tag_if_remote_and_external_authenticity_token_requested_and_embedding_is_on
+ original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
+ begin
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
+ assert_not_blocked do
+ get :form_for_remote_with_external_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
+ ensure
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
+ end
+ end
+
+ def test_should_render_form_with_token_tag_if_remote_and_external_authenticity_token_requested
+ assert_not_blocked do
+ get :form_for_remote_with_external_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
+ end
+
+ def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :form_for_remote_with_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ end
+ end
+
+ def test_should_render_form_with_token_tag_with_authenticity_token_requested
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :form_for_with_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ end
+ end
+
+ def test_should_render_form_with_with_token_tag_if_remote
+ assert_not_blocked do
+ get :form_with_remote
+ end
+ assert_match(/authenticity_token/, response.body)
+ end
+
+ def test_should_render_form_with_without_token_tag_if_remote_and_embedding_token_is_off
+ original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
+ begin
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = false
+ assert_not_blocked do
+ get :form_with_remote
+ end
+ assert_no_match(/authenticity_token/, response.body)
+ ensure
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
+ end
+ end
+
+ def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested_and_embedding_is_on
+ original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
+ begin
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
+ assert_not_blocked do
+ get :form_with_remote_with_external_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
+ ensure
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
+ end
+ end
+
+ def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested
+ assert_not_blocked do
+ get :form_with_remote_with_external_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token"
+ end
+
+ def test_should_render_form_with_with_token_tag_if_remote_and_authenticity_token_requested
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :form_with_remote_with_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ end
+ end
+
+ def test_should_render_form_with_with_token_tag_with_authenticity_token_requested
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :form_with_local_with_token
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ end
+ end
+
+ def test_should_render_form_with_with_token_tag_if_remote_and_embedding_token_is_on
+ original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms
+ begin
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true
+
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ get :form_with_remote
+ end
+ end
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
+ ensure
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
+ end
+ end
+
+ def test_should_allow_get
+ assert_not_blocked { get :index }
+ end
+
+ def test_should_allow_head
+ assert_not_blocked { head :index }
+ end
+
+ def test_should_allow_post_without_token_on_unsafe_action
+ assert_not_blocked { post :unsafe }
+ end
+
+ def test_should_not_allow_post_without_token
+ assert_blocked { post :index }
+ end
+
+ def test_should_not_allow_post_without_token_irrespective_of_format
+ assert_blocked { post :index, format: "xml" }
+ end
+
+ def test_should_not_allow_patch_without_token
+ assert_blocked { patch :index }
+ end
+
+ def test_should_not_allow_put_without_token
+ assert_blocked { put :index }
+ end
+
+ def test_should_not_allow_delete_without_token
+ assert_blocked { delete :index }
+ end
+
+ def test_should_not_allow_xhr_post_without_token
+ assert_blocked { post :index, xhr: true }
+ end
+
+ def test_should_allow_post_with_token
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked { post :index, params: { custom_authenticity_token: @token } }
+ end
+ end
+
+ def test_should_allow_patch_with_token
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } }
+ end
+ end
+
+ def test_should_allow_put_with_token
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked { put :index, params: { custom_authenticity_token: @token } }
+ end
+ end
+
+ def test_should_allow_delete_with_token
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } }
+ end
+ end
+
+ def test_should_allow_post_with_token_in_header
+ session[:_csrf_token] = @token
+ @request.env["HTTP_X_CSRF_TOKEN"] = @token
+ assert_not_blocked { post :index }
+ end
+
+ def test_should_allow_delete_with_token_in_header
+ session[:_csrf_token] = @token
+ @request.env["HTTP_X_CSRF_TOKEN"] = @token
+ assert_not_blocked { delete :index }
+ end
+
+ def test_should_allow_patch_with_token_in_header
+ session[:_csrf_token] = @token
+ @request.env["HTTP_X_CSRF_TOKEN"] = @token
+ assert_not_blocked { patch :index }
+ end
+
+ def test_should_allow_put_with_token_in_header
+ session[:_csrf_token] = @token
+ @request.env["HTTP_X_CSRF_TOKEN"] = @token
+ assert_not_blocked { put :index }
+ end
+
+ def test_should_allow_post_with_origin_checking_and_correct_origin
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ @request.set_header "HTTP_ORIGIN", "http://test.host"
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ end
+ end
+ end
+
+ def test_should_allow_post_with_origin_checking_and_no_origin
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_not_blocked do
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ end
+ end
+ end
+
+ def test_should_raise_for_post_with_null_origin
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ exception = assert_raises(ActionController::InvalidAuthenticityToken) do
+ @request.set_header "HTTP_ORIGIN", "null"
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ assert_match "The browser returned a 'null' origin for a request", exception.message
+ end
+ end
+ end
+
+ def test_should_block_post_with_origin_checking_and_wrong_origin
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ ActionController::Base.logger = logger
+
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ assert_blocked do
+ @request.set_header "HTTP_ORIGIN", "http://bad.host"
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ end
+ end
+
+ assert_match(
+ "HTTP Origin header (http://bad.host) didn't match request.base_url (http://test.host)",
+ logger.logged(:warn).last
+ )
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+
+ def test_should_warn_on_missing_csrf_token
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ ActionController::Base.logger = logger
+
+ begin
+ assert_blocked { post :index }
+
+ assert_equal 1, logger.logged(:warn).size
+ assert_match(/CSRF token authenticity/, logger.logged(:warn).last)
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+ end
+
+ def test_should_not_warn_if_csrf_logging_disabled
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ ActionController::Base.logger = logger
+ ActionController::Base.log_warning_on_csrf_failure = false
+
+ begin
+ assert_blocked { post :index }
+
+ assert_equal 0, logger.logged(:warn).size
+ ensure
+ ActionController::Base.logger = old_logger
+ ActionController::Base.log_warning_on_csrf_failure = true
+ end
+ end
+
+ def test_should_only_allow_same_origin_js_get_with_xhr_header
+ assert_cross_origin_blocked { get :same_origin_js }
+ assert_cross_origin_blocked { get :same_origin_js, format: "js" }
+ assert_cross_origin_blocked do
+ @request.accept = "text/javascript"
+ get :negotiate_same_origin
+ end
+
+ assert_cross_origin_blocked do
+ @request.accept = "application/javascript"
+ get :negotiate_same_origin
+ end
+
+ assert_cross_origin_not_blocked { get :same_origin_js, xhr: true }
+ assert_cross_origin_not_blocked { get :same_origin_js, xhr: true, format: "js" }
+ assert_cross_origin_not_blocked do
+ @request.accept = "text/javascript"
+ get :negotiate_same_origin, xhr: true
+ end
+ end
+
+ def test_should_warn_on_not_same_origin_js
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ ActionController::Base.logger = logger
+
+ begin
+ assert_cross_origin_blocked { get :same_origin_js }
+
+ assert_equal 1, logger.logged(:warn).size
+ assert_match(/<script> tag on another site requested protected JavaScript/, logger.logged(:warn).last)
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+ end
+
+ def test_should_not_warn_if_csrf_logging_disabled_and_not_same_origin_js
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ ActionController::Base.logger = logger
+ ActionController::Base.log_warning_on_csrf_failure = false
+
+ begin
+ assert_cross_origin_blocked { get :same_origin_js }
+
+ assert_equal 0, logger.logged(:warn).size
+ ensure
+ ActionController::Base.logger = old_logger
+ ActionController::Base.log_warning_on_csrf_failure = true
+ end
+ end
+
+ # Allow non-GET requests since GET is all a remote <script> tag can muster.
+ def test_should_allow_non_get_js_without_xhr_header
+ session[:_csrf_token] = @token
+ assert_cross_origin_not_blocked { post :same_origin_js, params: { custom_authenticity_token: @token } }
+ assert_cross_origin_not_blocked { post :same_origin_js, params: { format: "js", custom_authenticity_token: @token } }
+ assert_cross_origin_not_blocked do
+ @request.accept = "text/javascript"
+ post :negotiate_same_origin, params: { custom_authenticity_token: @token }
+ end
+ end
+
+ def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled
+ assert_cross_origin_not_blocked { get :cross_origin_js }
+ assert_cross_origin_not_blocked { get :cross_origin_js, format: "js" }
+ assert_cross_origin_not_blocked do
+ @request.accept = "text/javascript"
+ get :negotiate_cross_origin
+ end
+
+ assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true }
+ assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true, format: "js" }
+ assert_cross_origin_not_blocked do
+ @request.accept = "text/javascript"
+ get :negotiate_cross_origin, xhr: true
+ end
+ end
+
+ def test_should_not_raise_error_if_token_is_not_a_string
+ assert_blocked do
+ patch :index, params: { custom_authenticity_token: { foo: "bar" } }
+ end
+ end
+
+ def assert_blocked
+ session[:something_like_user_id] = 1
+ yield
+ assert_nil session[:something_like_user_id], "session values are still present"
+ assert_response :success
+ end
+
+ def assert_not_blocked
+ assert_nothing_raised { yield }
+ assert_response :success
+ end
+
+ def assert_cross_origin_blocked
+ assert_raises(ActionController::InvalidCrossOriginRequest) do
+ yield
+ end
+ end
+
+ def assert_cross_origin_not_blocked
+ assert_not_blocked { yield }
+ end
+
+ def forgery_protection_origin_check
+ old_setting = ActionController::Base.forgery_protection_origin_check
+ ActionController::Base.forgery_protection_origin_check = true
+ begin
+ yield
+ ensure
+ ActionController::Base.forgery_protection_origin_check = old_setting
+ end
+ end
+end
+
+# OK let's get our test on
+
+class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase
+ include RequestForgeryProtectionTests
+
+ test "should emit a csrf-param meta tag and a csrf-token meta tag" do
+ @controller.stub :form_authenticity_token, @token + "<=?" do
+ get :meta
+ assert_select "meta[name=?][content=?]", "csrf-param", "custom_authenticity_token"
+ assert_select "meta[name=?]", "csrf-token"
+ regexp = "#{@token}&lt;=\?"
+ assert_match(/#{regexp}/, @response.body)
+ end
+ end
+end
+
+class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
+ class NullSessionDummyKeyGenerator
+ def generate_key(secret, length = nil)
+ "03312270731a2ed0d11ed091c2338a06"
+ end
+ end
+
+ def setup
+ @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new
+ @request.env[ActionDispatch::Cookies::COOKIES_ROTATIONS] = ActiveSupport::Messages::RotationConfiguration.new
+ end
+
+ test "should allow to set signed cookies" do
+ post :signed
+ assert_response :ok
+ end
+
+ test "should allow to set encrypted cookies" do
+ post :encrypted
+ assert_response :ok
+ end
+
+ test "should allow reset_session" do
+ post :try_to_reset_session
+ assert_response :ok
+ end
+end
+
+class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase
+ include RequestForgeryProtectionTests
+ def assert_blocked
+ assert_raises(ActionController::InvalidAuthenticityToken) do
+ yield
+ end
+ end
+end
+
+class PrependProtectForgeryBaseControllerTest < ActionController::TestCase
+ PrependTrueController = Class.new(PrependProtectForgeryBaseController) do
+ protect_from_forgery prepend: true
+ end
+
+ PrependFalseController = Class.new(PrependProtectForgeryBaseController) do
+ protect_from_forgery prepend: false
+ end
+
+ PrependDefaultController = Class.new(PrependProtectForgeryBaseController) do
+ protect_from_forgery
+ end
+
+ def test_verify_authenticity_token_is_prepended
+ @controller = PrependTrueController.new
+ get :index
+ expected_callback_order = ["verify_authenticity_token", "custom_action"]
+ assert_equal(expected_callback_order, @controller.called_callbacks)
+ end
+
+ def test_verify_authenticity_token_is_not_prepended
+ @controller = PrependFalseController.new
+ get :index
+ expected_callback_order = ["custom_action", "verify_authenticity_token"]
+ assert_equal(expected_callback_order, @controller.called_callbacks)
+ end
+
+ def test_verify_authenticity_token_is_not_prepended_by_default
+ @controller = PrependDefaultController.new
+ get :index
+ expected_callback_order = ["custom_action", "verify_authenticity_token"]
+ assert_equal(expected_callback_order, @controller.called_callbacks)
+ end
+end
+
+class FreeCookieControllerTest < ActionController::TestCase
+ def setup
+ @controller = FreeCookieController.new
+ @token = "cf50faa3fe97702ca1ae"
+ super
+ end
+
+ def test_should_not_render_form_with_token_tag
+ SecureRandom.stub :base64, @token do
+ get :index
+ assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false
+ end
+ end
+
+ def test_should_not_render_button_to_with_token_tag
+ SecureRandom.stub :base64, @token do
+ get :show_button
+ assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false
+ end
+ end
+
+ def test_should_allow_all_methods_without_token
+ SecureRandom.stub :base64, @token do
+ [:post, :patch, :put, :delete].each do |method|
+ assert_nothing_raised { send(method, :index) }
+ end
+ end
+ end
+
+ test "should not emit a csrf-token meta tag" do
+ SecureRandom.stub :base64, @token do
+ get :meta
+ assert_predicate @response.body, :blank?
+ end
+ end
+end
+
+class CustomAuthenticityParamControllerTest < ActionController::TestCase
+ def setup
+ super
+ @old_logger = ActionController::Base.logger
+ @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ @token = Base64.strict_encode64(SecureRandom.random_bytes(32))
+ @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
+ ActionController::Base.request_forgery_protection_token = @token
+ end
+
+ def teardown
+ ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
+ super
+ end
+
+ def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
+ ActionController::Base.logger = @logger
+ begin
+ @controller.stub :valid_authenticity_token?, :true do
+ post :index, params: { custom_token_name: "foobar" }
+ assert_equal 0, @logger.logged(:warn).size
+ end
+ ensure
+ ActionController::Base.logger = @old_logger
+ end
+ end
+
+ def test_should_warn_if_form_authenticity_param_does_not_match_form_authenticity_token
+ ActionController::Base.logger = @logger
+
+ begin
+ post :index, params: { custom_token_name: "bazqux" }
+ assert_equal 1, @logger.logged(:warn).size
+ ensure
+ ActionController::Base.logger = @old_logger
+ end
+ end
+end
+
+class PerFormTokensControllerTest < ActionController::TestCase
+ def setup
+ @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
+ ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
+ end
+
+ def teardown
+ ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
+ end
+
+ def test_per_form_token_is_same_size_as_global_token
+ get :index
+ expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH
+ actual = @controller.send(:per_form_csrf_token, session, "/path", "post").size
+ assert_equal expected, actual
+ end
+
+ def test_accepts_token_for_correct_path_and_method
+ get :index
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ assert_response :success
+ end
+
+ def test_rejects_token_for_incorrect_path
+ get :index
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_two"
+ assert_raises(ActionController::InvalidAuthenticityToken) do
+ post :post_two, params: { custom_authenticity_token: form_token }
+ end
+ end
+
+ def test_rejects_token_for_incorrect_method
+ get :index
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_raises(ActionController::InvalidAuthenticityToken) do
+ patch :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+
+ def test_rejects_token_for_incorrect_method_button_to
+ get :button_to, params: { form_method: "delete" }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token, "delete"
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_raises(ActionController::InvalidAuthenticityToken) do
+ patch :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+
+ test "Accepts proper token for implicit post method on button_to tag" do
+ get :button_to
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token, "post"
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+
+ %w{delete post patch}.each do |verb|
+ test "Accepts proper token for #{verb} method on button_to tag" do
+ get :button_to, params: { form_method: verb }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token, verb
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ send verb, :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+ end
+
+ def test_accepts_global_csrf_token
+ get :index
+
+ token = @controller.send(:form_authenticity_token)
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: token }
+ end
+ assert_response :success
+ end
+
+ def test_ignores_params
+ get :index, params: { form_path: "/per_form_tokens/post_one?foo=bar" }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one?foo=baz"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token, baz: "foo" }
+ end
+ assert_response :success
+ end
+
+ def test_ignores_trailing_slash_during_generation
+ get :index, params: { form_path: "/per_form_tokens/post_one/" }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ assert_response :success
+ end
+
+ def test_ignores_origin_during_generation
+ get :index, params: { form_path: "https://example.com/per_form_tokens/post_one/" }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ assert_response :success
+ end
+
+ def test_ignores_trailing_slash_during_validation
+ get :index
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one/"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ assert_response :success
+ end
+
+ def test_method_is_case_insensitive
+ get :index, params: { form_method: "POST" }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one/"
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ assert_response :success
+ end
+
+ private
+ def assert_presence_and_fetch_form_csrf_token
+ assert_select 'input[name="custom_authenticity_token"]' do |input|
+ form_csrf_token = input.first["value"]
+ assert_not_nil form_csrf_token
+ return form_csrf_token
+ end
+ end
+
+ def assert_matches_session_token_on_server(form_token, method = "post")
+ actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token))
+ expected = @controller.send(:per_form_csrf_token, session, "/per_form_tokens/post_one", method)
+ assert_equal expected, actual
+ end
+end
+
+class SkipProtectionControllerTest < ActionController::TestCase
+ def test_should_not_allow_post_without_token_when_not_skipping
+ @controller.skip_requested = false
+ assert_blocked { post :index }
+ end
+
+ def test_should_allow_post_without_token_when_skipping
+ @controller.skip_requested = true
+ assert_not_blocked { post :index }
+ end
+
+ def assert_blocked
+ assert_raises(ActionController::InvalidAuthenticityToken) do
+ yield
+ end
+ end
+
+ def assert_not_blocked
+ assert_nothing_raised { yield }
+ assert_response :success
+ end
+end
diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb
new file mode 100644
index 0000000000..4a83d07e7d
--- /dev/null
+++ b/actionpack/test/controller/required_params_test.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class BooksController < ActionController::Base
+ def create
+ params.require(:book).require(:name)
+ head :ok
+ end
+end
+
+class ActionControllerRequiredParamsTest < ActionController::TestCase
+ tests BooksController
+
+ test "missing required parameters will raise exception" do
+ assert_raise ActionController::ParameterMissing do
+ post :create, params: { magazine: { name: "Mjallo!" } }
+ end
+
+ assert_raise ActionController::ParameterMissing do
+ post :create, params: { book: { title: "Mjallo!" } }
+ end
+ end
+
+ test "required parameters that are present will not raise" do
+ post :create, params: { book: { name: "Mjallo!" } }
+ assert_response :ok
+ end
+
+ test "required parameters with false value will not raise" do
+ post :create, params: { book: { name: false } }
+ assert_response :ok
+ end
+end
+
+class ParametersRequireTest < ActiveSupport::TestCase
+ test "required parameters should accept and return false value" do
+ assert_equal(false, ActionController::Parameters.new(person: false).require(:person))
+ end
+
+ test "required parameters must not be nil" do
+ assert_raises(ActionController::ParameterMissing) do
+ ActionController::Parameters.new(person: nil).require(:person)
+ end
+ end
+
+ test "required parameters must not be empty" do
+ assert_raises(ActionController::ParameterMissing) do
+ ActionController::Parameters.new(person: {}).require(:person)
+ end
+ end
+
+ test "require array when all required params are present" do
+ safe_params = ActionController::Parameters.new(person: { first_name: "Gaurish", title: "Mjallo", city: "Barcelona" })
+ .require(:person)
+ .require([:first_name, :title])
+
+ assert_kind_of Array, safe_params
+ assert_equal ["Gaurish", "Mjallo"], safe_params
+ end
+
+ test "require array when a required param is missing" do
+ assert_raises(ActionController::ParameterMissing) do
+ ActionController::Parameters.new(person: { first_name: "Gaurish", title: nil })
+ .require(:person)
+ .require([:first_name, :title])
+ end
+ end
+
+ test "value params" do
+ params = ActionController::Parameters.new(foo: "bar", dog: "cinco")
+ assert_equal ["bar", "cinco"], params.values
+ assert params.has_value?("cinco")
+ assert params.value?("cinco")
+ end
+
+ test "to_param works like in a Hash" do
+ params = ActionController::Parameters.new(nested: { key: "value" }).permit!
+ assert_equal({ nested: { key: "value" } }.to_param, params.to_param)
+
+ params = { root: ActionController::Parameters.new(nested: { key: "value" }).permit! }
+ assert_equal({ root: { nested: { key: "value" } } }.to_param, params.to_param)
+
+ assert_raise(ActionController::UnfilteredParameters) do
+ ActionController::Parameters.new(nested: { key: "value" }).to_param
+ end
+ end
+
+ test "to_query works like in a Hash" do
+ params = ActionController::Parameters.new(nested: { key: "value" }).permit!
+ assert_equal({ nested: { key: "value" } }.to_query, params.to_query)
+
+ params = { root: ActionController::Parameters.new(nested: { key: "value" }).permit! }
+ assert_equal({ root: { nested: { key: "value" } } }.to_query, params.to_query)
+
+ assert_raise(ActionController::UnfilteredParameters) do
+ ActionController::Parameters.new(nested: { key: "value" }).to_query
+ end
+ end
+end
diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb
new file mode 100644
index 0000000000..089b0b94d4
--- /dev/null
+++ b/actionpack/test/controller/rescue_test.rb
@@ -0,0 +1,364 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class RescueController < ActionController::Base
+ class NotAuthorized < StandardError
+ end
+ class NotAuthorizedToRescueAsString < StandardError
+ end
+
+ class RecordInvalid < StandardError
+ end
+ class RecordInvalidToRescueAsString < StandardError
+ end
+
+ class NotAllowed < StandardError
+ end
+ class NotAllowedToRescueAsString < StandardError
+ end
+
+ class InvalidRequest < StandardError
+ end
+ class InvalidRequestToRescueAsString < StandardError
+ end
+
+ class BadGateway < StandardError
+ end
+ class BadGatewayToRescueAsString < StandardError
+ end
+
+ class ResourceUnavailable < StandardError
+ end
+ class ResourceUnavailableToRescueAsString < StandardError
+ end
+
+ # We use a fully qualified name in some strings, and a relative constant
+ # name in some other to test correct handling of both cases.
+
+ rescue_from NotAuthorized, with: :deny_access
+ rescue_from "RescueController::NotAuthorizedToRescueAsString", with: :deny_access
+
+ rescue_from RecordInvalid, with: :show_errors
+ rescue_from "RescueController::RecordInvalidToRescueAsString", with: :show_errors
+
+ rescue_from NotAllowed, with: proc { head :forbidden }
+ rescue_from "RescueController::NotAllowedToRescueAsString", with: proc { head :forbidden }
+
+ rescue_from InvalidRequest, with: proc { |exception| render plain: exception.message }
+ rescue_from "InvalidRequestToRescueAsString", with: proc { |exception| render plain: exception.message }
+
+ rescue_from BadGateway do
+ head 502
+ end
+ rescue_from "BadGatewayToRescueAsString" do
+ head 502
+ end
+
+ rescue_from ResourceUnavailable do |exception|
+ render plain: exception.message
+ end
+ rescue_from "ResourceUnavailableToRescueAsString" do |exception|
+ render plain: exception.message
+ end
+
+ rescue_from ActionDispatch::Http::Parameters::ParseError do
+ render plain: "parse error", status: :bad_request
+ end
+
+ before_action(only: :before_action_raises) { raise "umm nice" }
+
+ def before_action_raises
+ end
+
+ def not_authorized
+ raise NotAuthorized
+ end
+ def not_authorized_raise_as_string
+ raise NotAuthorizedToRescueAsString
+ end
+
+ def not_allowed
+ raise NotAllowed
+ end
+ def not_allowed_raise_as_string
+ raise NotAllowedToRescueAsString
+ end
+
+ def invalid_request
+ raise InvalidRequest
+ end
+ def invalid_request_raise_as_string
+ raise InvalidRequestToRescueAsString
+ end
+
+ def record_invalid
+ raise RecordInvalid
+ end
+ def record_invalid_raise_as_string
+ raise RecordInvalidToRescueAsString
+ end
+
+ def bad_gateway
+ raise BadGateway
+ end
+ def bad_gateway_raise_as_string
+ raise BadGatewayToRescueAsString
+ end
+
+ def resource_unavailable
+ raise ResourceUnavailable
+ end
+ def resource_unavailable_raise_as_string
+ raise ResourceUnavailableToRescueAsString
+ end
+
+ def arbitrary_action
+ params
+ render plain: "arbitrary action"
+ end
+
+ def missing_template
+ end
+
+ def exception_with_more_specific_handler_for_wrapper
+ raise RecordInvalid
+ rescue
+ raise NotAuthorized
+ end
+
+ def exception_with_more_specific_handler_for_cause
+ raise NotAuthorized
+ rescue
+ raise RecordInvalid
+ end
+
+ def exception_with_no_handler_for_wrapper
+ raise RecordInvalid
+ rescue
+ raise RangeError
+ end
+
+ private
+ def deny_access
+ head :forbidden
+ end
+
+ def show_errors(exception)
+ head :unprocessable_entity
+ end
+end
+
+class ExceptionInheritanceRescueController < ActionController::Base
+ class ParentException < StandardError
+ end
+
+ class ChildException < ParentException
+ end
+
+ class GrandchildException < ChildException
+ end
+
+ rescue_from ChildException, with: lambda { head :ok }
+ rescue_from ParentException, with: lambda { head :created }
+ rescue_from GrandchildException, with: lambda { head :no_content }
+
+ def raise_parent_exception
+ raise ParentException
+ end
+
+ def raise_child_exception
+ raise ChildException
+ end
+
+ def raise_grandchild_exception
+ raise GrandchildException
+ end
+end
+
+class ExceptionInheritanceRescueControllerTest < ActionController::TestCase
+ def test_bottom_first
+ get :raise_grandchild_exception
+ assert_response :no_content
+ end
+
+ def test_inheritance_works
+ get :raise_child_exception
+ assert_response :created
+ end
+end
+
+class ControllerInheritanceRescueController < ExceptionInheritanceRescueController
+ class FirstExceptionInChildController < StandardError
+ end
+
+ class SecondExceptionInChildController < StandardError
+ end
+
+ rescue_from FirstExceptionInChildController, "SecondExceptionInChildController", with: lambda { head :gone }
+
+ def raise_first_exception_in_child_controller
+ raise FirstExceptionInChildController
+ end
+
+ def raise_second_exception_in_child_controller
+ raise SecondExceptionInChildController
+ end
+end
+
+class ControllerInheritanceRescueControllerTest < ActionController::TestCase
+ def test_first_exception_in_child_controller
+ get :raise_first_exception_in_child_controller
+ assert_response :gone
+ end
+
+ def test_second_exception_in_child_controller
+ get :raise_second_exception_in_child_controller
+ assert_response :gone
+ end
+
+ def test_exception_in_parent_controller
+ get :raise_parent_exception
+ assert_response :created
+ end
+end
+
+class RescueControllerTest < ActionController::TestCase
+ def test_rescue_handler
+ get :not_authorized
+ assert_response :forbidden
+ end
+ def test_rescue_handler_string
+ get :not_authorized_raise_as_string
+ assert_response :forbidden
+ end
+
+ def test_rescue_handler_with_argument
+ assert_called_with @controller, :show_errors, [Exception] do
+ get :record_invalid
+ end
+ end
+ def test_rescue_handler_with_argument_as_string
+ assert_called_with @controller, :show_errors, [Exception] do
+ get :record_invalid_raise_as_string
+ end
+ end
+
+ def test_proc_rescue_handler
+ get :not_allowed
+ assert_response :forbidden
+ end
+ def test_proc_rescue_handler_as_string
+ get :not_allowed_raise_as_string
+ assert_response :forbidden
+ end
+
+ def test_proc_rescue_handle_with_argument
+ get :invalid_request
+ assert_equal "RescueController::InvalidRequest", @response.body
+ end
+ def test_proc_rescue_handle_with_argument_as_string
+ get :invalid_request_raise_as_string
+ assert_equal "RescueController::InvalidRequestToRescueAsString", @response.body
+ end
+
+ def test_block_rescue_handler
+ get :bad_gateway
+ assert_response 502
+ end
+ def test_block_rescue_handler_as_string
+ get :bad_gateway_raise_as_string
+ assert_response 502
+ end
+
+ def test_block_rescue_handler_with_argument
+ get :resource_unavailable
+ assert_equal "RescueController::ResourceUnavailable", @response.body
+ end
+ def test_block_rescue_handler_with_argument_as_string
+ get :resource_unavailable_raise_as_string
+ assert_equal "RescueController::ResourceUnavailableToRescueAsString", @response.body
+ end
+
+ test "rescue when wrapper has more specific handler than cause" do
+ get :exception_with_more_specific_handler_for_wrapper
+ assert_response :forbidden
+ end
+
+ test "rescue when cause has more specific handler than wrapper" do
+ get :exception_with_more_specific_handler_for_cause
+ assert_response :unprocessable_entity
+ end
+
+ test "rescue when cause has handler, but wrapper doesnt" do
+ get :exception_with_no_handler_for_wrapper
+ assert_response :unprocessable_entity
+ end
+
+ test "can rescue a ParseError" do
+ capture_log_output do
+ post :arbitrary_action, body: "{", as: :json
+ end
+ assert_response :bad_request
+ assert_equal "parse error", response.body
+ end
+
+ private
+
+ def capture_log_output
+ output = StringIO.new
+ request.set_header "action_dispatch.logger", ActiveSupport::Logger.new(output)
+ yield
+ output.string
+ end
+end
+
+class RescueTest < ActionDispatch::IntegrationTest
+ class TestController < ActionController::Base
+ class RecordInvalid < StandardError
+ def message
+ "invalid"
+ end
+ end
+ rescue_from RecordInvalid, with: :show_errors
+
+ def foo
+ render plain: "foo"
+ end
+
+ def invalid
+ raise RecordInvalid
+ end
+
+ private
+ def show_errors(exception)
+ render plain: exception.message
+ end
+ end
+
+ test "normal request" do
+ with_test_routing do
+ get "/foo"
+ assert_equal "foo", response.body
+ end
+ end
+
+ test "rescue exceptions inside controller" do
+ with_test_routing do
+ get "/invalid"
+ assert_equal "invalid", response.body
+ end
+ end
+
+ private
+
+ def with_test_routing
+ with_routing do |set|
+ set.draw do
+ get "foo", to: ::RescueTest::TestController.action(:foo)
+ get "invalid", to: ::RescueTest::TestController.action(:invalid)
+ end
+ yield
+ end
+ end
+end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
new file mode 100644
index 0000000000..d2146f12a5
--- /dev/null
+++ b/actionpack/test/controller/resources_test.rb
@@ -0,0 +1,1390 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/object/try"
+require "active_support/core_ext/object/with_options"
+require "active_support/core_ext/array/extract_options"
+
+class AdminController < ResourcesController; end
+class MessagesController < ResourcesController; end
+class ProductsController < ResourcesController; end
+class ThreadsController < ResourcesController; end
+
+module Backoffice
+ class ProductsController < ResourcesController; end
+ class ImagesController < ResourcesController; end
+
+ module Admin
+ class ProductsController < ResourcesController; end
+ class ImagesController < ResourcesController; end
+ end
+end
+
+class ResourcesTest < ActionController::TestCase
+ def test_default_restful_routes
+ with_restful_routing :messages do
+ assert_simply_restful_for :messages
+ end
+ end
+
+ def test_override_paths_for_member_and_collection_methods
+ collection_methods = { rss: :get, reorder: :post, csv: :post }
+ member_methods = { rss: :get, atom: :get, upload: :post, fix: :post }
+ path_names = { new: "nuevo", rss: "canal", fix: "corrigir" }
+
+ with_restful_routing :messages,
+ collection: collection_methods,
+ member: member_methods,
+ path_names: path_names do
+
+ assert_restful_routes_for :messages,
+ collection: collection_methods,
+ member: member_methods,
+ path_names: path_names do |options|
+ member_methods.each do |action, method|
+ assert_recognizes(options.merge(action: action.to_s, id: "1"),
+ path: "/messages/1/#{path_names[action] || action}",
+ method: method)
+ end
+
+ collection_methods.each do |action, method|
+ assert_recognizes(options.merge(action: action.to_s),
+ path: "/messages/#{path_names[action] || action}",
+ method: method)
+ end
+ end
+
+ assert_restful_named_routes_for :messages,
+ collection: collection_methods,
+ member: member_methods,
+ path_names: path_names do |options|
+
+ collection_methods.each_key do |action|
+ assert_named_route "/messages/#{path_names[action] || action}", "#{action}_messages_path", action: action
+ end
+
+ member_methods.each_key do |action|
+ assert_named_route "/messages/1/#{path_names[action] || action}", "#{action}_message_path", action: action, id: "1"
+ end
+ end
+ end
+ end
+
+ def test_multiple_default_restful_routes
+ with_restful_routing :messages, :comments do
+ assert_simply_restful_for :messages
+ assert_simply_restful_for :comments
+ end
+ end
+
+ def test_multiple_resources_with_options
+ expected_options = { controller: "threads", action: "index" }
+
+ with_restful_routing :messages, :comments, expected_options.slice(:controller) do
+ assert_recognizes(expected_options, path: "comments")
+ assert_recognizes(expected_options, path: "messages")
+ end
+ end
+
+ def test_with_custom_conditions
+ with_restful_routing :messages, conditions: { subdomain: "app" } do
+ assert @routes.recognize_path("/messages", method: :get, subdomain: "app")
+ end
+ end
+
+ def test_irregular_id_with_no_constraints_should_raise_error
+ expected_options = { controller: "messages", action: "show", id: "1.1.1" }
+
+ with_restful_routing :messages do
+ assert_raise(Assertion) do
+ assert_recognizes(expected_options, path: "messages/1.1.1", method: :get)
+ end
+ end
+ end
+
+ def test_irregular_id_with_constraints_should_pass
+ expected_options = { controller: "messages", action: "show", id: "1.1.1" }
+
+ with_restful_routing(:messages, constraints: { id: /[0-9]\.[0-9]\.[0-9]/ }) do
+ assert_recognizes(expected_options, path: "messages/1.1.1", method: :get)
+ end
+ end
+
+ def test_with_path_prefix_constraints
+ expected_options = { controller: "messages", action: "show", thread_id: "1.1.1", id: "1" }
+ with_restful_routing :messages, path_prefix: "/thread/:thread_id", constraints: { thread_id: /[0-9]\.[0-9]\.[0-9]/ } do
+ assert_recognizes(expected_options, path: "thread/1.1.1/messages/1", method: :get)
+ end
+ end
+
+ def test_irregular_id_constraints_should_get_passed_to_member_actions
+ expected_options = { controller: "messages", action: "custom", id: "1.1.1" }
+
+ with_restful_routing(:messages, member: { custom: :get }, constraints: { id: /[0-9]\.[0-9]\.[0-9]/ }) do
+ assert_recognizes(expected_options, path: "messages/1.1.1/custom", method: :get)
+ end
+ end
+
+ def test_with_path_prefix
+ with_restful_routing :messages, path_prefix: "/thread/:thread_id" do
+ assert_simply_restful_for :messages, path_prefix: "thread/5/", options: { thread_id: "5" }
+ end
+ end
+
+ def test_multiple_with_path_prefix
+ with_restful_routing :messages, :comments, path_prefix: "/thread/:thread_id" do
+ assert_simply_restful_for :messages, path_prefix: "thread/5/", options: { thread_id: "5" }
+ assert_simply_restful_for :comments, path_prefix: "thread/5/", options: { thread_id: "5" }
+ end
+ end
+
+ def test_with_name_prefix
+ with_restful_routing :messages, as: "post_messages" do
+ assert_simply_restful_for :messages, name_prefix: "post_"
+ end
+ end
+
+ def test_with_collection_actions
+ actions = { "a" => :get, "b" => :put, "c" => :post, "d" => :delete, "e" => :patch }
+
+ with_routing do |set|
+ set.draw do
+ resources :messages do
+ get :a, on: :collection
+ put :b, on: :collection
+ post :c, on: :collection
+ delete :d, on: :collection
+ patch :e, on: :collection
+ end
+ end
+
+ assert_restful_routes_for :messages do |options|
+ actions.each do |action, method|
+ assert_recognizes(options.merge(action: action), path: "/messages/#{action}", method: method)
+ end
+ end
+
+ assert_restful_named_routes_for :messages do
+ actions.each_key do |action|
+ assert_named_route "/messages/#{action}", "#{action}_messages_path", action: action
+ end
+ end
+ end
+ end
+
+ def test_with_collection_actions_and_name_prefix
+ actions = { "a" => :get, "b" => :put, "c" => :post, "d" => :delete, "e" => :patch }
+
+ with_routing do |set|
+ set.draw do
+ scope "/threads/:thread_id" do
+ resources :messages, as: "thread_messages" do
+ get :a, on: :collection
+ put :b, on: :collection
+ post :c, on: :collection
+ delete :d, on: :collection
+ patch :e, on: :collection
+ end
+ end
+ end
+
+ assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options|
+ actions.each do |action, method|
+ assert_recognizes(options.merge(action: action), path: "/threads/1/messages/#{action}", method: method)
+ end
+ end
+
+ assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do
+ actions.each_key do |action|
+ assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", action: action
+ end
+ end
+ end
+ end
+
+ def test_with_collection_actions_and_name_prefix_and_member_action_with_same_name
+ actions = { "a" => :get }
+
+ with_routing do |set|
+ set.draw do
+ scope "/threads/:thread_id" do
+ resources :messages, as: "thread_messages" do
+ get :a, on: :collection
+ get :a, on: :member
+ end
+ end
+ end
+
+ assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options|
+ actions.each do |action, method|
+ assert_recognizes(options.merge(action: action), path: "/threads/1/messages/#{action}", method: method)
+ end
+ end
+
+ assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do
+ actions.each_key do |action|
+ assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", action: action
+ end
+ end
+ end
+ end
+
+ def test_with_collection_action_and_name_prefix_and_formatted
+ actions = { "a" => :get, "b" => :put, "c" => :post, "d" => :delete, "e" => :patch }
+
+ with_routing do |set|
+ set.draw do
+ scope "/threads/:thread_id" do
+ resources :messages, as: "thread_messages" do
+ get :a, on: :collection
+ put :b, on: :collection
+ post :c, on: :collection
+ delete :d, on: :collection
+ patch :e, on: :collection
+ end
+ end
+ end
+
+ assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options|
+ actions.each do |action, method|
+ assert_recognizes(options.merge(action: action, format: "xml"), path: "/threads/1/messages/#{action}.xml", method: method)
+ end
+ end
+
+ assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do
+ actions.each_key do |action|
+ assert_named_route "/threads/1/messages/#{action}.xml", "#{action}_thread_messages_path", action: action, format: "xml"
+ end
+ end
+ end
+ end
+
+ def test_with_member_action
+ [:patch, :put, :post].each do |method|
+ with_restful_routing :messages, member: { mark: method } do
+ mark_options = { action: "mark", id: "1" }
+ mark_path = "/messages/1/mark"
+ assert_restful_routes_for :messages do |options|
+ assert_recognizes(options.merge(mark_options), path: mark_path, method: method)
+ end
+
+ assert_restful_named_routes_for :messages do
+ assert_named_route mark_path, :mark_message_path, mark_options
+ end
+ end
+ end
+ end
+
+ def test_with_member_action_and_requirement
+ expected_options = { controller: "messages", action: "mark", id: "1.1.1" }
+
+ with_restful_routing(:messages, constraints: { id: /[0-9]\.[0-9]\.[0-9]/ }, member: { mark: :get }) do
+ assert_recognizes(expected_options, path: "messages/1.1.1/mark", method: :get)
+ end
+ end
+
+ def test_member_when_override_paths_for_default_restful_actions_with
+ [:patch, :put, :post].each do |method|
+ with_restful_routing :messages, member: { mark: method }, path_names: { new: "nuevo" } do
+ mark_options = { action: "mark", id: "1", controller: "messages" }
+ mark_path = "/messages/1/mark"
+
+ assert_restful_routes_for :messages, path_names: { new: "nuevo" } do |options|
+ assert_recognizes(options.merge(mark_options), path: mark_path, method: method)
+ end
+
+ assert_restful_named_routes_for :messages, path_names: { new: "nuevo" } do
+ assert_named_route mark_path, :mark_message_path, mark_options
+ end
+ end
+ end
+ end
+
+ def test_with_two_member_actions_with_same_method
+ [:patch, :put, :post].each do |method|
+ with_routing do |set|
+ set.draw do
+ resources :messages do
+ member do
+ match :mark, via: method
+ match :unmark, via: method
+ end
+ end
+ end
+
+ %w(mark unmark).each do |action|
+ action_options = { action: action, id: "1" }
+ action_path = "/messages/1/#{action}"
+ assert_restful_routes_for :messages do |options|
+ assert_recognizes(options.merge(action_options), path: action_path, method: method)
+ end
+
+ assert_restful_named_routes_for :messages do
+ assert_named_route action_path, "#{action}_message_path".to_sym, action_options
+ end
+ end
+ end
+ end
+ end
+
+ def test_array_as_collection_or_member_method_value
+ with_routing do |set|
+ set.draw do
+ resources :messages do
+ collection do
+ match :search, via: [:post, :get]
+ end
+
+ member do
+ match :toggle, via: [:post, :get]
+ end
+ end
+ end
+
+ assert_restful_routes_for :messages do |options|
+ [:get, :post].each do |method|
+ assert_recognizes(options.merge(action: "search"), path: "/messages/search", method: method)
+ end
+ [:get, :post].each do |method|
+ assert_recognizes(options.merge(action: "toggle", id: "1"), path: "/messages/1/toggle", method: method)
+ end
+ end
+ end
+ end
+
+ def test_with_new_action
+ with_routing do |set|
+ set.draw do
+ resources :messages do
+ post :preview, on: :new
+ end
+ end
+
+ preview_options = { action: "preview" }
+ preview_path = "/messages/new/preview"
+ assert_restful_routes_for :messages do |options|
+ assert_recognizes(options.merge(preview_options), path: preview_path, method: :post)
+ end
+
+ assert_restful_named_routes_for :messages do
+ assert_named_route preview_path, :preview_new_message_path, preview_options
+ end
+ end
+ end
+
+ def test_with_new_action_with_name_prefix
+ with_routing do |set|
+ set.draw do
+ scope("/threads/:thread_id") do
+ resources :messages, as: "thread_messages" do
+ post :preview, on: :new
+ end
+ end
+ end
+
+ preview_options = { action: "preview", thread_id: "1" }
+ preview_path = "/threads/1/messages/new/preview"
+ assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options|
+ assert_recognizes(options.merge(preview_options), path: preview_path, method: :post)
+ end
+
+ assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do
+ assert_named_route preview_path, :preview_new_thread_message_path, preview_options
+ end
+ end
+ end
+
+ def test_with_formatted_new_action_with_name_prefix
+ with_routing do |set|
+ set.draw do
+ scope("/threads/:thread_id") do
+ resources :messages, as: "thread_messages" do
+ post :preview, on: :new
+ end
+ end
+ end
+
+ preview_options = { action: "preview", thread_id: "1", format: "xml" }
+ preview_path = "/threads/1/messages/new/preview.xml"
+ assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options|
+ assert_recognizes(options.merge(preview_options), path: preview_path, method: :post)
+ end
+
+ assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do
+ assert_named_route preview_path, :preview_new_thread_message_path, preview_options
+ end
+ end
+ end
+
+ def test_override_new_method
+ with_restful_routing :messages do
+ assert_restful_routes_for :messages do |options|
+ assert_recognizes(options.merge(action: "new"), path: "/messages/new", method: :get)
+ assert_raise(ActionController::RoutingError) do
+ @routes.recognize_path("/messages/new", method: :post)
+ end
+ end
+ end
+
+ with_routing do |set|
+ set.draw do
+ resources :messages do
+ match :new, via: [:post, :get], on: :new
+ end
+ end
+
+ assert_restful_routes_for :messages do |options|
+ assert_recognizes(options.merge(action: "new"), path: "/messages/new", method: :post)
+ assert_recognizes(options.merge(action: "new"), path: "/messages/new", method: :get)
+ end
+ end
+ end
+
+ def test_nested_restful_routes
+ with_routing do |set|
+ set.draw do
+ resources :threads do
+ resources :messages do
+ resources :comments
+ end
+ end
+ end
+
+ assert_simply_restful_for :threads
+ assert_simply_restful_for :messages,
+ name_prefix: "thread_",
+ path_prefix: "threads/1/",
+ options: { thread_id: "1" }
+ assert_simply_restful_for :comments,
+ name_prefix: "thread_message_",
+ path_prefix: "threads/1/messages/2/",
+ options: { thread_id: "1", message_id: "2" }
+ end
+ end
+
+ def test_shallow_nested_restful_routes
+ with_routing do |set|
+ set.draw do
+ resources :threads, shallow: true do
+ resources :messages do
+ resources :comments
+ end
+ end
+ end
+
+ assert_simply_restful_for :threads,
+ shallow: true
+ assert_simply_restful_for :messages,
+ name_prefix: "thread_",
+ path_prefix: "threads/1/",
+ shallow: true,
+ options: { thread_id: "1" }
+ assert_simply_restful_for :comments,
+ name_prefix: "message_",
+ path_prefix: "messages/2/",
+ shallow: true,
+ options: { message_id: "2" }
+ end
+ end
+
+ def test_shallow_nested_restful_routes_with_namespaces
+ with_routing do |set|
+ set.draw do
+ namespace :backoffice do
+ namespace :admin do
+ resources :products, shallow: true do
+ resources :images
+ end
+ end
+ end
+ end
+
+ assert_simply_restful_for :products,
+ controller: "backoffice/admin/products",
+ namespace: "backoffice/admin/",
+ name_prefix: "backoffice_admin_",
+ path_prefix: "backoffice/admin/",
+ shallow: true
+ assert_simply_restful_for :images,
+ controller: "backoffice/admin/images",
+ namespace: "backoffice/admin/",
+ name_prefix: "backoffice_admin_product_",
+ path_prefix: "backoffice/admin/products/1/",
+ shallow: true,
+ options: { product_id: "1" }
+ end
+ end
+
+ def test_restful_routes_dont_generate_duplicates
+ with_restful_routing :messages do
+ routes = @routes.routes
+ routes.each do |route|
+ routes.each do |r|
+ next if route == r # skip the comparison instance
+ assert_not_equal [route.conditions, route.path.spec.to_s, route.verb], [r.conditions, r.path.spec.to_s, r.verb]
+ end
+ end
+ end
+ end
+
+ def test_should_create_singleton_resource_routes
+ with_singleton_resources :account do
+ assert_singleton_restful_for :account
+ end
+ end
+
+ def test_should_create_multiple_singleton_resource_routes
+ with_singleton_resources :account, :product do
+ assert_singleton_restful_for :account
+ assert_singleton_restful_for :product
+ end
+ end
+
+ def test_should_create_nested_singleton_resource_routes
+ with_routing do |set|
+ set.draw do
+ resource :admin, controller: "admin" do
+ resource :account
+ end
+ end
+
+ assert_singleton_restful_for :admin, controller: "admin"
+ assert_singleton_restful_for :account, name_prefix: "admin_", path_prefix: "admin/"
+ end
+ end
+
+ def test_singleton_resource_with_member_action
+ [:patch, :put, :post].each do |method|
+ with_routing do |set|
+ set.draw do
+ resource :account do
+ match :reset, on: :member, via: method
+ end
+ end
+
+ reset_options = { action: "reset" }
+ reset_path = "/account/reset"
+ assert_singleton_routes_for :account do |options|
+ assert_recognizes(options.merge(reset_options), path: reset_path, method: method)
+ end
+
+ assert_singleton_named_routes_for :account do
+ assert_named_route reset_path, :reset_account_path, reset_options
+ end
+ end
+ end
+ end
+
+ def test_singleton_resource_with_two_member_actions_with_same_method
+ [:patch, :put, :post].each do |method|
+ with_routing do |set|
+ set.draw do
+ resource :account do
+ match :reset, on: :member, via: method
+ match :disable, on: :member, via: method
+ end
+ end
+
+ %w(reset disable).each do |action|
+ action_options = { action: action }
+ action_path = "/account/#{action}"
+ assert_singleton_routes_for :account do |options|
+ assert_recognizes(options.merge(action_options), path: action_path, method: method)
+ end
+
+ assert_singleton_named_routes_for :account do
+ assert_named_route action_path, "#{action}_account_path".to_sym, action_options
+ end
+ end
+ end
+ end
+ end
+
+ def test_should_nest_resources_in_singleton_resource
+ with_routing do |set|
+ set.draw do
+ resource :account do
+ resources :messages
+ end
+ end
+
+ assert_singleton_restful_for :account
+ assert_simply_restful_for :messages, name_prefix: "account_", path_prefix: "account/"
+ end
+ end
+
+ def test_should_nest_resources_in_singleton_resource_with_path_scope
+ with_routing do |set|
+ set.draw do
+ scope ":site_id" do
+ resource(:account) do
+ resources :messages
+ end
+ end
+ end
+
+ assert_singleton_restful_for :account, path_prefix: "7/", options: { site_id: "7" }
+ assert_simply_restful_for :messages, name_prefix: "account_", path_prefix: "7/account/", options: { site_id: "7" }
+ end
+ end
+
+ def test_should_nest_singleton_resource_in_resources
+ with_routing do |set|
+ set.draw do
+ resources :threads do
+ resource :admin, controller: "admin"
+ end
+ end
+
+ assert_simply_restful_for :threads
+ assert_singleton_restful_for :admin, controller: "admin", name_prefix: "thread_", path_prefix: "threads/5/", options: { thread_id: "5" }
+ end
+ end
+
+ def test_should_not_allow_delete_or_patch_or_put_on_collection_path
+ controller_name = :messages
+ with_restful_routing controller_name do
+ options = { controller: controller_name.to_s }
+ collection_path = "/#{controller_name}"
+
+ assert_raise(Assertion) do
+ assert_recognizes(options.merge(action: "update"), path: collection_path, method: :patch)
+ end
+
+ assert_raise(Assertion) do
+ assert_recognizes(options.merge(action: "update"), path: collection_path, method: :put)
+ end
+
+ assert_raise(Assertion) do
+ assert_recognizes(options.merge(action: "destroy"), path: collection_path, method: :delete)
+ end
+ end
+ end
+
+ def test_new_style_named_routes_for_resource
+ with_routing do |set|
+ set.draw do
+ scope "/threads/:thread_id" do
+ resources :messages, as: "thread_messages" do
+ get :search, on: :collection
+ get :preview, on: :new
+ end
+ end
+ end
+
+ assert_simply_restful_for :messages, name_prefix: "thread_", path_prefix: "threads/1/", options: { thread_id: "1" }
+ assert_named_route "/threads/1/messages/search", "search_thread_messages_path", {}
+ assert_named_route "/threads/1/messages/new", "new_thread_message_path", {}
+ assert_named_route "/threads/1/messages/new/preview", "preview_new_thread_message_path", {}
+ end
+ end
+
+ def test_new_style_named_routes_for_singleton_resource
+ with_routing do |set|
+ set.draw do
+ scope "/admin" do
+ resource :account, as: :admin_account do
+ get :login, on: :member
+ get :preview, on: :new
+ end
+ end
+ end
+ assert_singleton_restful_for :account, name_prefix: "admin_", path_prefix: "admin/"
+ assert_named_route "/admin/account/login", "login_admin_account_path", {}
+ assert_named_route "/admin/account/new", "new_admin_account_path", {}
+ assert_named_route "/admin/account/new/preview", "preview_new_admin_account_path", {}
+ end
+ end
+
+ def test_resources_in_namespace
+ with_routing do |set|
+ set.draw do
+ namespace :backoffice do
+ resources :products
+ end
+ end
+
+ assert_simply_restful_for :products, controller: "backoffice/products", name_prefix: "backoffice_", path_prefix: "backoffice/"
+ end
+ end
+
+ def test_resources_in_nested_namespace
+ with_routing do |set|
+ set.draw do
+ namespace :backoffice do
+ namespace :admin do
+ resources :products
+ end
+ end
+ end
+
+ assert_simply_restful_for :products, controller: "backoffice/admin/products", name_prefix: "backoffice_admin_", path_prefix: "backoffice/admin/"
+ end
+ end
+
+ def test_resources_using_namespace
+ with_routing do |set|
+ set.draw do
+ namespace :backoffice, path: nil, as: nil do
+ resources :products
+ end
+ end
+
+ assert_simply_restful_for :products, controller: "backoffice/products"
+ end
+ end
+
+ def test_nested_resources_using_namespace
+ with_routing do |set|
+ set.draw do
+ namespace :backoffice do
+ resources :products do
+ resources :images
+ end
+ end
+ end
+
+ assert_simply_restful_for :images, controller: "backoffice/images", name_prefix: "backoffice_product_", path_prefix: "backoffice/products/1/", options: { product_id: "1" }
+ end
+ end
+
+ def test_nested_resources_in_nested_namespace
+ with_routing do |set|
+ set.draw do
+ namespace :backoffice do
+ namespace :admin do
+ resources :products do
+ resources :images
+ end
+ end
+ end
+ end
+
+ assert_simply_restful_for :images, controller: "backoffice/admin/images", name_prefix: "backoffice_admin_product_", path_prefix: "backoffice/admin/products/1/", options: { product_id: "1" }
+ end
+ end
+
+ def test_with_path_segment
+ with_restful_routing :messages do
+ assert_simply_restful_for :messages
+ assert_recognizes({ controller: "messages", action: "index" }, "/messages")
+ assert_recognizes({ controller: "messages", action: "index" }, "/messages/")
+ end
+
+ with_routing do |set|
+ set.draw do
+ resources :messages, path: "reviews"
+ end
+ assert_simply_restful_for :messages, as: "reviews"
+ assert_recognizes({ controller: "messages", action: "index" }, "/reviews")
+ assert_recognizes({ controller: "messages", action: "index" }, "/reviews/")
+ end
+ end
+
+ def test_multiple_with_path_segment_and_controller
+ with_routing do |set|
+ set.draw do
+ resources :products do
+ resources :product_reviews, path: "reviews", controller: "messages"
+ end
+ resources :tutors do
+ resources :tutor_reviews, path: "reviews", controller: "comments"
+ end
+ end
+
+ assert_simply_restful_for :product_reviews, controller: "messages", as: "reviews", name_prefix: "product_", path_prefix: "products/1/", options: { product_id: "1" }
+ assert_simply_restful_for :tutor_reviews, controller: "comments", as: "reviews", name_prefix: "tutor_", path_prefix: "tutors/1/", options: { tutor_id: "1" }
+ end
+ end
+
+ def test_with_path_segment_path_prefix_constraints
+ expected_options = { controller: "messages", action: "show", thread_id: "1.1.1", id: "1" }
+ with_routing do |set|
+ set.draw do
+ scope "/thread/:thread_id", constraints: { thread_id: /[0-9]\.[0-9]\.[0-9]/ } do
+ resources :messages, path: "comments"
+ end
+ end
+ assert_recognizes(expected_options, path: "thread/1.1.1/comments/1", method: :get)
+ end
+ end
+
+ def test_resource_has_only_show_action
+ with_routing do |set|
+ set.draw do
+ resources :products, only: :show
+ end
+
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ end
+ end
+
+ def test_singleton_resource_has_only_show_action
+ with_routing do |set|
+ set.draw do
+ resource :account, only: :show
+ end
+
+ assert_singleton_resource_allowed_routes("accounts", {}, :show, [:index, :new, :create, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ end
+ end
+
+ def test_resource_does_not_have_destroy_action
+ with_routing do |set|
+ set.draw do
+ resources :products, except: :destroy
+ end
+
+ assert_resource_allowed_routes("products", {}, { id: "1" }, [:index, :new, :create, :show, :edit, :update], :destroy)
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, [:index, :new, :create, :show, :edit, :update], :destroy)
+ end
+ end
+
+ def test_singleton_resource_does_not_have_destroy_action
+ with_routing do |set|
+ set.draw do
+ resource :account, except: :destroy
+ end
+
+ assert_singleton_resource_allowed_routes("accounts", {}, [:new, :create, :show, :edit, :update], :destroy)
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, [:new, :create, :show, :edit, :update], :destroy)
+ end
+ end
+
+ def test_resource_has_show_action_but_does_not_have_destroy_action
+ with_routing do |set|
+ set.draw do
+ resources :products, only: [:show, :destroy], except: :destroy
+ end
+
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy])
+ end
+ end
+
+ def test_singleton_resource_has_show_action_but_does_not_have_destroy_action
+ with_routing do |set|
+ set.draw do
+ resource :account, only: [:show, :destroy], except: :destroy
+ end
+
+ assert_singleton_resource_allowed_routes("accounts", {}, :show, [:new, :create, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :show, [:new, :create, :edit, :update, :destroy])
+ end
+ end
+
+ def test_resource_has_only_create_action_and_named_route
+ with_routing do |set|
+ set.draw do
+ resources :products, only: :create
+ end
+
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :create, [:index, :new, :show, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :create, [:index, :new, :show, :edit, :update, :destroy])
+
+ assert_not_nil set.named_routes[:products]
+ end
+ end
+
+ def test_resource_has_only_update_action_and_named_route
+ with_routing do |set|
+ set.draw do
+ resources :products, only: :update
+ end
+
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :update, [:index, :new, :create, :show, :edit, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :update, [:index, :new, :create, :show, :edit, :destroy])
+
+ assert_not_nil set.named_routes[:product]
+ end
+ end
+
+ def test_resource_has_only_destroy_action_and_named_route
+ with_routing do |set|
+ set.draw do
+ resources :products, only: :destroy
+ end
+
+ assert_resource_allowed_routes("products", {}, { id: "1" }, :destroy, [:index, :new, :create, :show, :edit, :update])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :destroy, [:index, :new, :create, :show, :edit, :update])
+
+ assert_not_nil set.named_routes[:product]
+ end
+ end
+
+ def test_singleton_resource_has_only_create_action_and_named_route
+ with_routing do |set|
+ set.draw do
+ resource :account, only: :create
+ end
+
+ assert_singleton_resource_allowed_routes("accounts", {}, :create, [:new, :show, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :create, [:new, :show, :edit, :update, :destroy])
+
+ assert_not_nil set.named_routes[:account]
+ end
+ end
+
+ def test_singleton_resource_has_only_update_action_and_named_route
+ with_routing do |set|
+ set.draw do
+ resource :account, only: :update
+ end
+
+ assert_singleton_resource_allowed_routes("accounts", {}, :update, [:new, :create, :show, :edit, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :update, [:new, :create, :show, :edit, :destroy])
+
+ assert_not_nil set.named_routes[:account]
+ end
+ end
+
+ def test_singleton_resource_has_only_destroy_action_and_named_route
+ with_routing do |set|
+ set.draw do
+ resource :account, only: :destroy
+ end
+
+ assert_singleton_resource_allowed_routes("accounts", {}, :destroy, [:new, :create, :show, :edit, :update])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :destroy, [:new, :create, :show, :edit, :update])
+
+ assert_not_nil set.named_routes[:account]
+ end
+ end
+
+ def test_resource_has_only_collection_action
+ with_routing do |set|
+ set.draw do
+ resources :products, only: [] do
+ get :sale, on: :collection
+ end
+ end
+
+ assert_resource_allowed_routes("products", {}, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
+
+ assert_recognizes({ controller: "products", action: "sale" }, { path: "products/sale", method: :get })
+ assert_recognizes({ controller: "products", action: "sale", format: "xml" }, { path: "products/sale.xml", method: :get })
+ end
+ end
+
+ def test_resource_has_only_member_action
+ with_routing do |set|
+ set.draw do
+ resources :products, only: [] do
+ get :preview, on: :member
+ end
+ end
+
+ assert_resource_allowed_routes("products", {}, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
+ assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy])
+
+ assert_recognizes({ controller: "products", action: "preview", id: "1" }, { path: "products/1/preview", method: :get })
+ assert_recognizes({ controller: "products", action: "preview", id: "1", format: "xml" }, { path: "products/1/preview.xml", method: :get })
+ end
+ end
+
+ def test_singleton_resource_has_only_member_action
+ with_routing do |set|
+ set.draw do
+ resource :account, only: [] do
+ member do
+ get :signup
+ end
+ end
+ end
+
+ assert_singleton_resource_allowed_routes("accounts", {}, [], [:new, :create, :show, :edit, :update, :destroy])
+ assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, [], [:new, :create, :show, :edit, :update, :destroy])
+
+ assert_recognizes({ controller: "accounts", action: "signup" }, { path: "account/signup", method: :get })
+ assert_recognizes({ controller: "accounts", action: "signup", format: "xml" }, { path: "account/signup.xml", method: :get })
+ end
+ end
+
+ def test_nested_resource_has_only_show_and_member_action
+ with_routing do |set|
+ set.draw do
+ resources :products, only: [:index, :show] do
+ resources :images, only: :show do
+ get :thumbnail, on: :member
+ end
+ end
+ end
+
+ assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, :show, [:index, :new, :create, :edit, :update, :destroy], "products/1/images")
+ assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, :show, [:index, :new, :create, :edit, :update, :destroy], "products/1/images")
+
+ assert_recognizes({ controller: "images", action: "thumbnail", product_id: "1", id: "2" }, { path: "products/1/images/2/thumbnail", method: :get })
+ assert_recognizes({ controller: "images", action: "thumbnail", product_id: "1", id: "2", format: "jpg" }, { path: "products/1/images/2/thumbnail.jpg", method: :get })
+ end
+ end
+
+ def test_nested_resource_does_not_inherit_only_option
+ with_routing do |set|
+ set.draw do
+ resources :products, only: :show do
+ resources :images, except: :destroy
+ end
+ end
+
+ assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update], :destroy, "products/1/images")
+ assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update], :destroy, "products/1/images")
+ end
+ end
+
+ def test_nested_resource_does_not_inherit_only_option_by_default
+ with_routing do |set|
+ set.draw do
+ resources :products, only: :show do
+ resources :images
+ end
+ end
+
+ assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images")
+ assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images")
+ end
+ end
+
+ def test_nested_resource_does_not_inherit_except_option
+ with_routing do |set|
+ set.draw do
+ resources :products, except: :show do
+ resources :images, only: :destroy
+ end
+ end
+
+ assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, :destroy, [:index, :new, :create, :show, :edit, :update], "products/1/images")
+ assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, :destroy, [:index, :new, :create, :show, :edit, :update], "products/1/images")
+ end
+ end
+
+ def test_nested_resource_does_not_inherit_except_option_by_default
+ with_routing do |set|
+ set.draw do
+ resources :products, except: :show do
+ resources :images
+ end
+ end
+
+ assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images")
+ assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images")
+ end
+ end
+
+ def test_default_singleton_restful_route_uses_get
+ with_routing do |set|
+ set.draw do
+ resource :product
+ end
+
+ assert_routing "/product", controller: "products", action: "show"
+ assert set.recognize_path("/product", method: :get)
+ end
+ end
+
+ def test_assert_routing_accepts_all_as_a_valid_method
+ with_routing do |set|
+ set.draw do
+ match "/products", to: "products#show", via: :all
+ end
+
+ assert_routing({ method: "all", path: "/products" }, { controller: "products", action: "show" })
+ end
+ end
+
+ def test_assert_routing_fails_when_not_all_http_methods_are_recognized
+ with_routing do |set|
+ set.draw do
+ match "/products", to: "products#show", via: [:get, :post, :put]
+ end
+
+ assert_raises(Minitest::Assertion) do
+ assert_routing({ method: "all", path: "/products" }, { controller: "products", action: "show" })
+ end
+ end
+ end
+
+ def test_singleton_resource_name_is_not_singularized
+ with_singleton_resources(:products) do
+ assert_singleton_restful_for :products
+ end
+ end
+
+ private
+ def with_restful_routing(*args)
+ options = args.extract_options!
+ collection_methods = options.delete(:collection)
+ member_methods = options.delete(:member)
+ path_prefix = options.delete(:path_prefix)
+ args.push(options)
+
+ with_routing do |set|
+ set.draw do
+ scope(path_prefix || "") do
+ resources(*args) do
+ if collection_methods
+ collection do
+ collection_methods.each do |name, method|
+ send(method, name)
+ end
+ end
+ end
+
+ if member_methods
+ member do
+ member_methods.each do |name, method|
+ send(method, name)
+ end
+ end
+ end
+ end
+ end
+ end
+ yield
+ end
+ end
+
+ def with_singleton_resources(*args)
+ with_routing do |set|
+ set.draw { resource(*args) }
+ yield
+ end
+ end
+
+ # runs assert_restful_routes_for and assert_restful_named_routes for on the controller_name and options, without passing a block.
+ def assert_simply_restful_for(controller_name, options = {})
+ assert_restful_routes_for controller_name, options
+ assert_restful_named_routes_for controller_name, nil, options
+ end
+
+ def assert_singleton_restful_for(singleton_name, options = {})
+ assert_singleton_routes_for singleton_name, options
+ assert_singleton_named_routes_for singleton_name, options
+ end
+
+ def assert_restful_routes_for(controller_name, options = {})
+ route_options = (options[:options] ||= {}).dup
+ route_options[:controller] = options[:controller] || controller_name.to_s
+
+ if options[:shallow]
+ options[:shallow_options] ||= {}
+ options[:shallow_options][:controller] = route_options[:controller]
+ else
+ options[:shallow_options] = route_options
+ end
+
+ new_action = @routes.resources_path_names[:new] || "new"
+ edit_action = @routes.resources_path_names[:edit] || "edit"
+
+ if options[:path_names]
+ new_action = options[:path_names][:new] if options[:path_names][:new]
+ edit_action = options[:path_names][:edit] if options[:path_names][:edit]
+ end
+
+ path = "#{options[:as] || controller_name}"
+ collection_path = "/#{options[:path_prefix]}#{path}"
+ shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}"
+ member_path = "#{shallow_path}/1"
+ new_path = "#{collection_path}/#{new_action}"
+ edit_member_path = "#{member_path}/#{edit_action}"
+ formatted_edit_member_path = "#{member_path}/#{edit_action}.xml"
+
+ with_options(route_options) do |controller|
+ controller.assert_routing collection_path, action: "index"
+ controller.assert_routing new_path, action: "new"
+ controller.assert_routing "#{collection_path}.xml", action: "index", format: "xml"
+ controller.assert_routing "#{new_path}.xml", action: "new", format: "xml"
+ end
+
+ with_options(options[:shallow_options]) do |controller|
+ controller.assert_routing member_path, action: "show", id: "1"
+ controller.assert_routing edit_member_path, action: "edit", id: "1"
+ controller.assert_routing "#{member_path}.xml", action: "show", id: "1", format: "xml"
+ controller.assert_routing formatted_edit_member_path, action: "edit", id: "1", format: "xml"
+ end
+
+ assert_recognizes(route_options.merge(action: "index"), path: collection_path, method: :get)
+ assert_recognizes(route_options.merge(action: "new"), path: new_path, method: :get)
+ assert_recognizes(route_options.merge(action: "create"), path: collection_path, method: :post)
+ assert_recognizes(options[:shallow_options].merge(action: "show", id: "1"), path: member_path, method: :get)
+ assert_recognizes(options[:shallow_options].merge(action: "edit", id: "1"), path: edit_member_path, method: :get)
+ assert_recognizes(options[:shallow_options].merge(action: "update", id: "1"), path: member_path, method: :put)
+ assert_recognizes(options[:shallow_options].merge(action: "destroy", id: "1"), path: member_path, method: :delete)
+
+ assert_recognizes(route_options.merge(action: "index", format: "xml"), path: "#{collection_path}.xml", method: :get)
+ assert_recognizes(route_options.merge(action: "new", format: "xml"), path: "#{new_path}.xml", method: :get)
+ assert_recognizes(route_options.merge(action: "create", format: "xml"), path: "#{collection_path}.xml", method: :post)
+ assert_recognizes(options[:shallow_options].merge(action: "show", id: "1", format: "xml"), path: "#{member_path}.xml", method: :get)
+ assert_recognizes(options[:shallow_options].merge(action: "edit", id: "1", format: "xml"), path: formatted_edit_member_path, method: :get)
+ assert_recognizes(options[:shallow_options].merge(action: "update", id: "1", format: "xml"), path: "#{member_path}.xml", method: :put)
+ assert_recognizes(options[:shallow_options].merge(action: "destroy", id: "1", format: "xml"), path: "#{member_path}.xml", method: :delete)
+
+ yield route_options if block_given?
+ end
+
+ # test named routes like foo_path and foos_path map to the correct options.
+ def assert_restful_named_routes_for(controller_name, singular_name = nil, options = {})
+ if singular_name.is_a?(Hash)
+ options = singular_name
+ singular_name = nil
+ end
+ singular_name ||= controller_name.to_s.singularize
+
+ route_options = (options[:options] ||= {}).dup
+ route_options[:controller] = options[:controller] || controller_name.to_s
+
+ if options[:shallow]
+ options[:shallow_options] ||= {}
+ options[:shallow_options][:controller] = route_options[:controller]
+ else
+ options[:shallow_options] = route_options
+ end
+
+ @controller = "#{route_options[:controller].camelize}Controller".constantize.new
+ @controller.singleton_class.include(@routes.url_helpers)
+ get :index, params: route_options
+ route_options.delete :action
+
+ path = "#{options[:as] || controller_name}"
+ shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}"
+ full_path = "/#{options[:path_prefix]}#{path}"
+ name_prefix = options[:name_prefix]
+ shallow_prefix = options[:shallow] ? options[:namespace].try(:gsub, /\//, "_") : options[:name_prefix]
+
+ new_action = "new"
+ edit_action = "edit"
+ if options[:path_names]
+ new_action = options[:path_names][:new] || "new"
+ edit_action = options[:path_names][:edit] || "edit"
+ end
+
+ assert_named_route "#{full_path}", "#{name_prefix}#{controller_name}_path", route_options
+ assert_named_route "#{full_path}.xml", "#{name_prefix}#{controller_name}_path", route_options.merge(format: "xml")
+ assert_named_route "#{shallow_path}/1", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1")
+ assert_named_route "#{shallow_path}/1.xml", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1", format: "xml")
+
+ assert_named_route "#{full_path}/#{new_action}", "new_#{name_prefix}#{singular_name}_path", route_options
+ assert_named_route "#{full_path}/#{new_action}.xml", "new_#{name_prefix}#{singular_name}_path", route_options.merge(format: "xml")
+ assert_named_route "#{shallow_path}/1/#{edit_action}", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1")
+ assert_named_route "#{shallow_path}/1/#{edit_action}.xml", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1", format: "xml")
+
+ yield route_options if block_given?
+ end
+
+ def assert_singleton_routes_for(singleton_name, options = {})
+ route_options = (options[:options] ||= {}).dup
+ route_options[:controller] = options[:controller] || singleton_name.to_s.pluralize
+
+ full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}"
+ new_path = "#{full_path}/new"
+ edit_path = "#{full_path}/edit"
+ formatted_edit_path = "#{full_path}/edit.xml"
+
+ with_options route_options do |controller|
+ controller.assert_routing full_path, action: "show"
+ controller.assert_routing new_path, action: "new"
+ controller.assert_routing edit_path, action: "edit"
+ controller.assert_routing "#{full_path}.xml", action: "show", format: "xml"
+ controller.assert_routing "#{new_path}.xml", action: "new", format: "xml"
+ controller.assert_routing formatted_edit_path, action: "edit", format: "xml"
+ end
+
+ assert_recognizes(route_options.merge(action: "show"), path: full_path, method: :get)
+ assert_recognizes(route_options.merge(action: "new"), path: new_path, method: :get)
+ assert_recognizes(route_options.merge(action: "edit"), path: edit_path, method: :get)
+ assert_recognizes(route_options.merge(action: "create"), path: full_path, method: :post)
+ assert_recognizes(route_options.merge(action: "update"), path: full_path, method: :put)
+ assert_recognizes(route_options.merge(action: "destroy"), path: full_path, method: :delete)
+
+ assert_recognizes(route_options.merge(action: "show", format: "xml"), path: "#{full_path}.xml", method: :get)
+ assert_recognizes(route_options.merge(action: "new", format: "xml"), path: "#{new_path}.xml", method: :get)
+ assert_recognizes(route_options.merge(action: "edit", format: "xml"), path: formatted_edit_path, method: :get)
+ assert_recognizes(route_options.merge(action: "create", format: "xml"), path: "#{full_path}.xml", method: :post)
+ assert_recognizes(route_options.merge(action: "update", format: "xml"), path: "#{full_path}.xml", method: :put)
+ assert_recognizes(route_options.merge(action: "destroy", format: "xml"), path: "#{full_path}.xml", method: :delete)
+
+ yield route_options if block_given?
+ end
+
+ def assert_singleton_named_routes_for(singleton_name, options = {})
+ route_options = (options[:options] ||= {}).dup
+ controller_name = route_options[:controller] || options[:controller] || singleton_name.to_s.pluralize
+ @controller = "#{controller_name.camelize}Controller".constantize.new
+ @controller.singleton_class.include(@routes.url_helpers)
+ get :show, params: route_options
+ route_options.delete :action
+
+ full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}"
+ name_prefix = options[:name_prefix]
+
+ assert_named_route "#{full_path}", "#{name_prefix}#{singleton_name}_path", route_options
+ assert_named_route "#{full_path}.xml", "#{name_prefix}#{singleton_name}_path", route_options.merge(format: "xml")
+
+ assert_named_route "#{full_path}/new", "new_#{name_prefix}#{singleton_name}_path", route_options
+ assert_named_route "#{full_path}/new.xml", "new_#{name_prefix}#{singleton_name}_path", route_options.merge(format: "xml")
+ assert_named_route "#{full_path}/edit", "edit_#{name_prefix}#{singleton_name}_path", route_options
+ assert_named_route "#{full_path}/edit.xml", "edit_#{name_prefix}#{singleton_name}_path", route_options.merge(format: "xml")
+ end
+
+ def assert_named_route(expected, route, options)
+ actual = @controller.send(route, options) rescue $!.class.name
+ assert_equal expected, actual, "Error on route: #{route}(#{options.inspect})"
+ end
+
+ def assert_resource_methods(expected, resource, action_method, method)
+ assert_equal expected.length, resource.send("#{action_method}_methods")[method].size, "#{resource.send("#{action_method}_methods")[method].inspect}"
+ expected.each do |action|
+ assert_includes resource.send("#{action_method}_methods")[method], action,
+ "#{method} not in #{action_method} methods: #{resource.send("#{action_method}_methods")[method].inspect}"
+ end
+ end
+
+ def assert_resource_allowed_routes(controller, options, shallow_options, allowed, not_allowed, path = controller)
+ shallow_path = "#{path}/#{shallow_options[:id]}"
+ format = options[:format] && ".#{options[:format]}"
+ options[:controller] = controller
+ shallow_options.merge!(options)
+
+ assert_whether_allowed(allowed, not_allowed, options, "index", "#{path}#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, options, "new", "#{path}/new#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, options, "create", "#{path}#{format}", :post)
+ assert_whether_allowed(allowed, not_allowed, shallow_options, "show", "#{shallow_path}#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, shallow_options, "edit", "#{shallow_path}/edit#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, shallow_options, "update", "#{shallow_path}#{format}", :put)
+ assert_whether_allowed(allowed, not_allowed, shallow_options, "destroy", "#{shallow_path}#{format}", :delete)
+ end
+
+ def assert_singleton_resource_allowed_routes(controller, options, allowed, not_allowed, path = controller.singularize)
+ format = options[:format] && ".#{options[:format]}"
+ options[:controller] = controller
+
+ assert_whether_allowed(allowed, not_allowed, options, "new", "#{path}/new#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, options, "create", "#{path}#{format}", :post)
+ assert_whether_allowed(allowed, not_allowed, options, "show", "#{path}#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, options, "edit", "#{path}/edit#{format}", :get)
+ assert_whether_allowed(allowed, not_allowed, options, "update", "#{path}#{format}", :put)
+ assert_whether_allowed(allowed, not_allowed, options, "destroy", "#{path}#{format}", :delete)
+ end
+
+ def assert_whether_allowed(allowed, not_allowed, options, action, path, method)
+ action = action.to_sym
+ options = options.merge(action: action.to_s)
+ path_options = { path: path, method: method }
+
+ if Array(allowed).include?(action)
+ assert_recognizes options, path_options
+ elsif Array(not_allowed).include?(action)
+ assert_not_recognizes options, path_options
+ else
+ raise Assertion, "Invalid Action has passed"
+ end
+ end
+
+ def assert_not_recognizes(expected_options, path)
+ assert_raise Assertion do
+ assert_recognizes(expected_options, path)
+ end
+ end
+end
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
new file mode 100644
index 0000000000..b378bb80b8
--- /dev/null
+++ b/actionpack/test/controller/routing_test.rb
@@ -0,0 +1,2105 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
+require "active_support/core_ext/object/with_options"
+require "active_support/core_ext/object/json"
+
+class MilestonesController < ActionController::Base
+ def index() head :ok end
+ alias_method :show, :index
+end
+
+# See RFC 3986, section 3.3 for allowed path characters.
+class UriReservedCharactersRoutingTest < ActiveSupport::TestCase
+ include RoutingTestHelpers
+
+ def setup
+ @set = ActionDispatch::Routing::RouteSet.new
+ @set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:variable/*additional"
+ end
+ end
+
+ safe, unsafe = %w(: @ & = + $ , ;), %w(^ ? # [ ])
+ hex = unsafe.map { |char| "%" + char.unpack1("H2").upcase }
+
+ @segment = "#{safe.join}#{unsafe.join}"
+ @escaped = "#{safe.join}#{hex.join}"
+ end
+
+ def test_route_generation_escapes_unsafe_path_characters
+ assert_equal "/content/act#{@escaped}ion/var#{@escaped}iable/add#{@escaped}itional-1/add#{@escaped}itional-2",
+ url_for(@set,
+ controller: "content",
+ action: "act#{@segment}ion",
+ variable: "var#{@segment}iable",
+ additional: ["add#{@segment}itional-1", "add#{@segment}itional-2"])
+ end
+
+ def test_route_recognition_unescapes_path_components
+ options = { controller: "content",
+ action: "act#{@segment}ion",
+ variable: "var#{@segment}iable",
+ additional: "add#{@segment}itional-1/add#{@segment}itional-2" }
+ assert_equal options, @set.recognize_path("/content/act#{@escaped}ion/var#{@escaped}iable/add#{@escaped}itional-1/add#{@escaped}itional-2")
+ end
+
+ def test_route_generation_allows_passing_non_string_values_to_generated_helper
+ assert_equal "/content/action/variable/1/2",
+ url_for(@set,
+ controller: "content",
+ action: "action",
+ variable: "variable",
+ additional: [1, 2])
+ end
+end
+
+class MockController
+ def self.build(helpers, additional_options = {})
+ Class.new do
+ define_method :url_options do
+ options = super()
+ options[:protocol] ||= "http"
+ options[:host] ||= "test.host"
+ options.merge(additional_options)
+ end
+
+ include helpers
+ end
+ end
+end
+
+class LegacyRouteSetTests < ActiveSupport::TestCase
+ include RoutingTestHelpers
+ include ActionDispatch::RoutingVerbs
+
+ attr_reader :rs
+ attr_accessor :controller
+ alias :routes :rs
+
+ def setup
+ @rs = make_set
+ @response = nil
+ end
+
+ def test_symbols_with_dashes
+ rs.draw do
+ get "/:artist/:song-omg", to: lambda { |env|
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
+ [200, {}, [resp]]
+ }
+ end
+
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/faithfully-omg"))
+ assert_equal({ "artist" => "journey", "song" => "faithfully" }, hash)
+ end
+
+ def test_id_encoding
+ rs.draw do
+ get "/journey/:id", to: lambda { |env|
+ param = ActionDispatch::Request.new(env).path_parameters
+ resp = ActiveSupport::JSON.encode param
+ [200, {}, [resp]]
+ }
+ end
+
+ # The encoding of the URL in production is *binary*, so we add a
+ # .b here.
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/%E5%A4%AA%E9%83%8E".b))
+ assert_equal({ "id" => "太郎" }, hash)
+ assert_equal ::Encoding::UTF_8, hash["id"].encoding
+ end
+
+ def test_id_with_dash
+ rs.draw do
+ get "/journey/:id", to: lambda { |env|
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
+ [200, {}, [resp]]
+ }
+ end
+
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/faithfully-omg"))
+ assert_equal({ "id" => "faithfully-omg" }, hash)
+ end
+
+ def test_dash_with_custom_regexp
+ rs.draw do
+ get "/:artist/:song-omg", constraints: { song: /\d+/ }, to: lambda { |env|
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
+ [200, {}, [resp]]
+ }
+ end
+
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/123-omg"))
+ assert_equal({ "artist" => "journey", "song" => "123" }, hash)
+ assert_equal "Not Found", get(URI("http://example.org/journey/faithfully-omg"))
+ end
+
+ def test_pre_dash
+ rs.draw do
+ get "/:artist/omg-:song", to: lambda { |env|
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
+ [200, {}, [resp]]
+ }
+ end
+
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/omg-faithfully"))
+ assert_equal({ "artist" => "journey", "song" => "faithfully" }, hash)
+ end
+
+ def test_pre_dash_with_custom_regexp
+ rs.draw do
+ get "/:artist/omg-:song", constraints: { song: /\d+/ }, to: lambda { |env|
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
+ [200, {}, [resp]]
+ }
+ end
+
+ hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/omg-123"))
+ assert_equal({ "artist" => "journey", "song" => "123" }, hash)
+ assert_equal "Not Found", get(URI("http://example.org/journey/omg-faithfully"))
+ end
+
+ def test_star_paths_are_greedy
+ rs.draw do
+ get "/*path", to: lambda { |env|
+ x = env["action_dispatch.request.path_parameters"][:path]
+ [200, {}, [x]]
+ }, format: false
+ end
+
+ u = URI("http://example.org/foo/bar.html")
+ assert_equal u.path.sub(/^\//, ""), get(u)
+ end
+
+ def test_star_paths_are_greedy_but_not_too_much
+ rs.draw do
+ get "/*path", to: lambda { |env|
+ x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"]
+ [200, {}, [x]]
+ }
+ end
+
+ expected = { "path" => "foo/bar", "format" => "html" }
+ u = URI("http://example.org/foo/bar.html")
+ assert_equal expected, ActiveSupport::JSON.decode(get(u))
+ end
+
+ def test_optional_star_paths_are_greedy
+ rs.draw do
+ get "/(*filters)", to: lambda { |env|
+ x = env["action_dispatch.request.path_parameters"][:filters]
+ [200, {}, [x]]
+ }, format: false
+ end
+
+ u = URI("http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794")
+ assert_equal u.path.sub(/^\//, ""), get(u)
+ end
+
+ def test_optional_star_paths_are_greedy_but_not_too_much
+ rs.draw do
+ get "/(*filters)", to: lambda { |env|
+ x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"]
+ [200, {}, [x]]
+ }
+ end
+
+ expected = { "filters" => "ne_27.065938,-80.6092/sw_25.489856,-82",
+ "format" => "542794" }
+ u = URI("http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794")
+ assert_equal expected, ActiveSupport::JSON.decode(get(u))
+ end
+
+ def test_regexp_precedence
+ rs.draw do
+ get "/whois/:domain", constraints: {
+ domain: /\w+\.[\w\.]+/ },
+ to: lambda { |env| [200, {}, %w{regexp}] }
+
+ get "/whois/:id", to: lambda { |env| [200, {}, %w{id}] }
+ end
+
+ assert_equal "regexp", get(URI("http://example.org/whois/example.org"))
+ assert_equal "id", get(URI("http://example.org/whois/123"))
+ end
+
+ def test_class_and_lambda_constraints
+ subdomain = Class.new {
+ def matches?(request)
+ request.subdomain.present? && request.subdomain != "clients"
+ end
+ }
+
+ rs.draw do
+ get "/", constraints: subdomain.new,
+ to: lambda { |env| [200, {}, %w{default}] }
+ get "/", constraints: { subdomain: "clients" },
+ to: lambda { |env| [200, {}, %w{clients}] }
+ end
+
+ assert_equal "default", get(URI("http://www.example.org/"))
+ assert_equal "clients", get(URI("http://clients.example.org/"))
+ end
+
+ def test_lambda_constraints
+ rs.draw do
+ get "/", constraints: lambda { |req|
+ req.subdomain.present? && req.subdomain != "clients" },
+ to: lambda { |env| [200, {}, %w{default}] }
+
+ get "/", constraints: lambda { |req|
+ req.subdomain.present? && req.subdomain == "clients" },
+ to: lambda { |env| [200, {}, %w{clients}] }
+ end
+
+ assert_equal "default", get(URI("http://www.example.org/"))
+ assert_equal "clients", get(URI("http://clients.example.org/"))
+ end
+
+ def test_scoped_lambda
+ scope_called = false
+ rs.draw do
+ scope "/foo", constraints: lambda { |req| scope_called = true } do
+ get "/", to: lambda { |env| [200, {}, %w{default}] }
+ end
+ end
+
+ assert_equal "default", get(URI("http://www.example.org/foo/"))
+ assert scope_called, "scope constraint should be called"
+ end
+
+ def test_scoped_lambda_with_get_lambda
+ inner_called = false
+
+ rs.draw do
+ scope "/foo", constraints: lambda { |req| flunk "should not be called" } do
+ get "/", constraints: lambda { |req| inner_called = true },
+ to: lambda { |env| [200, {}, %w{default}] }
+ end
+ end
+
+ assert_equal "default", get(URI("http://www.example.org/foo/"))
+ assert inner_called, "inner constraint should be called"
+ end
+
+ def test_empty_string_match
+ rs.draw do
+ get "/:username", constraints: { username: /[^\/]+/ },
+ to: lambda { |e| [200, {}, ["foo"]] }
+ end
+ assert_equal "Not Found", get(URI("http://example.org/"))
+ assert_equal "foo", get(URI("http://example.org/hello"))
+ end
+
+ def test_non_greedy_glob_regexp
+ params = nil
+ rs.draw do
+ get "/posts/:id(/*filters)", constraints: { filters: /.+?/ },
+ to: lambda { |e|
+ params = e["action_dispatch.request.path_parameters"]
+ [200, {}, ["foo"]]
+ }
+ end
+ assert_equal "foo", get(URI("http://example.org/posts/1/foo.js"))
+ assert_equal({ id: "1", filters: "foo", format: "js" }, params)
+ end
+
+ def test_specific_controller_action_failure
+ rs.draw do
+ mount lambda { } => "/foo"
+ end
+
+ assert_raises(ActionController::UrlGenerationError) do
+ url_for(rs, controller: "omg", action: "lol")
+ end
+ end
+
+ def test_default_setup
+ rs.draw { ActiveSupport::Deprecation.silence { get "/:controller(/:action(/:id))" } }
+ assert_equal({ controller: "content", action: "index" }, rs.recognize_path("/content"))
+ assert_equal({ controller: "content", action: "list" }, rs.recognize_path("/content/list"))
+ assert_equal({ controller: "content", action: "show", id: "10" }, rs.recognize_path("/content/show/10"))
+
+ assert_equal({ controller: "admin/user", action: "show", id: "10" }, rs.recognize_path("/admin/user/show/10"))
+
+ assert_equal "/admin/user/show/10", url_for(rs, controller: "admin/user", action: "show", id: 10)
+
+ get URI("http://test.host/admin/user/list/10")
+
+ assert_equal({ controller: "admin/user", action: "list", id: "10" },
+ controller.request.path_parameters)
+
+ assert_equal "/admin/user/show", controller.url_for(action: "show", only_path: true)
+ assert_equal "/admin/user/list/10", controller.url_for(only_path: true)
+
+ assert_equal "/admin/stuff", controller.url_for(controller: "stuff", only_path: true)
+ assert_equal "/stuff", controller.url_for(controller: "/stuff", only_path: true)
+ end
+
+ def test_route_with_colon_first
+ rs.draw do
+ ActiveSupport::Deprecation.silence do
+ get "/:controller/:action/:id", action: "index", id: nil
+ end
+
+ get ":url", controller: "content", action: "translate"
+ end
+
+ assert_equal({ controller: "content", action: "translate", url: "example" }, rs.recognize_path("/example"))
+ end
+
+ def test_route_with_regexp_for_action
+ rs.draw { ActiveSupport::Deprecation.silence { get "/:controller/:action", action: /auth[-|_].+/ } }
+
+ assert_equal({ action: "auth_google", controller: "content" }, rs.recognize_path("/content/auth_google"))
+ assert_equal({ action: "auth-twitter", controller: "content" }, rs.recognize_path("/content/auth-twitter"))
+
+ assert_equal "/content/auth_google", url_for(rs, controller: "content", action: "auth_google")
+ assert_equal "/content/auth-twitter", url_for(rs, controller: "content", action: "auth-twitter")
+ end
+
+ def test_route_with_regexp_for_controller
+ rs.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:admintoken(/:action(/:id))", controller: /admin\/.+/
+ get "/:controller(/:action(/:id))"
+ end
+ end
+
+ assert_equal({ controller: "admin/user", admintoken: "foo", action: "index" },
+ rs.recognize_path("/admin/user/foo"))
+ assert_equal({ controller: "content", action: "foo" },
+ rs.recognize_path("/content/foo"))
+
+ assert_equal "/admin/user/foo", url_for(rs, controller: "admin/user", admintoken: "foo", action: "index")
+ assert_equal "/content/foo", url_for(rs, controller: "content", action: "foo")
+ end
+
+ def test_route_with_regexp_and_captures_for_controller
+ rs.draw do
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id))", controller: /admin\/(accounts|users)/
+ end
+ end
+ assert_equal({ controller: "admin/accounts", action: "index" }, rs.recognize_path("/admin/accounts"))
+ assert_equal({ controller: "admin/users", action: "index" }, rs.recognize_path("/admin/users"))
+ assert_raise(ActionController::RoutingError) { rs.recognize_path("/admin/products") }
+ end
+
+ def test_route_with_regexp_and_dot
+ rs.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:file",
+ controller: /admin|user/,
+ action: /upload|download/,
+ defaults: { file: nil },
+ constraints: { file: %r{[^/]+(\.[^/]+)?} }
+ end
+ end
+ # Without a file extension
+ assert_equal "/user/download/file",
+ url_for(rs, controller: "user", action: "download", file: "file")
+
+ assert_equal({ controller: "user", action: "download", file: "file" },
+ rs.recognize_path("/user/download/file"))
+
+ # Now, let's try a file with an extension, really a dot (.)
+ assert_equal "/user/download/file.jpg",
+ url_for(rs, controller: "user", action: "download", file: "file.jpg")
+
+ assert_equal({ controller: "user", action: "download", file: "file.jpg" },
+ rs.recognize_path("/user/download/file.jpg"))
+ end
+
+ def test_basic_named_route
+ rs.draw do
+ root to: "content#list", as: "home"
+ end
+ assert_equal("http://test.host/", setup_for_named_route.send(:home_url))
+ end
+
+ def test_named_route_with_option
+ rs.draw do
+ get "page/:title" => "content#show_page", :as => "page"
+ end
+
+ assert_equal("http://test.host/page/new%20stuff",
+ setup_for_named_route.send(:page_url, title: "new stuff"))
+ end
+
+ def test_named_route_with_default
+ rs.draw do
+ get "page/:title" => "content#show_page", :title => "AboutPage", :as => "page"
+ end
+
+ assert_equal("http://test.host/page/AboutRails",
+ setup_for_named_route.send(:page_url, title: "AboutRails"))
+ end
+
+ def test_named_route_with_path_prefix
+ rs.draw do
+ scope "my" do
+ get "page" => "content#show_page", :as => "page"
+ end
+ end
+
+ assert_equal("http://test.host/my/page",
+ setup_for_named_route.send(:page_url))
+ end
+
+ def test_named_route_with_blank_path_prefix
+ rs.draw do
+ scope "" do
+ get "page" => "content#show_page", :as => "page"
+ end
+ end
+
+ assert_equal("http://test.host/page",
+ setup_for_named_route.send(:page_url))
+ end
+
+ def test_named_route_with_nested_controller
+ rs.draw do
+ get "admin/user" => "admin/user#index", :as => "users"
+ end
+
+ assert_equal("http://test.host/admin/user",
+ setup_for_named_route.send(:users_url))
+ end
+
+ def test_optimised_named_route_with_host
+ rs.draw do
+ get "page" => "content#show_page", :as => "pages", :host => "foo.com"
+ end
+ routes = setup_for_named_route
+ assert_equal "http://foo.com/page", routes.pages_url
+ end
+
+ def setup_for_named_route(options = {})
+ MockController.build(rs.url_helpers, options).new
+ end
+
+ def test_named_route_without_hash
+ rs.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id", as: "normal"
+ end
+ end
+ end
+
+ def test_named_route_root
+ rs.draw do
+ root to: "hello#index"
+ end
+ routes = setup_for_named_route
+ assert_equal("http://test.host/", routes.send(:root_url))
+ assert_equal("/", routes.send(:root_path))
+ end
+
+ def test_named_route_root_without_hash
+ rs.draw do
+ root "hello#index"
+ end
+ routes = setup_for_named_route
+ assert_equal("http://test.host/", routes.send(:root_url))
+ assert_equal("/", routes.send(:root_path))
+ end
+
+ def test_named_route_root_with_hash
+ rs.draw do
+ root "hello#index", as: :index
+ end
+
+ routes = setup_for_named_route
+ assert_equal("http://test.host/", routes.send(:index_url))
+ assert_equal("/", routes.send(:index_path))
+ end
+
+ def test_root_without_path_raises_argument_error
+ assert_raises ArgumentError do
+ rs.draw { root nil }
+ end
+ end
+
+ def test_named_route_root_with_trailing_slash
+ rs.draw do
+ root "hello#index"
+ end
+
+ routes = setup_for_named_route(trailing_slash: true)
+ assert_equal("http://test.host/", routes.send(:root_url))
+ assert_equal("http://test.host/?foo=bar", routes.send(:root_url, foo: :bar))
+ end
+
+ def test_named_route_with_regexps
+ rs.draw do
+ get "page/:year/:month/:day/:title" => "page#show", :as => "article",
+ :year => /\d+/, :month => /\d+/, :day => /\d+/
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+
+ routes = setup_for_named_route
+
+ assert_equal "http://test.host/page/2005/6/10/hi",
+ routes.send(:article_url, title: "hi", day: 10, year: 2005, month: 6)
+ end
+
+ def test_changing_controller
+ rs.draw { ActiveSupport::Deprecation.silence { get ":controller/:action/:id" } }
+
+ get URI("http://test.host/admin/user/index/10")
+
+ assert_equal "/admin/stuff/show/10",
+ controller.url_for(controller: "stuff", action: "show", id: 10, only_path: true)
+ end
+
+ def test_paths_escaped
+ rs.draw do
+ get "file/*path" => "content#show_file", :as => "path"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+
+ # No + to space in URI escaping, only for query params.
+ results = rs.recognize_path "/file/hello+world/how+are+you%3F"
+ assert results, "Recognition should have succeeded"
+ assert_equal "hello+world/how+are+you?", results[:path]
+
+ # Use %20 for space instead.
+ results = rs.recognize_path "/file/hello%20world/how%20are%20you%3F"
+ assert results, "Recognition should have succeeded"
+ assert_equal "hello world/how are you?", results[:path]
+ end
+
+ def test_paths_slashes_unescaped_with_ordered_parameters
+ rs.draw do
+ get "/file/*path" => "content#index", :as => "path"
+ end
+
+ # No / to %2F in URI, only for query params.
+ assert_equal("/file/hello/world", setup_for_named_route.send(:path_path, ["hello", "world"]))
+ end
+
+ def test_non_controllers_cannot_be_matched
+ rs.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+ assert_raise(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") }
+ end
+
+ def test_should_list_options_diff_when_routing_constraints_dont_match
+ rs.draw do
+ get "post/:id" => "post#show", :constraints => { id: /\d+/ }, :as => "post"
+ end
+ assert_raise(ActionController::UrlGenerationError) do
+ url_for(rs, controller: "post", action: "show", bad_param: "foo", use_route: "post")
+ end
+ end
+
+ def test_dynamic_path_allowed
+ rs.draw do
+ get "*path" => "content#show_file"
+ end
+
+ assert_equal "/pages/boo",
+ url_for(rs, controller: "content", action: "show_file", path: %w(pages boo))
+ end
+
+ def test_dynamic_recall_paths_allowed
+ rs.draw do
+ get "*path" => "content#show_file"
+ end
+
+ get URI("http://test.host/pages/boo")
+ assert_equal({ controller: "content", action: "show_file", path: "pages/boo" },
+ controller.request.path_parameters)
+
+ assert_equal "/pages/boo",
+ controller.url_for(only_path: true)
+ end
+
+ def test_backwards
+ rs.draw do
+ ActiveSupport::Deprecation.silence do
+ get "page/:id(/:action)" => "pages#show"
+ get ":controller(/:action(/:id))"
+ end
+ end
+
+ get URI("http://test.host/pages/show")
+ assert_equal "/page/20", controller.url_for(id: 20, only_path: true)
+ assert_equal "/page/20", url_for(rs, controller: "pages", id: 20, action: "show")
+ assert_equal "/pages/boo", url_for(rs, controller: "pages", action: "boo")
+ end
+
+ def test_route_with_integer_default
+ rs.draw do
+ get "page(/:id)" => "content#show_page", :id => 1
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+
+ assert_equal "/page", url_for(rs, controller: "content", action: "show_page")
+ assert_equal "/page", url_for(rs, controller: "content", action: "show_page", id: 1)
+ assert_equal "/page", url_for(rs, controller: "content", action: "show_page", id: "1")
+ assert_equal "/page/10", url_for(rs, controller: "content", action: "show_page", id: 10)
+
+ assert_equal({ controller: "content", action: "show_page", id: 1 }, rs.recognize_path("/page"))
+ assert_equal({ controller: "content", action: "show_page", id: "1" }, rs.recognize_path("/page/1"))
+ assert_equal({ controller: "content", action: "show_page", id: "10" }, rs.recognize_path("/page/10"))
+ end
+
+ # For newer revision
+ def test_route_with_text_default
+ rs.draw do
+ get "page/:id" => "content#show_page", :id => 1
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+
+ assert_equal "/page/foo", url_for(rs, controller: "content", action: "show_page", id: "foo")
+ assert_equal({ controller: "content", action: "show_page", id: "foo" }, rs.recognize_path("/page/foo"))
+
+ token = +"\321\202\320\265\320\272\321\201\321\202" # 'text' in Russian
+ token.force_encoding(Encoding::BINARY)
+ escaped_token = CGI.escape(token)
+
+ assert_equal "/page/" + escaped_token, url_for(rs, controller: "content", action: "show_page", id: token)
+ assert_equal({ controller: "content", action: "show_page", id: token }, rs.recognize_path("/page/#{escaped_token}"))
+ end
+
+ def test_action_expiry
+ rs.draw { ActiveSupport::Deprecation.silence { get ":controller(/:action(/:id))" } }
+ get URI("http://test.host/content/show")
+ assert_equal "/content", controller.url_for(controller: "content", only_path: true)
+ end
+
+ def test_requirement_should_prevent_optional_id
+ rs.draw do
+ get "post/:id" => "post#show", :constraints => { id: /\d+/ }, :as => "post"
+ end
+
+ assert_equal "/post/10", url_for(rs, controller: "post", action: "show", id: 10)
+
+ assert_raise(ActionController::UrlGenerationError) do
+ url_for(rs, controller: "post", action: "show")
+ end
+ end
+
+ def test_both_requirement_and_optional
+ rs.draw do
+ get("test(/:year)" => "post#show", :as => "blog",
+ :defaults => { year: nil },
+ :constraints => { year: /\d{4}/ }
+ )
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+
+ assert_equal "/test", url_for(rs, controller: "post", action: "show")
+ assert_equal "/test", url_for(rs, controller: "post", action: "show", year: nil)
+
+ assert_equal("http://test.host/test", setup_for_named_route.send(:blog_url))
+ end
+
+ def test_set_to_nil_forgets
+ rs.draw do
+ get "pages(/:year(/:month(/:day)))" => "content#list_pages", :month => nil, :day => nil
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+
+ assert_equal "/pages/2005",
+ url_for(rs, controller: "content", action: "list_pages", year: 2005)
+ assert_equal "/pages/2005/6",
+ url_for(rs, controller: "content", action: "list_pages", year: 2005, month: 6)
+ assert_equal "/pages/2005/6/12",
+ url_for(rs, controller: "content", action: "list_pages", year: 2005, month: 6, day: 12)
+
+ get URI("http://test.host/pages/2005/6/12")
+ assert_equal({ controller: "content", action: "list_pages", year: "2005", month: "6", day: "12" },
+ controller.request.path_parameters)
+
+ assert_equal "/pages/2005/6/4",
+ controller.url_for(day: 4, only_path: true)
+
+ assert_equal "/pages/2005/6",
+ controller.url_for(day: nil, only_path: true)
+
+ assert_equal "/pages/2005",
+ controller.url_for(day: nil, month: nil, only_path: true)
+ end
+
+ def test_root_url_generation_with_controller_and_action
+ rs.draw do
+ root to: "content#index"
+ end
+
+ assert_equal "/", url_for(rs, controller: "content", action: "index")
+ assert_equal "/", url_for(rs, controller: "content")
+ end
+
+ def test_named_root_url_generation_with_controller_and_action
+ rs.draw do
+ root to: "content#index", as: "home"
+ end
+
+ assert_equal "/", url_for(rs, controller: "content", action: "index")
+ assert_equal "/", url_for(rs, controller: "content")
+
+ assert_equal("http://test.host/", setup_for_named_route.send(:home_url))
+ end
+
+ def test_named_route_method
+ rs.draw do
+ get "categories" => "content#categories", :as => "categories"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
+ end
+
+ assert_equal "/categories", url_for(rs, controller: "content", action: "categories")
+ assert_equal "/content/hi", url_for(rs, controller: "content", action: "hi")
+ end
+
+ def test_named_routes_array
+ test_named_route_method
+ assert_equal [:categories], rs.named_routes.names
+ end
+
+ def test_nil_defaults
+ rs.draw do
+ get "journal" => "content#list_journal",
+ :date => nil, :user_id => nil
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+
+ assert_equal "/journal", url_for(rs,
+ controller: "content",
+ action: "list_journal",
+ date: nil,
+ user_id: nil)
+ end
+
+ def setup_request_method_routes_for(method)
+ rs.draw do
+ match "/match" => "books##{method}", :via => method.to_sym
+ end
+ end
+
+ %w(GET PATCH POST PUT DELETE).each do |request_method|
+ define_method("test_request_method_recognized_with_#{request_method}") do
+ setup_request_method_routes_for(request_method.downcase)
+ params = rs.recognize_path("/match", method: request_method)
+ assert_equal request_method.downcase, params[:action]
+ end
+ end
+
+ def test_recognize_array_of_methods
+ rs.draw do
+ match "/match" => "books#get_or_post", :via => [:get, :post]
+ put "/match" => "books#not_get_or_post"
+ end
+
+ params = rs.recognize_path("/match", method: :post)
+ assert_equal "get_or_post", params[:action]
+
+ params = rs.recognize_path("/match", method: :put)
+ assert_equal "not_get_or_post", params[:action]
+ end
+
+ def test_subpath_recognized
+ rs.draw do
+ ActiveSupport::Deprecation.silence do
+ get "/books/:id/edit" => "subpath_books#edit"
+ get "/items/:id/:action" => "subpath_books"
+ get "/posts/new/:action" => "subpath_books"
+ get "/posts/:id" => "subpath_books#show"
+ end
+ end
+
+ hash = rs.recognize_path "/books/17/edit"
+ assert_not_nil hash
+ assert_equal %w(subpath_books 17 edit), [hash[:controller], hash[:id], hash[:action]]
+
+ hash = rs.recognize_path "/items/3/complete"
+ assert_not_nil hash
+ assert_equal %w(subpath_books 3 complete), [hash[:controller], hash[:id], hash[:action]]
+
+ hash = rs.recognize_path "/posts/new/preview"
+ assert_not_nil hash
+ assert_equal %w(subpath_books preview), [hash[:controller], hash[:action]]
+
+ hash = rs.recognize_path "/posts/7"
+ assert_not_nil hash
+ assert_equal %w(subpath_books show 7), [hash[:controller], hash[:action], hash[:id]]
+ end
+
+ def test_subpath_generated
+ rs.draw do
+ ActiveSupport::Deprecation.silence do
+ get "/books/:id/edit" => "subpath_books#edit"
+ get "/items/:id/:action" => "subpath_books"
+ get "/posts/new/:action" => "subpath_books"
+ end
+ end
+
+ assert_equal "/books/7/edit", url_for(rs, controller: "subpath_books", id: 7, action: "edit")
+ assert_equal "/items/15/complete", url_for(rs, controller: "subpath_books", id: 15, action: "complete")
+ assert_equal "/posts/new/preview", url_for(rs, controller: "subpath_books", action: "preview")
+ end
+
+ def test_failed_constraints_raises_exception_with_violated_constraints
+ rs.draw do
+ get "foos/:id" => "foos#show", :as => "foo_with_requirement", :constraints => { id: /\d+/ }
+ end
+
+ assert_raise(ActionController::UrlGenerationError) do
+ setup_for_named_route.send(:foo_with_requirement_url, "I am Against the constraints")
+ end
+ end
+
+ def test_routes_changed_correctly_after_clear
+ rs = ::ActionDispatch::Routing::RouteSet.new
+ rs.draw do
+ get "ca" => "ca#aa"
+ get "cb" => "cb#ab"
+ get "cc" => "cc#ac"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ get ":controller/:action/:id.:format"
+ end
+ end
+
+ hash = rs.recognize_path "/cc"
+
+ assert_not_nil hash
+ assert_equal %w(cc ac), [hash[:controller], hash[:action]]
+
+ rs.draw do
+ get "cb" => "cb#ab"
+ get "cc" => "cc#ac"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ get ":controller/:action/:id.:format"
+ end
+ end
+
+ hash = rs.recognize_path "/cc"
+
+ assert_not_nil hash
+ assert_equal %w(cc ac), [hash[:controller], hash[:action]]
+ end
+end
+
+class RouteSetTest < ActiveSupport::TestCase
+ include RoutingTestHelpers
+ include ActionDispatch::RoutingVerbs
+
+ attr_reader :set
+ alias :routes :set
+ attr_accessor :controller
+
+ def setup
+ super
+ @set = make_set
+ end
+
+ def request
+ @request ||= ActionController::TestRequest.new
+ end
+
+ def default_route_set
+ @default_route_set ||= begin
+ set = ActionDispatch::Routing::RouteSet.new
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id))"
+ end
+ end
+ set
+ end
+ end
+
+ def test_generate_extras
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller/(:action(/:id))" } }
+ path, extras = set.generate_extras(controller: "foo", action: "bar", id: 15, this: "hello", that: "world")
+ assert_equal "/foo/bar/15", path
+ assert_equal %w(that this), extras.map(&:to_s).sort
+ end
+
+ def test_extra_keys
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller/:action/:id" } }
+ extras = set.extra_keys(controller: "foo", action: "bar", id: 15, this: "hello", that: "world")
+ assert_equal %w(that this), extras.map(&:to_s).sort
+ end
+
+ def test_generate_extras_not_first
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id.:format"
+ get ":controller/:action/:id"
+ end
+ end
+ path, extras = set.generate_extras(controller: "foo", action: "bar", id: 15, this: "hello", that: "world")
+ assert_equal "/foo/bar/15", path
+ assert_equal %w(that this), extras.map(&:to_s).sort
+ end
+
+ def test_generate_not_first
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id.:format"
+ get ":controller/:action/:id"
+ end
+ end
+ assert_equal "/foo/bar/15?this=hello",
+ url_for(set, controller: "foo", action: "bar", id: 15, this: "hello")
+ end
+
+ def test_extra_keys_not_first
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id.:format"
+ get ":controller/:action/:id"
+ end
+ end
+ extras = set.extra_keys(controller: "foo", action: "bar", id: 15, this: "hello", that: "world")
+ assert_equal %w(that this), extras.map(&:to_s).sort
+ end
+
+ def test_draw
+ assert_equal 0, set.routes.size
+ set.draw do
+ get "/hello/world" => "a#b"
+ end
+ assert_equal 1, set.routes.size
+ end
+
+ def test_draw_symbol_controller_name
+ assert_equal 0, set.routes.size
+ set.draw do
+ get "/users/index" => "users#index"
+ end
+ set.recognize_path("/users/index", method: :get)
+ assert_equal 1, set.routes.size
+ end
+
+ def test_named_draw
+ assert_equal 0, set.routes.size
+ set.draw do
+ get "/hello/world" => "a#b", :as => "hello"
+ end
+ assert_equal 1, set.routes.size
+ assert_equal set.routes.first, set.named_routes[:hello]
+ end
+
+ def test_duplicate_named_route_raises_rather_than_pick_precedence
+ assert_raise ArgumentError do
+ set.draw do
+ get "/hello/world" => "a#b", :as => "hello"
+ get "/hello" => "a#b", :as => "hello"
+ end
+ end
+ end
+
+ def setup_named_route_test
+ set.draw do
+ get "/people(/:id)" => "people#show", :as => "show"
+ get "/people" => "people#index", :as => "index"
+ get "/people/go/:foo/:bar/joe(/:id)" => "people#multi", :as => "multi"
+ get "/admin/users" => "admin/users#index", :as => "users"
+ end
+
+ get URI("http://test.host/people")
+ controller
+ end
+
+ def test_named_route_url_method
+ controller = setup_named_route_test
+
+ assert_equal "http://test.host/people/5", controller.send(:show_url, id: 5)
+ assert_equal "/people/5", controller.send(:show_path, id: 5)
+
+ assert_equal "http://test.host/people", controller.send(:index_url)
+ assert_equal "/people", controller.send(:index_path)
+
+ assert_equal "http://test.host/admin/users", controller.send(:users_url)
+ assert_equal "/admin/users", controller.send(:users_path)
+ end
+
+ def test_named_route_url_method_with_anchor
+ controller = setup_named_route_test
+
+ assert_equal "http://test.host/people/5#location", controller.send(:show_url, id: 5, anchor: "location")
+ assert_equal "/people/5#location", controller.send(:show_path, id: 5, anchor: "location")
+
+ assert_equal "http://test.host/people#location", controller.send(:index_url, anchor: "location")
+ assert_equal "/people#location", controller.send(:index_path, anchor: "location")
+
+ assert_equal "http://test.host/admin/users#location", controller.send(:users_url, anchor: "location")
+ assert_equal "/admin/users#location", controller.send(:users_path, anchor: "location")
+
+ assert_equal "http://test.host/people/go/7/hello/joe/5#location",
+ controller.send(:multi_url, 7, "hello", 5, anchor: "location")
+
+ assert_equal "http://test.host/people/go/7/hello/joe/5?baz=bar#location",
+ controller.send(:multi_url, 7, "hello", 5, baz: "bar", anchor: "location")
+
+ assert_equal "http://test.host/people?baz=bar#location",
+ controller.send(:index_url, baz: "bar", anchor: "location")
+
+ assert_equal "http://test.host/people", controller.send(:index_url, anchor: nil)
+ assert_equal "http://test.host/people", controller.send(:index_url, anchor: false)
+ end
+
+ def test_named_route_url_method_with_port
+ controller = setup_named_route_test
+ assert_equal "http://test.host:8080/people/5", controller.send(:show_url, 5, port: 8080)
+ end
+
+ def test_named_route_url_method_with_host
+ controller = setup_named_route_test
+ assert_equal "http://some.example.com/people/5", controller.send(:show_url, 5, host: "some.example.com")
+ end
+
+ def test_named_route_url_method_with_protocol
+ controller = setup_named_route_test
+ assert_equal "https://test.host/people/5", controller.send(:show_url, 5, protocol: "https")
+ end
+
+ def test_named_route_url_method_with_ordered_parameters
+ controller = setup_named_route_test
+ assert_equal "http://test.host/people/go/7/hello/joe/5",
+ controller.send(:multi_url, 7, "hello", 5)
+ end
+
+ def test_named_route_url_method_with_ordered_parameters_and_hash
+ controller = setup_named_route_test
+ assert_equal "http://test.host/people/go/7/hello/joe/5?baz=bar",
+ controller.send(:multi_url, 7, "hello", 5, baz: "bar")
+ end
+
+ def test_named_route_url_method_with_ordered_parameters_and_empty_hash
+ controller = setup_named_route_test
+ assert_equal "http://test.host/people/go/7/hello/joe/5",
+ controller.send(:multi_url, 7, "hello", 5, {})
+ end
+
+ def test_named_route_url_method_with_no_positional_arguments
+ controller = setup_named_route_test
+ assert_equal "http://test.host/people?baz=bar",
+ controller.send(:index_url, baz: "bar")
+ end
+
+ def test_draw_default_route
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+
+ assert_equal 1, set.routes.size
+
+ assert_equal "/users/show/10", url_for(set, controller: "users", action: "show", id: 10)
+ assert_equal "/users/index/10", url_for(set, controller: "users", id: 10)
+
+ assert_equal({ controller: "users", action: "index", id: "10" }, set.recognize_path("/users/index/10"))
+ assert_equal({ controller: "users", action: "index", id: "10" }, set.recognize_path("/users/index/10/"))
+ end
+
+ def test_route_with_parameter_shell
+ set.draw do
+ get "page/:id" => "pages#show", :id => /\d+/
+
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id))"
+ end
+ end
+
+ assert_equal({ controller: "pages", action: "index" }, request_path_params("/pages"))
+ assert_equal({ controller: "pages", action: "index" }, request_path_params("/pages/index"))
+ assert_equal({ controller: "pages", action: "list" }, request_path_params("/pages/list"))
+
+ assert_equal({ controller: "pages", action: "show", id: "10" }, request_path_params("/pages/show/10"))
+ assert_equal({ controller: "pages", action: "show", id: "10" }, request_path_params("/page/10"))
+ end
+
+ def test_route_constraints_on_request_object_with_anchors_are_valid
+ assert_nothing_raised do
+ set.draw do
+ get "page/:id" => "pages#show", :constraints => { host: /^foo$/ }
+ end
+ end
+ end
+
+ def test_route_constraints_with_anchor_chars_are_invalid
+ assert_raise ArgumentError do
+ set.draw do
+ get "page/:id" => "pages#show", :id => /^\d+/
+ end
+ end
+ assert_raise ArgumentError do
+ set.draw do
+ get "page/:id" => "pages#show", :id => /\A\d+/
+ end
+ end
+ assert_raise ArgumentError do
+ set.draw do
+ get "page/:id" => "pages#show", :id => /\d+$/
+ end
+ end
+ assert_raise ArgumentError do
+ set.draw do
+ get "page/:id" => "pages#show", :id => /\d+\Z/
+ end
+ end
+ assert_raise ArgumentError do
+ set.draw do
+ get "page/:id" => "pages#show", :id => /\d+\z/
+ end
+ end
+ end
+
+ def test_route_constraints_with_options_method_condition_is_valid
+ assert_nothing_raised do
+ set.draw do
+ match "valid/route" => "pages#show", :via => :options
+ end
+ end
+ end
+
+ def test_route_error_with_missing_controller
+ set.draw do
+ get "/people" => "missing#index"
+ end
+
+ assert_raises(ActionController::RoutingError) { request_path_params "/people" }
+ end
+
+ def test_recognize_with_encoded_id_and_regex
+ set.draw do
+ get "page/:id" => "pages#show", :id => /[a-zA-Z0-9\+]+/
+ end
+
+ assert_equal({ controller: "pages", action: "show", id: "10" }, request_path_params("/page/10"))
+ assert_equal({ controller: "pages", action: "show", id: "hello+world" }, request_path_params("/page/hello+world"))
+ end
+
+ def test_recognize_with_http_methods
+ set.draw do
+ get "/people" => "people#index", :as => "people"
+ post "/people" => "people#create"
+ get "/people/:id" => "people#show", :as => "person"
+ put "/people/:id" => "people#update"
+ patch "/people/:id" => "people#update"
+ delete "/people/:id" => "people#destroy"
+ end
+
+ params = request_path_params("/people", method: :get)
+ assert_equal("index", params[:action])
+
+ params = request_path_params("/people", method: :post)
+ assert_equal("create", params[:action])
+
+ params = request_path_params("/people/5", method: :put)
+ assert_equal("update", params[:action])
+
+ params = request_path_params("/people/5", method: :patch)
+ assert_equal("update", params[:action])
+
+ assert_raise(ActionController::UnknownHttpMethod) {
+ request_path_params("/people", method: :bacon)
+ }
+
+ params = request_path_params("/people/5", method: :get)
+ assert_equal("show", params[:action])
+ assert_equal("5", params[:id])
+
+ params = request_path_params("/people/5", method: :put)
+ assert_equal("update", params[:action])
+ assert_equal("5", params[:id])
+
+ params = request_path_params("/people/5", method: :patch)
+ assert_equal("update", params[:action])
+ assert_equal("5", params[:id])
+
+ params = request_path_params("/people/5", method: :delete)
+ assert_equal("destroy", params[:action])
+ assert_equal("5", params[:id])
+
+ assert_raise(ActionController::RoutingError) {
+ request_path_params("/people/5", method: :post)
+ }
+ end
+
+ def test_recognize_with_alias_in_conditions
+ set.draw do
+ match "/people" => "people#index", :as => "people", :via => :get
+ root to: "people#index"
+ end
+
+ params = request_path_params("/people", method: :get)
+ assert_equal("people", params[:controller])
+ assert_equal("index", params[:action])
+
+ params = request_path_params("/", method: :get)
+ assert_equal("people", params[:controller])
+ assert_equal("index", params[:action])
+ end
+
+ def test_typo_recognition
+ set.draw do
+ get "articles/:year/:month/:day/:title" => "articles#permalink",
+ :year => /\d{4}/, :day => /\d{1,2}/, :month => /\d{1,2}/
+ end
+
+ params = request_path_params("/articles/2005/11/05/a-very-interesting-article", method: :get)
+ assert_equal("permalink", params[:action])
+ assert_equal("2005", params[:year])
+ assert_equal("11", params[:month])
+ assert_equal("05", params[:day])
+ assert_equal("a-very-interesting-article", params[:title])
+ end
+
+ def test_routing_traversal_does_not_load_extra_classes
+ assert_not Object.const_defined?("Profiler__"), "Profiler should not be loaded"
+ set.draw do
+ get "/profile" => "profile#index"
+ end
+
+ request_path_params("/profile") rescue nil
+
+ assert_not Object.const_defined?("Profiler__"), "Profiler should not be loaded"
+ end
+
+ def test_recognize_with_conditions_and_format
+ set.draw do
+ get "people/:id" => "people#show", :as => "person"
+ put "people/:id" => "people#update"
+ patch "people/:id" => "people#update"
+ get "people/:id(.:format)" => "people#show"
+ end
+
+ params = request_path_params("/people/5", method: :get)
+ assert_equal("show", params[:action])
+ assert_equal("5", params[:id])
+
+ params = request_path_params("/people/5", method: :put)
+ assert_equal("update", params[:action])
+
+ params = request_path_params("/people/5", method: :patch)
+ assert_equal("update", params[:action])
+
+ params = request_path_params("/people/5.png", method: :get)
+ assert_equal("show", params[:action])
+ assert_equal("5", params[:id])
+ assert_equal("png", params[:format])
+ end
+
+ def test_generate_with_default_action
+ set.draw do
+ get "/people", controller: "people", action: "index"
+ get "/people/list", controller: "people", action: "list"
+ end
+
+ url = url_for(set, controller: "people", action: "list")
+ assert_equal "/people/list", url
+ end
+
+ def test_root_map
+ set.draw { root to: "people#index" }
+
+ params = request_path_params("", method: :get)
+ assert_equal("people", params[:controller])
+ assert_equal("index", params[:action])
+ end
+
+ def test_namespace
+ set.draw do
+ namespace "api" do
+ get "inventory" => "products#inventory"
+ end
+ end
+
+ params = request_path_params("/api/inventory", method: :get)
+ assert_equal("api/products", params[:controller])
+ assert_equal("inventory", params[:action])
+ end
+
+ def test_namespaced_root_map
+ set.draw do
+ namespace "api" do
+ root to: "products#index"
+ end
+ end
+
+ params = request_path_params("/api", method: :get)
+ assert_equal("api/products", params[:controller])
+ assert_equal("index", params[:action])
+ end
+
+ def test_namespace_with_path_prefix
+ set.draw do
+ scope module: "api", path: "prefix" do
+ get "inventory" => "products#inventory"
+ end
+ end
+
+ params = request_path_params("/prefix/inventory", method: :get)
+ assert_equal("api/products", params[:controller])
+ assert_equal("inventory", params[:action])
+ end
+
+ def test_namespace_with_blank_path_prefix
+ set.draw do
+ scope module: "api", path: "" do
+ get "inventory" => "products#inventory"
+ end
+ end
+
+ params = request_path_params("/inventory", method: :get)
+ assert_equal("api/products", params[:controller])
+ assert_equal("inventory", params[:action])
+ end
+
+ def test_id_is_sticky_when_it_ought_to_be
+ @set = make_set false
+
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:id/:action"
+ end
+ end
+
+ get URI("http://test.host/people/7/show")
+
+ assert_equal "/people/7/destroy", controller.url_for(action: "destroy", only_path: true)
+ end
+
+ def test_use_static_path_when_possible
+ @set = make_set false
+
+ set.draw do
+ get "about" => "welcome#about"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:id/:action"
+ end
+ end
+
+ get URI("http://test.host/welcom/get/7")
+
+ assert_equal "/about", controller.url_for(controller: "welcome",
+ action: "about",
+ only_path: true)
+ end
+
+ def test_generate
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller/:action/:id" } }
+
+ args = { controller: "foo", action: "bar", id: "7", x: "y" }
+ assert_equal "/foo/bar/7?x=y", url_for(set, args)
+ assert_equal ["/foo/bar/7", [:x]], set.generate_extras(args)
+ assert_equal [:x], set.extra_keys(args)
+ end
+
+ def test_generate_with_path_prefix
+ set.draw do
+ scope "my" do
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
+ end
+ end
+
+ args = { controller: "foo", action: "bar", id: "7", x: "y" }
+ assert_equal "/my/foo/bar/7?x=y", url_for(set, args)
+ end
+
+ def test_generate_with_blank_path_prefix
+ set.draw do
+ scope "" do
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
+ end
+ end
+
+ args = { controller: "foo", action: "bar", id: "7", x: "y" }
+ assert_equal "/foo/bar/7?x=y", url_for(set, args)
+ end
+
+ def test_named_routes_are_never_relative_to_modules
+ @set = make_set false
+
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get "/connection/manage(/:action)" => "connection/manage#index"
+ get "/connection/connection" => "connection/connection#index"
+ get "/connection" => "connection#index", :as => "family_connection"
+ end
+ end
+
+ assert_equal({ controller: "connection/manage",
+ action: "index", }, request_path_params("/connection/manage"))
+
+ url = controller.url_for(controller: "connection", only_path: true)
+ assert_equal "/connection/connection", url
+
+ url = controller.url_for(use_route: "family_connection",
+ controller: "connection", only_path: true)
+ assert_equal "/connection", url
+ end
+
+ def test_action_left_off_when_id_is_recalled
+ @set = make_set false
+
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
+ end
+
+ get URI("http://test.host/books/show/10")
+
+ assert_equal "/books", controller.url_for(controller: "books",
+ only_path: true,
+ action: "index")
+ end
+
+ def test_query_params_will_be_shown_when_recalled
+ @set = make_set false
+
+ set.draw do
+ get "show_weblog/:parameter" => "weblog#show"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
+ end
+
+ get URI("http://test.host/weblog/show/1")
+
+ assert_equal "/weblog/edit?parameter=1", controller.url_for(
+ action: "edit", parameter: 1, only_path: true)
+ end
+
+ def test_format_is_not_inherit
+ set.draw do
+ get "/posts(.:format)" => "posts#index"
+ end
+
+ get URI("http://test.host/posts.xml")
+ assert_equal({ controller: "posts", action: "index", format: "xml" },
+ controller.request.path_parameters)
+
+ assert_equal "/posts", controller.url_for(
+ controller: "posts", only_path: true)
+
+ assert_equal "/posts.xml", controller.url_for(
+ controller: "posts", format: "xml", only_path: true)
+ end
+
+ def test_expiry_determination_should_consider_values_with_to_param
+ @set = make_set false
+
+ set.draw { ActiveSupport::Deprecation.silence { get "projects/:project_id/:controller/:action" } }
+
+ get URI("http://test.host/projects/1/weblog/show")
+
+ assert_equal(
+ { controller: "weblog", action: "show", project_id: "1" },
+ controller.request.path_parameters)
+
+ assert_equal "/projects/1/weblog/show",
+ controller.url_for(action: "show", project_id: 1, only_path: true)
+ end
+
+ def test_named_route_in_nested_resource
+ set.draw do
+ resources :projects do
+ member do
+ get "milestones" => "milestones#index", :as => "milestones"
+ end
+ end
+ end
+
+ params = set.recognize_path("/projects/1/milestones", method: :get)
+ assert_equal("milestones", params[:controller])
+ assert_equal("index", params[:action])
+ end
+
+ def test_setting_root_in_namespace_using_symbol
+ assert_nothing_raised do
+ set.draw do
+ namespace :admin do
+ root to: "home#index"
+ end
+ end
+ end
+ end
+
+ def test_setting_root_in_namespace_using_string
+ assert_nothing_raised do
+ set.draw do
+ namespace "admin" do
+ root to: "home#index"
+ end
+ end
+ end
+ end
+
+ def test_route_constraints_with_unsupported_regexp_options_must_error
+ assert_raise ArgumentError do
+ set.draw do
+ get "page/:name" => "pages#show",
+ :constraints => { name: /(david|jamis)/m }
+ end
+ end
+ end
+
+ def test_route_constraints_with_supported_options_must_not_error
+ assert_nothing_raised do
+ set.draw do
+ get "page/:name" => "pages#show",
+ :constraints => { name: /(david|jamis)/i }
+ end
+ end
+ assert_nothing_raised do
+ set.draw do
+ get "page/:name" => "pages#show",
+ :constraints => { name: / # Desperately overcommented regexp
+ ( #Either
+ david #The Creator
+ | #Or
+ jamis #The Deployer
+ )/x }
+ end
+ end
+ end
+
+ def test_route_with_subdomain_and_constraints_must_receive_params
+ name_param = nil
+ set.draw do
+ get "page/:name" => "pages#show", :constraints => lambda { |request|
+ name_param = request.params[:name]
+ return true
+ }
+ end
+ assert_equal({ controller: "pages", action: "show", name: "mypage" },
+ set.recognize_path("http://subdomain.example.org/page/mypage"))
+ assert_equal(name_param, "mypage")
+ end
+
+ def test_route_requirement_recognize_with_ignore_case
+ set.draw do
+ get "page/:name" => "pages#show",
+ :constraints => { name: /(david|jamis)/i }
+ end
+ assert_equal({ controller: "pages", action: "show", name: "jamis" }, set.recognize_path("/page/jamis"))
+ assert_raise ActionController::RoutingError do
+ set.recognize_path("/page/davidjamis")
+ end
+ assert_equal({ controller: "pages", action: "show", name: "DAVID" }, set.recognize_path("/page/DAVID"))
+ end
+
+ def test_route_requirement_generate_with_ignore_case
+ set.draw do
+ get "page/:name" => "pages#show",
+ :constraints => { name: /(david|jamis)/i }
+ end
+
+ url = url_for(set, controller: "pages", action: "show", name: "david")
+ assert_equal "/page/david", url
+ assert_raise(ActionController::UrlGenerationError) do
+ url_for(set, controller: "pages", action: "show", name: "davidjamis")
+ end
+ url = url_for(set, controller: "pages", action: "show", name: "JAMIS")
+ assert_equal "/page/JAMIS", url
+ end
+
+ def test_route_requirement_recognize_with_extended_syntax
+ set.draw do
+ get "page/:name" => "pages#show",
+ :constraints => { name: / # Desperately overcommented regexp
+ ( #Either
+ david #The Creator
+ | #Or
+ jamis #The Deployer
+ )/x }
+ end
+ assert_equal({ controller: "pages", action: "show", name: "jamis" }, set.recognize_path("/page/jamis"))
+ assert_equal({ controller: "pages", action: "show", name: "david" }, set.recognize_path("/page/david"))
+ assert_raise ActionController::RoutingError do
+ set.recognize_path("/page/david #The Creator")
+ end
+ assert_raise ActionController::RoutingError do
+ set.recognize_path("/page/David")
+ end
+ end
+
+ def test_route_requirement_with_xi_modifiers
+ set.draw do
+ get "page/:name" => "pages#show",
+ :constraints => { name: / # Desperately overcommented regexp
+ ( #Either
+ david #The Creator
+ | #Or
+ jamis #The Deployer
+ )/xi }
+ end
+
+ assert_equal({ controller: "pages", action: "show", name: "JAMIS" },
+ set.recognize_path("/page/JAMIS"))
+
+ assert_equal "/page/JAMIS",
+ url_for(set, controller: "pages", action: "show", name: "JAMIS")
+ end
+
+ def test_routes_with_symbols
+ set.draw do
+ get "unnamed", controller: :pages, action: :show, name: :as_symbol
+ get "named", controller: :pages, action: :show, name: :as_symbol, as: :named
+ end
+ assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/unnamed"))
+ assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/named"))
+ end
+
+ def test_regexp_chunk_should_add_question_mark_for_optionals
+ set.draw do
+ get "/" => "foo#index"
+ get "/hello" => "bar#index"
+ end
+
+ assert_equal "/", url_for(set, controller: "foo")
+ assert_equal "/hello", url_for(set, controller: "bar")
+
+ assert_equal({ controller: "foo", action: "index" }, set.recognize_path("/"))
+ assert_equal({ controller: "bar", action: "index" }, set.recognize_path("/hello"))
+ end
+
+ def test_assign_route_options_with_anchor_chars
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get "/cars/:action/:person/:car/", controller: "cars"
+ end
+ end
+
+ assert_equal "/cars/buy/1/2", url_for(set, controller: "cars", action: "buy", person: "1", car: "2")
+
+ assert_equal({ controller: "cars", action: "buy", person: "1", car: "2" }, set.recognize_path("/cars/buy/1/2"))
+ end
+
+ def test_segmentation_of_dot_path
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get "/books/:action.rss", controller: "books"
+ end
+ end
+
+ assert_equal "/books/list.rss", url_for(set, controller: "books", action: "list")
+
+ assert_equal({ controller: "books", action: "list" }, set.recognize_path("/books/list.rss"))
+ end
+
+ def test_segmentation_of_dynamic_dot_path
+ set.draw do
+ ActiveSupport::Deprecation.silence do
+ get "/books(/:action(.:format))", controller: "books"
+ end
+ end
+
+ assert_equal "/books/list.rss", url_for(set, controller: "books", action: "list", format: "rss")
+ assert_equal "/books/list.xml", url_for(set, controller: "books", action: "list", format: "xml")
+ assert_equal "/books/list", url_for(set, controller: "books", action: "list")
+ assert_equal "/books", url_for(set, controller: "books", action: "index")
+
+ assert_equal({ controller: "books", action: "list", format: "rss" }, set.recognize_path("/books/list.rss"))
+ assert_equal({ controller: "books", action: "list", format: "xml" }, set.recognize_path("/books/list.xml"))
+ assert_equal({ controller: "books", action: "list" }, set.recognize_path("/books/list"))
+ assert_equal({ controller: "books", action: "index" }, set.recognize_path("/books"))
+ end
+
+ def test_slashes_are_implied
+ set.draw { ActiveSupport::Deprecation.silence { get("/:controller(/:action(/:id))") } }
+
+ assert_equal "/content", url_for(set, controller: "content", action: "index")
+ assert_equal "/content/list", url_for(set, controller: "content", action: "list")
+ assert_equal "/content/show/1", url_for(set, controller: "content", action: "show", id: "1")
+
+ assert_equal({ controller: "content", action: "index" }, set.recognize_path("/content"))
+ assert_equal({ controller: "content", action: "index" }, set.recognize_path("/content/index"))
+ assert_equal({ controller: "content", action: "list" }, set.recognize_path("/content/list"))
+ assert_equal({ controller: "content", action: "show", id: "1" }, set.recognize_path("/content/show/1"))
+ end
+
+ def test_default_route_recognition
+ expected = { controller: "pages", action: "show", id: "10" }
+ assert_equal expected, default_route_set.recognize_path("/pages/show/10")
+ assert_equal expected, default_route_set.recognize_path("/pages/show/10/")
+
+ expected[:id] = "jamis"
+ assert_equal expected, default_route_set.recognize_path("/pages/show/jamis/")
+
+ expected.delete :id
+ assert_equal expected, default_route_set.recognize_path("/pages/show")
+ assert_equal expected, default_route_set.recognize_path("/pages/show/")
+
+ expected[:action] = "index"
+ assert_equal expected, default_route_set.recognize_path("/pages/")
+ assert_equal expected, default_route_set.recognize_path("/pages")
+
+ assert_raise(ActionController::RoutingError) { default_route_set.recognize_path("/") }
+ assert_raise(ActionController::RoutingError) { default_route_set.recognize_path("/pages/how/goood/it/is/to/be/free") }
+ end
+
+ def test_default_route_should_omit_default_action
+ assert_equal "/accounts", url_for(default_route_set, controller: "accounts", action: "index")
+ end
+
+ def test_default_route_should_include_default_action_when_id_present
+ assert_equal "/accounts/index/20", url_for(default_route_set, controller: "accounts", action: "index", id: "20")
+ end
+
+ def test_default_route_should_work_with_action_but_no_id
+ assert_equal "/accounts/list_all", url_for(default_route_set, controller: "accounts", action: "list_all")
+ end
+
+ def test_default_route_should_uri_escape_pluses
+ expected = { controller: "pages", action: "show", id: "hello world" }
+ assert_equal expected, default_route_set.recognize_path("/pages/show/hello%20world")
+ assert_equal "/pages/show/hello%20world", url_for(default_route_set, expected)
+
+ expected[:id] = "hello+world"
+ assert_equal expected, default_route_set.recognize_path("/pages/show/hello+world")
+ assert_equal expected, default_route_set.recognize_path("/pages/show/hello%2Bworld")
+ assert_equal "/pages/show/hello+world", url_for(default_route_set, expected)
+ end
+
+ def test_build_empty_query_string
+ assert_uri_equal "/foo", url_for(default_route_set, controller: "foo")
+ end
+
+ def test_build_query_string_with_nil_value
+ assert_uri_equal "/foo", url_for(default_route_set, controller: "foo", x: nil)
+ end
+
+ def test_simple_build_query_string
+ assert_uri_equal "/foo?x=1&y=2", url_for(default_route_set, controller: "foo", x: "1", y: "2")
+ end
+
+ def test_convert_ints_build_query_string
+ assert_uri_equal "/foo?x=1&y=2", url_for(default_route_set, controller: "foo", x: 1, y: 2)
+ end
+
+ def test_escape_spaces_build_query_string
+ assert_uri_equal "/foo?x=hello+world&y=goodbye+world", url_for(default_route_set, controller: "foo", x: "hello world", y: "goodbye world")
+ end
+
+ def test_expand_array_build_query_string
+ assert_uri_equal "/foo?x%5B%5D=1&x%5B%5D=2", url_for(default_route_set, controller: "foo", x: [1, 2])
+ end
+
+ def test_escape_spaces_build_query_string_selected_keys
+ assert_uri_equal "/foo?x=hello+world", url_for(default_route_set, controller: "foo", x: "hello world")
+ end
+
+ def test_generate_with_default_params
+ set.draw do
+ get "dummy/page/:page" => "dummy#show"
+ get "dummy/dots/page.:page" => "dummy#dots"
+ get "ibocorp(/:page)" => "ibocorp#show",
+ :constraints => { page: /\d+/ },
+ :defaults => { page: 1 }
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+
+ assert_equal "/ibocorp", url_for(set, controller: "ibocorp", action: "show", page: 1)
+ end
+
+ include ActionDispatch::RoutingVerbs
+
+ alias :routes :set
+
+ def test_generate_with_optional_params_recalls_last_request
+ @set = make_set false
+
+ set.draw do
+ get "blog/", controller: "blog", action: "index"
+
+ get "blog(/:year(/:month(/:day)))",
+ controller: "blog",
+ action: "show_date",
+ constraints: { year: /(19|20)\d\d/, month: /[01]?\d/, day: /[0-3]?\d/ },
+ day: nil, month: nil
+
+ get "blog/show/:id", controller: "blog", action: "show", id: /\d+/
+
+ ActiveSupport::Deprecation.silence do
+ get "blog/:controller/:action(/:id)"
+ end
+
+ get "*anything", controller: "blog", action: "unknown_request"
+ end
+
+ recognize_path = ->(path) {
+ get(URI("http://example.org" + path))
+ controller.request.path_parameters
+ }
+
+ assert_equal({ controller: "blog", action: "index" }, recognize_path.("/blog"))
+ assert_equal({ controller: "blog", action: "show", id: "123" }, recognize_path.("/blog/show/123"))
+ assert_equal({ controller: "blog", action: "show_date", year: "2004", day: nil, month: nil }, recognize_path.("/blog/2004"))
+ assert_equal({ controller: "blog", action: "show_date", year: "2004", month: "12", day: nil }, recognize_path.("/blog/2004/12"))
+ assert_equal({ controller: "blog", action: "show_date", year: "2004", month: "12", day: "25" }, recognize_path.("/blog/2004/12/25"))
+ assert_equal({ controller: "articles", action: "edit", id: "123" }, recognize_path.("/blog/articles/edit/123"))
+ assert_equal({ controller: "articles", action: "show_stats" }, recognize_path.("/blog/articles/show_stats"))
+ assert_equal({ controller: "blog", action: "unknown_request", anything: "blog/wibble" }, recognize_path.("/blog/wibble"))
+ assert_equal({ controller: "blog", action: "unknown_request", anything: "junk" }, recognize_path.("/junk"))
+
+ get URI("http://example.org/blog/2006/07/28")
+
+ assert_equal({ controller: "blog", action: "show_date", year: "2006", month: "07", day: "28" }, controller.request.path_parameters)
+ assert_equal("/blog/2006/07/25", controller.url_for(day: 25, only_path: true))
+ assert_equal("/blog/2005", controller.url_for(year: 2005, only_path: true))
+ assert_equal("/blog/show/123", controller.url_for(action: "show", id: 123, only_path: true))
+ assert_equal("/blog/2006", controller.url_for(year: 2006, only_path: true))
+ assert_equal("/blog/2006", controller.url_for(year: 2006, month: nil, only_path: true))
+ end
+
+ private
+ def assert_uri_equal(expected, actual)
+ assert_equal(sort_query_string_params(expected), sort_query_string_params(actual))
+ end
+
+ def sort_query_string_params(uri)
+ path, qs = uri.split("?")
+ qs = qs.split("&").sort.join("&") if qs
+ qs ? "#{path}?#{qs}" : path
+ end
+end
+
+class RackMountIntegrationTests < ActiveSupport::TestCase
+ include RoutingTestHelpers
+
+ Model = Struct.new(:to_param)
+
+ Mapping = lambda {
+ namespace :admin do
+ resources :users, :posts
+ end
+
+ namespace "api" do
+ root to: "users#index"
+ end
+
+ get "/blog(/:year(/:month(/:day)))" => "posts#show_date",
+ :constraints => {
+ year: /(19|20)\d\d/,
+ month: /[01]?\d/,
+ day: /[0-3]?\d/
+ },
+ :day => nil,
+ :month => nil
+
+ get "archive/:year", controller: "archive", action: "index",
+ defaults: { year: nil },
+ constraints: { year: /\d{4}/ },
+ as: "blog"
+
+ resources :people
+ get "legacy/people" => "people#index", :legacy => "true"
+
+ get "symbols", controller: :symbols, action: :show, name: :as_symbol
+ get "id_default(/:id)" => "foo#id_default", :id => 1
+ match "get_or_post" => "foo#get_or_post", :via => [:get, :post]
+ get "optional/:optional" => "posts#index"
+ get "projects/:project_id" => "project#index", :as => "project"
+ get "clients" => "projects#index"
+
+ get "ignorecase/geocode/:postalcode" => "geocode#show", :postalcode => /hx\d\d-\d[a-z]{2}/i
+ get "extended/geocode/:postalcode" => "geocode#show", :constraints => {
+ postalcode: /# Postcode format
+ \d{5} #Prefix
+ (-\d{4})? #Suffix
+ /x
+ }, :as => "geocode"
+
+ get "news(.:format)" => "news#index"
+
+ ActiveSupport::Deprecation.silence do
+ get "comment/:id(/:action)" => "comments#show"
+ get "ws/:controller(/:action(/:id))", ws: true
+ get "account(/:action)" => "account#subscription"
+ get "pages/:page_id/:controller(/:action(/:id))"
+ get ":controller/ping", action: "ping"
+ end
+
+ get "こんにちは/世界", controller: "news", action: "index"
+
+ ActiveSupport::Deprecation.silence do
+ match ":controller(/:action(/:id))(.:format)", via: :all
+ end
+
+ root to: "news#index"
+ }
+
+ attr_reader :routes
+ attr_reader :controller
+
+ def setup
+ @routes = ActionDispatch::Routing::RouteSet.new
+ @routes.draw(&Mapping)
+ end
+
+ def test_recognize_path
+ assert_equal({ controller: "admin/users", action: "index" }, @routes.recognize_path("/admin/users", method: :get))
+ assert_equal({ controller: "admin/users", action: "create" }, @routes.recognize_path("/admin/users", method: :post))
+ assert_equal({ controller: "admin/users", action: "new" }, @routes.recognize_path("/admin/users/new", method: :get))
+ assert_equal({ controller: "admin/users", action: "show", id: "1" }, @routes.recognize_path("/admin/users/1", method: :get))
+ assert_equal({ controller: "admin/users", action: "update", id: "1" }, @routes.recognize_path("/admin/users/1", method: :put))
+ assert_equal({ controller: "admin/users", action: "destroy", id: "1" }, @routes.recognize_path("/admin/users/1", method: :delete))
+ assert_equal({ controller: "admin/users", action: "edit", id: "1" }, @routes.recognize_path("/admin/users/1/edit", method: :get))
+
+ assert_equal({ controller: "admin/posts", action: "index" }, @routes.recognize_path("/admin/posts", method: :get))
+ assert_equal({ controller: "admin/posts", action: "new" }, @routes.recognize_path("/admin/posts/new", method: :get))
+
+ assert_equal({ controller: "api/users", action: "index" }, @routes.recognize_path("/api", method: :get))
+ assert_equal({ controller: "api/users", action: "index" }, @routes.recognize_path("/api/", method: :get))
+
+ assert_equal({ controller: "posts", action: "show_date", year: "2009", month: nil, day: nil }, @routes.recognize_path("/blog/2009", method: :get))
+ assert_equal({ controller: "posts", action: "show_date", year: "2009", month: "01", day: nil }, @routes.recognize_path("/blog/2009/01", method: :get))
+ assert_equal({ controller: "posts", action: "show_date", year: "2009", month: "01", day: "01" }, @routes.recognize_path("/blog/2009/01/01", method: :get))
+
+ assert_equal({ controller: "archive", action: "index", year: "2010" }, @routes.recognize_path("/archive/2010"))
+ assert_equal({ controller: "archive", action: "index" }, @routes.recognize_path("/archive"))
+
+ assert_equal({ controller: "people", action: "index" }, @routes.recognize_path("/people", method: :get))
+ assert_equal({ controller: "people", action: "index", format: "xml" }, @routes.recognize_path("/people.xml", method: :get))
+ assert_equal({ controller: "people", action: "create" }, @routes.recognize_path("/people", method: :post))
+ assert_equal({ controller: "people", action: "new" }, @routes.recognize_path("/people/new", method: :get))
+ assert_equal({ controller: "people", action: "show", id: "1" }, @routes.recognize_path("/people/1", method: :get))
+ assert_equal({ controller: "people", action: "show", id: "1", format: "xml" }, @routes.recognize_path("/people/1.xml", method: :get))
+ assert_equal({ controller: "people", action: "update", id: "1" }, @routes.recognize_path("/people/1", method: :put))
+ assert_equal({ controller: "people", action: "destroy", id: "1" }, @routes.recognize_path("/people/1", method: :delete))
+ assert_equal({ controller: "people", action: "edit", id: "1" }, @routes.recognize_path("/people/1/edit", method: :get))
+ assert_equal({ controller: "people", action: "edit", id: "1", format: "xml" }, @routes.recognize_path("/people/1/edit.xml", method: :get))
+
+ assert_equal({ controller: "symbols", action: "show", name: :as_symbol }, @routes.recognize_path("/symbols"))
+ assert_equal({ controller: "foo", action: "id_default", id: "1" }, @routes.recognize_path("/id_default/1"))
+ assert_equal({ controller: "foo", action: "id_default", id: "2" }, @routes.recognize_path("/id_default/2"))
+ assert_equal({ controller: "foo", action: "id_default", id: 1 }, @routes.recognize_path("/id_default"))
+ assert_equal({ controller: "foo", action: "get_or_post" }, @routes.recognize_path("/get_or_post", method: :get))
+ assert_equal({ controller: "foo", action: "get_or_post" }, @routes.recognize_path("/get_or_post", method: :post))
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path("/get_or_post", method: :put) }
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path("/get_or_post", method: :delete) }
+
+ assert_equal({ controller: "posts", action: "index", optional: "bar" }, @routes.recognize_path("/optional/bar"))
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path("/optional") }
+
+ assert_equal({ controller: "posts", action: "show", id: "1", ws: true }, @routes.recognize_path("/ws/posts/show/1", method: :get))
+ assert_equal({ controller: "posts", action: "list", ws: true }, @routes.recognize_path("/ws/posts/list", method: :get))
+ assert_equal({ controller: "posts", action: "index", ws: true }, @routes.recognize_path("/ws/posts", method: :get))
+
+ assert_equal({ controller: "account", action: "subscription" }, @routes.recognize_path("/account", method: :get))
+ assert_equal({ controller: "account", action: "subscription" }, @routes.recognize_path("/account/subscription", method: :get))
+ assert_equal({ controller: "account", action: "billing" }, @routes.recognize_path("/account/billing", method: :get))
+
+ assert_equal({ page_id: "1", controller: "notes", action: "index" }, @routes.recognize_path("/pages/1/notes", method: :get))
+ assert_equal({ page_id: "1", controller: "notes", action: "list" }, @routes.recognize_path("/pages/1/notes/list", method: :get))
+ assert_equal({ page_id: "1", controller: "notes", action: "show", id: "2" }, @routes.recognize_path("/pages/1/notes/show/2", method: :get))
+
+ assert_equal({ controller: "posts", action: "ping" }, @routes.recognize_path("/posts/ping", method: :get))
+ assert_equal({ controller: "posts", action: "index" }, @routes.recognize_path("/posts", method: :get))
+ assert_equal({ controller: "posts", action: "index" }, @routes.recognize_path("/posts/index", method: :get))
+ assert_equal({ controller: "posts", action: "show" }, @routes.recognize_path("/posts/show", method: :get))
+ assert_equal({ controller: "posts", action: "show", id: "1" }, @routes.recognize_path("/posts/show/1", method: :get))
+ assert_equal({ controller: "posts", action: "create" }, @routes.recognize_path("/posts/create", method: :post))
+
+ assert_equal({ controller: "geocode", action: "show", postalcode: "hx12-1az" }, @routes.recognize_path("/ignorecase/geocode/hx12-1az"))
+ assert_equal({ controller: "geocode", action: "show", postalcode: "hx12-1AZ" }, @routes.recognize_path("/ignorecase/geocode/hx12-1AZ"))
+ assert_equal({ controller: "geocode", action: "show", postalcode: "12345-1234" }, @routes.recognize_path("/extended/geocode/12345-1234"))
+ assert_equal({ controller: "geocode", action: "show", postalcode: "12345" }, @routes.recognize_path("/extended/geocode/12345"))
+
+ assert_equal({ controller: "news", action: "index" }, @routes.recognize_path("/", method: :get))
+ assert_equal({ controller: "news", action: "index", format: "rss" }, @routes.recognize_path("/news.rss", method: :get))
+
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path("/none", method: :get) }
+ end
+
+ def test_generate_extras
+ assert_equal ["/people", []], @routes.generate_extras(controller: "people")
+ assert_equal ["/people", [:foo]], @routes.generate_extras(controller: "people", foo: "bar")
+ assert_equal ["/people", []], @routes.generate_extras(controller: "people", action: "index")
+ assert_equal ["/people", [:foo]], @routes.generate_extras(controller: "people", action: "index", foo: "bar")
+ assert_equal ["/people/new", []], @routes.generate_extras(controller: "people", action: "new")
+ assert_equal ["/people/new", [:foo]], @routes.generate_extras(controller: "people", action: "new", foo: "bar")
+ assert_equal ["/people/1", []], @routes.generate_extras(controller: "people", action: "show", id: "1")
+ assert_equal ["/people/1", [:bar, :foo]], sort_extras!(@routes.generate_extras(controller: "people", action: "show", id: "1", foo: "2", bar: "3"))
+ assert_equal ["/people", [:person]], @routes.generate_extras(controller: "people", action: "create", person: { first_name: "Josh", last_name: "Peek" })
+ assert_equal ["/people", [:people]], @routes.generate_extras(controller: "people", action: "create", people: ["Josh", "Dave"])
+
+ assert_equal ["/posts/show/1", []], @routes.generate_extras(controller: "posts", action: "show", id: "1")
+ assert_equal ["/posts/show/1", [:bar, :foo]], sort_extras!(@routes.generate_extras(controller: "posts", action: "show", id: "1", foo: "2", bar: "3"))
+ assert_equal ["/posts", []], @routes.generate_extras(controller: "posts", action: "index")
+ assert_equal ["/posts", [:foo]], @routes.generate_extras(controller: "posts", action: "index", foo: "bar")
+ end
+
+ def test_extras
+ params = { controller: "people" }
+ assert_equal [], @routes.extra_keys(params)
+ assert_equal({ controller: "people", action: "index" }, params)
+
+ params = { controller: "people", foo: "bar" }
+ assert_equal [:foo], @routes.extra_keys(params)
+ assert_equal({ controller: "people", action: "index", foo: "bar" }, params)
+
+ params = { controller: "people", action: "create", person: { name: "Josh" } }
+ assert_equal [:person], @routes.extra_keys(params)
+ assert_equal({ controller: "people", action: "create", person: { name: "Josh" } }, params)
+ end
+
+ def test_unicode_path
+ assert_equal({ controller: "news", action: "index" }, @routes.recognize_path(URI.parser.escape("こんにちは/世界"), method: :get))
+ end
+
+ def test_downcased_unicode_path
+ assert_equal({ controller: "news", action: "index" }, @routes.recognize_path(URI.parser.escape("こんにちは/世界").downcase, method: :get))
+ end
+
+ private
+ def sort_extras!(extras)
+ if extras.length == 2
+ extras[1].sort! { |a, b| a.to_s <=> b.to_s }
+ end
+ extras
+ end
+end
diff --git a/actionpack/test/controller/runner_test.rb b/actionpack/test/controller/runner_test.rb
new file mode 100644
index 0000000000..1709ab5f6d
--- /dev/null
+++ b/actionpack/test/controller/runner_test.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch/testing/integration"
+
+module ActionDispatch
+ class RunnerTest < ActiveSupport::TestCase
+ class MyRunner
+ include Integration::Runner
+
+ def initialize(session)
+ @integration_session = session
+ end
+
+ def hi; end
+ end
+
+ def test_respond_to?
+ runner = MyRunner.new(Class.new { def x; end }.new)
+ assert_respond_to runner, :hi
+ assert_respond_to runner, :x
+ end
+ end
+end
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
new file mode 100644
index 0000000000..c917cdf761
--- /dev/null
+++ b/actionpack/test/controller/send_file_test.rb
@@ -0,0 +1,259 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module TestFileUtils
+ def file_name() File.basename(__FILE__) end
+ def file_path() __FILE__ end
+ def file_data() @data ||= File.open(file_path, "rb") { |f| f.read } end
+end
+
+class SendFileController < ActionController::Base
+ include TestFileUtils
+ include ActionController::Testing
+ layout "layouts/standard" # to make sure layouts don't interfere
+
+ before_action :file, only: :file_from_before_action
+
+ attr_writer :options
+ def options
+ @options ||= {}
+ end
+
+ def file
+ send_file(file_path, options)
+ end
+
+ def file_from_before_action
+ raise "No file sent from before action."
+ end
+
+ def test_send_file_headers_bang
+ options = {
+ type: Mime[:png],
+ disposition: "disposition",
+ filename: "filename"
+ }
+
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_disposition_as_a_symbol
+ options = {
+ type: Mime[:png],
+ disposition: :disposition,
+ filename: "filename"
+ }
+
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_mime_lookup_with_symbol
+ options = { type: :png }
+
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_bad_symbol
+ options = { type: :this_type_is_not_registered }
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_nil_content_type
+ options = { type: nil }
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_guess_type_from_extension
+ options = { filename: params[:filename] }
+ send_data "foo", options
+ end
+
+ def data
+ send_data(file_data, options)
+ end
+end
+
+class SendFileWithActionControllerLive < SendFileController
+ include ActionController::Live
+end
+
+class SendFileTest < ActionController::TestCase
+ include TestFileUtils
+
+ def setup
+ @controller = SendFileController.new
+ end
+
+ def test_file_nostream
+ @controller.options = { stream: false }
+ response = nil
+ assert_nothing_raised { response = process("file") }
+ assert_not_nil response
+ body = response.body
+ assert_kind_of String, body
+ assert_equal file_data, body
+ end
+
+ def test_file_stream
+ response = nil
+ assert_nothing_raised { response = process("file") }
+ assert_not_nil response
+ assert_respond_to response.stream, :each
+ assert_respond_to response.stream, :to_path
+
+ require "stringio"
+ output = StringIO.new
+ output.binmode
+ output.string.force_encoding(file_data.encoding)
+ response.body_parts.each { |part| output << part.to_s }
+ assert_equal file_data, output.string
+ end
+
+ def test_file_url_based_filename
+ @controller.options = { url_based_filename: true }
+ response = nil
+ assert_nothing_raised { response = process("file") }
+ assert_not_nil response
+ assert_equal "attachment", response.headers["Content-Disposition"]
+ end
+
+ def test_data
+ response = nil
+ assert_nothing_raised { response = process("data") }
+ assert_not_nil response
+
+ assert_kind_of String, response.body
+ assert_equal file_data, response.body
+ end
+
+ def test_headers_after_send_shouldnt_include_charset
+ response = process("data")
+ assert_equal "application/octet-stream", response.headers["Content-Type"]
+
+ response = process("file")
+ assert_equal "application/octet-stream", response.headers["Content-Type"]
+ end
+
+ # Test that send_file_headers! is setting the correct HTTP headers.
+ def test_send_file_headers_bang
+ # Do it a few times: the resulting headers should be identical
+ # no matter how many times you send with the same options.
+ # Test resolving Ticket #458.
+ 5.times do
+ get :test_send_file_headers_bang
+
+ assert_equal "image/png", response.content_type
+ assert_equal %(disposition; filename="filename"; filename*=UTF-8''filename), response.get_header("Content-Disposition")
+ assert_equal "binary", response.get_header("Content-Transfer-Encoding")
+ assert_equal "private", response.get_header("Cache-Control")
+ end
+ end
+
+ def test_send_file_headers_with_disposition_as_a_symbol
+ get :test_send_file_headers_with_disposition_as_a_symbol
+
+ assert_equal %(disposition; filename="filename"; filename*=UTF-8''filename), response.get_header("Content-Disposition")
+ end
+
+ def test_send_file_headers_with_mime_lookup_with_symbol
+ get __method__
+ assert_equal "image/png", response.content_type
+ end
+
+ def test_send_file_headers_with_bad_symbol
+ error = assert_raise(ArgumentError) { get __method__ }
+ assert_equal "Unknown MIME type this_type_is_not_registered", error.message
+ end
+
+ def test_send_file_headers_with_nil_content_type
+ error = assert_raise(ArgumentError) { get __method__ }
+ assert_equal ":type option required", error.message
+ end
+
+ def test_send_file_headers_guess_type_from_extension
+ {
+ "image.png" => "image/png",
+ "image.jpeg" => "image/jpeg",
+ "image.jpg" => "image/jpeg",
+ "image.tif" => "image/tiff",
+ "image.gif" => "image/gif",
+ "movie.mp4" => "video/mp4",
+ "file.zip" => "application/zip",
+ "file.unk" => "application/octet-stream",
+ "zip" => "application/octet-stream"
+ }.each do |filename, expected_type|
+ get __method__, params: { filename: filename }
+ assert_equal expected_type, response.content_type
+ end
+ end
+
+ def test_send_file_with_default_content_disposition_header
+ process("data")
+ assert_equal "attachment", @controller.headers["Content-Disposition"]
+ end
+
+ def test_send_file_without_content_disposition_header
+ @controller.options = { disposition: nil }
+ process("data")
+ assert_nil @controller.headers["Content-Disposition"]
+ end
+
+ def test_send_file_from_before_action
+ response = nil
+ assert_nothing_raised { response = process("file_from_before_action") }
+ assert_not_nil response
+
+ assert_kind_of String, response.body
+ assert_equal file_data, response.body
+ end
+
+ %w(file data).each do |method|
+ define_method "test_send_#{method}_status" do
+ @controller.options = { stream: false, status: 500 }
+ assert_not_nil process(method)
+ assert_equal 500, @response.status
+ end
+
+ define_method "test_send_#{method}_content_type" do
+ @controller.options = { stream: false, content_type: "application/x-ruby" }
+ assert_nothing_raised { assert_not_nil process(method) }
+ assert_equal "application/x-ruby", @response.content_type
+ end
+
+ define_method "test_default_send_#{method}_status" do
+ @controller.options = { stream: false }
+ assert_nothing_raised { assert_not_nil process(method) }
+ assert_equal 200, @response.status
+ end
+ end
+
+ def test_send_file_with_action_controller_live
+ @controller = SendFileWithActionControllerLive.new
+ @controller.options = { content_type: "application/x-ruby" }
+
+ response = process("file")
+ assert_equal 200, response.status
+ end
+
+ def test_send_file_charset_with_type_options_key
+ @controller = SendFileWithActionControllerLive.new
+ @controller.options = { type: "text/calendar; charset=utf-8" }
+ response = process("file")
+ assert_equal "text/calendar; charset=utf-8", response.headers["Content-Type"]
+ end
+
+ def test_send_file_charset_with_type_options_key_without_charset
+ @controller = SendFileWithActionControllerLive.new
+ @controller.options = { type: "image/png" }
+ response = process("file")
+ assert_equal "image/png", response.headers["Content-Type"]
+ end
+
+ def test_send_file_charset_with_content_type_options_key
+ @controller = SendFileWithActionControllerLive.new
+ @controller.options = { content_type: "text/calendar" }
+ response = process("file")
+ assert_equal "text/calendar", response.headers["Content-Type"]
+ end
+end
diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb
new file mode 100644
index 0000000000..2094aa1aed
--- /dev/null
+++ b/actionpack/test/controller/show_exceptions_test.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ShowExceptions
+ class ShowExceptionsController < ActionController::Base
+ use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
+ use ActionDispatch::DebugExceptions
+
+ before_action only: :another_boom do
+ request.env["action_dispatch.show_detailed_exceptions"] = true
+ end
+
+ def boom
+ raise "boom!"
+ end
+
+ def another_boom
+ raise "boom!"
+ end
+
+ def show_detailed_exceptions?
+ request.local?
+ end
+ end
+
+ class ShowExceptionsTest < ActionDispatch::IntegrationTest
+ test "show error page from a remote ip" do
+ @app = ShowExceptionsController.action(:boom)
+ self.remote_addr = "208.77.188.166"
+ get "/"
+ assert_equal "500 error fixture\n", body
+ end
+
+ test "show diagnostics from a local ip if show_detailed_exceptions? is set to request.local?" do
+ @app = ShowExceptionsController.action(:boom)
+ ["127.0.0.1", "127.0.0.127", "127.12.1.1", "::1", "0:0:0:0:0:0:0:1", "0:0:0:0:0:0:0:1%0"].each do |ip_address|
+ self.remote_addr = ip_address
+ get "/"
+ assert_match(/boom/, body)
+ end
+ end
+
+ test "show diagnostics from a remote ip when env is already set" do
+ @app = ShowExceptionsController.action(:another_boom)
+ self.remote_addr = "208.77.188.166"
+ get "/"
+ assert_match(/boom/, body)
+ end
+ end
+
+ class ShowExceptionsOverriddenController < ShowExceptionsController
+ private
+
+ def show_detailed_exceptions?
+ params["detailed"] == "1"
+ end
+ end
+
+ class ShowExceptionsOverriddenTest < ActionDispatch::IntegrationTest
+ test "show error page" do
+ @app = ShowExceptionsOverriddenController.action(:boom)
+ get "/", params: { "detailed" => "0" }
+ assert_equal "500 error fixture\n", body
+ end
+
+ test "show diagnostics message" do
+ @app = ShowExceptionsOverriddenController.action(:boom)
+ get "/", params: { "detailed" => "1" }
+ assert_match(/boom/, body)
+ end
+ end
+
+ class ShowExceptionsFormatsTest < ActionDispatch::IntegrationTest
+ def test_render_json_exception
+ @app = ShowExceptionsOverriddenController.action(:boom)
+ get "/", headers: { "HTTP_ACCEPT" => "application/json" }
+ assert_response :internal_server_error
+ assert_equal "application/json", response.content_type.to_s
+ assert_equal({ status: 500, error: "Internal Server Error" }.to_json, response.body)
+ end
+
+ def test_render_xml_exception
+ @app = ShowExceptionsOverriddenController.action(:boom)
+ get "/", headers: { "HTTP_ACCEPT" => "application/xml" }
+ assert_response :internal_server_error
+ assert_equal "application/xml", response.content_type.to_s
+ assert_equal({ status: 500, error: "Internal Server Error" }.to_xml, response.body)
+ end
+
+ def test_render_fallback_exception
+ @app = ShowExceptionsOverriddenController.action(:boom)
+ get "/", headers: { "HTTP_ACCEPT" => "text/csv" }
+ assert_response :internal_server_error
+ assert_equal "text/html", response.content_type.to_s
+ end
+ end
+
+ class ShowFailsafeExceptionsTest < ActionDispatch::IntegrationTest
+ def test_render_failsafe_exception
+ @app = ShowExceptionsOverriddenController.action(:boom)
+ @exceptions_app = @app.instance_variable_get(:@exceptions_app)
+ @app.instance_variable_set(:@exceptions_app, nil)
+ $stderr = StringIO.new
+
+ get "/", headers: { "HTTP_ACCEPT" => "text/json" }
+ assert_response :internal_server_error
+ assert_equal "text/plain", response.content_type.to_s
+ ensure
+ @app.instance_variable_set(:@exceptions_app, @exceptions_app)
+ $stderr = STDERR
+ end
+ end
+end
diff --git a/actionpack/test/controller/streaming_test.rb b/actionpack/test/controller/streaming_test.rb
new file mode 100644
index 0000000000..5a42e2ae6d
--- /dev/null
+++ b/actionpack/test/controller/streaming_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ActionController
+ class StreamingResponseTest < ActionController::TestCase
+ class TestController < ActionController::Base
+ def self.controller_path
+ "test"
+ end
+
+ def basic_stream
+ %w{ hello world }.each do |word|
+ response.stream.write word
+ response.stream.write "\n"
+ end
+ response.stream.close
+ end
+ end
+
+ tests TestController
+
+ def test_write_to_stream
+ get :basic_stream
+ assert_equal "hello\nworld\n", @response.body
+ end
+ end
+end
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
new file mode 100644
index 0000000000..d1cd190747
--- /dev/null
+++ b/actionpack/test/controller/test_case_test.rb
@@ -0,0 +1,1197 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
+require "active_support/json/decoding"
+require "rails/engine"
+
+class TestCaseTest < ActionController::TestCase
+ def self.fixture_path; end
+
+ class TestController < ActionController::Base
+ def no_op
+ render plain: "dummy"
+ end
+
+ def set_flash
+ flash["test"] = ">#{flash["test"]}<"
+ render plain: "ignore me"
+ end
+
+ def delete_flash
+ flash.delete("test")
+ render plain: "ignore me"
+ end
+
+ def set_flash_now
+ flash.now["test_now"] = ">#{flash["test_now"]}<"
+ render plain: "ignore me"
+ end
+
+ def set_session
+ session["string"] = "A wonder"
+ session[:symbol] = "it works"
+ render plain: "Success"
+ end
+
+ def reset_the_session
+ reset_session
+ render plain: "ignore me"
+ end
+
+ def render_raw_post
+ raise ActiveSupport::TestCase::Assertion, "#raw_post is blank" if request.raw_post.blank?
+ render plain: request.raw_post
+ end
+
+ def render_body
+ render plain: request.body.read
+ end
+
+ def test_params
+ render plain: ::JSON.dump(params.to_unsafe_h)
+ end
+
+ def test_query_parameters
+ render plain: ::JSON.dump(request.query_parameters)
+ end
+
+ def test_request_parameters
+ render plain: request.request_parameters.inspect
+ end
+
+ def test_uri
+ render plain: request.fullpath
+ end
+
+ def test_format
+ render plain: request.format
+ end
+
+ def test_query_string
+ render plain: request.query_string
+ end
+
+ def test_protocol
+ render plain: request.protocol
+ end
+
+ def test_headers
+ render plain: ::JSON.dump(request.headers.env)
+ end
+
+ def test_html_output
+ render plain: <<HTML
+<html>
+ <body>
+ <a href="/"><img src="/images/button.png" /></a>
+ <div id="foo">
+ <ul>
+ <li class="item">hello</li>
+ <li class="item">goodbye</li>
+ </ul>
+ </div>
+ <div id="bar">
+ <form action="/somewhere">
+ Name: <input type="text" name="person[name]" id="person_name" />
+ </form>
+ </div>
+ </body>
+</html>
+HTML
+ end
+
+ def test_xml_output
+ response.content_type = params[:response_as]
+ render plain: <<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+ <area><p>area is an empty tag in HTML, so it won't contain this content</p></area>
+</root>
+XML
+ end
+
+ def test_only_one_param
+ render plain: (params[:left] && params[:right]) ? "EEP, Both here!" : "OK"
+ end
+
+ def test_remote_addr
+ render plain: (request.remote_addr || "not specified")
+ end
+
+ def test_file_upload
+ render plain: params[:file].size
+ end
+
+ def test_send_file
+ send_file(__FILE__)
+ end
+
+ def redirect_to_same_controller
+ redirect_to controller: "test", action: "test_uri", id: 5
+ end
+
+ def redirect_to_different_controller
+ redirect_to controller: "fail", id: 5
+ end
+
+ def create
+ head :created, location: "/resource"
+ end
+
+ def render_cookie
+ render plain: cookies["foo"]
+ end
+
+ def delete_cookie
+ cookies.delete("foo")
+ render plain: "ok"
+ end
+
+ def test_without_body
+ render html: '<div class="foo"></div>'.html_safe
+ end
+
+ def test_with_body
+ render html: '<body class="foo"></body>'.html_safe
+ end
+
+ def render_json
+ render json: request.raw_post
+ end
+
+ def boom
+ raise "boom!"
+ end
+
+ private
+
+ def generate_url(opts)
+ url_for(opts.merge(action: "test_uri"))
+ end
+ end
+
+ def setup
+ super
+ @controller = TestController.new
+ @request.delete_header "PATH_INFO"
+ @routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
+ r.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
+ end
+ end
+ end
+
+ class DefaultUrlOptionsCachingController < ActionController::Base
+ before_action { @dynamic_opt = "opt" }
+
+ def test_url_options_reset
+ render plain: url_for
+ end
+
+ def default_url_options
+ if defined?(@dynamic_opt)
+ super.merge dynamic_opt: @dynamic_opt
+ else
+ super
+ end
+ end
+ end
+
+ def test_assert_select_without_body
+ get :test_without_body
+
+ assert_select "body", 0
+ assert_select "div.foo"
+ end
+
+ def test_assert_select_with_body
+ get :test_with_body
+
+ assert_select "body.foo"
+ end
+
+ def test_url_options_reset
+ @controller = DefaultUrlOptionsCachingController.new
+ get :test_url_options_reset
+ assert_nil @request.params["dynamic_opt"]
+ assert_match(/dynamic_opt=opt/, @response.body)
+ end
+
+ def test_raw_post_handling
+ params = Hash[:page, { name: "page name" }, "some key", 123]
+ post :render_raw_post, params: params.dup
+
+ assert_equal params.to_query, @response.body
+ end
+
+ def test_params_round_trip
+ params = { "foo" => { "contents" => [{ "name" => "gorby", "id" => "123" }, { "name" => "puff", "d" => "true" }] } }
+ post :test_params, params: params.dup
+
+ controller_info = { "controller" => "test_case_test/test", "action" => "test_params" }
+ assert_equal params.merge(controller_info), JSON.parse(@response.body)
+ end
+
+ def test_handle_to_params
+ klass = Class.new do
+ def to_param
+ "bar"
+ end
+ end
+
+ post :test_params, params: { foo: klass.new }
+
+ assert_equal JSON.parse(@response.body)["foo"], "bar"
+ end
+
+
+ def test_body_stream
+ params = Hash[:page, { name: "page name" }, "some key", 123]
+
+ post :render_body, params: params.dup
+
+ assert_equal params.to_query, @response.body
+ end
+
+ def test_document_body_and_params_with_post
+ post :test_params, params: { id: 1 }
+ assert_equal({ "id" => "1", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body))
+ end
+
+ def test_document_body_with_post
+ post :render_body, body: "document body"
+ assert_equal "document body", @response.body
+ end
+
+ def test_document_body_with_put
+ put :render_body, body: "document body"
+ assert_equal "document body", @response.body
+ end
+
+ def test_head
+ head :test_params
+ assert_equal 200, @response.status
+ end
+
+ def test_process_without_flash
+ process :set_flash
+ assert_equal "><", flash["test"]
+ end
+
+ def test_process_with_flash
+ process :set_flash,
+ method: "GET",
+ flash: { "test" => "value" }
+ assert_equal ">value<", flash["test"]
+ end
+
+ def test_process_with_flash_now
+ process :set_flash_now,
+ method: "GET",
+ flash: { "test_now" => "value_now" }
+ assert_equal ">value_now<", flash["test_now"]
+ end
+
+ def test_process_delete_flash
+ process :set_flash
+ process :delete_flash
+ assert_empty flash
+ assert_empty session
+ end
+
+ def test_process_with_session
+ process :set_session
+ assert_equal "A wonder", session["string"], "A value stored in the session should be available by string key"
+ assert_equal "A wonder", session[:string], "Test session hash should allow indifferent access"
+ assert_equal "it works", session["symbol"], "Test session hash should allow indifferent access"
+ assert_equal "it works", session[:symbol], "Test session hash should allow indifferent access"
+ end
+
+ def test_process_with_session_kwarg
+ process :no_op, method: "GET", session: { "string" => "value1", symbol: "value2" }
+ assert_equal "value1", session["string"]
+ assert_equal "value1", session[:string]
+ assert_equal "value2", session["symbol"]
+ assert_equal "value2", session[:symbol]
+ end
+
+ def test_process_merges_session_arg
+ session[:foo] = "bar"
+ get :no_op, session: { bar: "baz" }
+ assert_equal "bar", session[:foo]
+ assert_equal "baz", session[:bar]
+ end
+
+ def test_merged_session_arg_is_retained_across_requests
+ get :no_op, session: { foo: "bar" }
+ assert_equal "bar", session[:foo]
+ get :no_op
+ assert_equal "bar", session[:foo]
+ end
+
+ def test_process_overwrites_existing_session_arg
+ session[:foo] = "bar"
+ get :no_op, session: { foo: "baz" }
+ assert_equal "baz", session[:foo]
+ end
+
+ def test_session_is_cleared_from_controller_after_reset_session
+ process :set_session
+ process :reset_the_session
+ assert_equal Hash.new, @controller.session.to_hash
+ end
+
+ def test_session_is_cleared_from_request_after_reset_session
+ process :set_session
+ process :reset_the_session
+ assert_equal Hash.new, @request.session.to_hash
+ end
+
+ def test_response_and_request_have_nice_accessors
+ process :no_op
+ assert_equal @response, response
+ assert_equal @request, request
+ end
+
+ def test_process_with_request_uri_with_no_params
+ process :test_uri
+ assert_equal "/test_case_test/test/test_uri", @response.body
+ end
+
+ def test_process_with_symbol_method
+ process :test_uri, method: :get
+ assert_equal "/test_case_test/test/test_uri", @response.body
+ end
+
+ def test_process_with_request_uri_with_params
+ process :test_uri,
+ method: "GET",
+ params: { id: 7 }
+
+ assert_equal "/test_case_test/test/test_uri/7", @response.body
+ end
+
+ def test_process_with_request_uri_with_params_with_explicit_uri
+ @request.env["PATH_INFO"] = "/explicit/uri"
+ process :test_uri, method: "GET", params: { id: 7 }
+ assert_equal "/explicit/uri", @response.body
+ end
+
+ def test_process_with_query_string
+ process :test_query_string,
+ method: "GET",
+ params: { q: "test" }
+ assert_equal "q=test", @response.body
+ end
+
+ def test_process_with_query_string_with_explicit_uri
+ @request.env["PATH_INFO"] = "/explicit/uri"
+ @request.env["QUERY_STRING"] = "q=test?extra=question"
+ process :test_query_string
+ assert_equal "q=test?extra=question", @response.body
+ end
+
+ def test_multiple_calls
+ process :test_only_one_param, method: "GET", params: { left: true }
+ assert_equal "OK", @response.body
+ process :test_only_one_param, method: "GET", params: { right: true }
+ assert_equal "OK", @response.body
+ end
+
+ def test_should_impose_childless_html_tags_in_html
+ process :test_xml_output, params: { response_as: "text/html" }
+
+ # <area> auto-closes, so the <p> becomes a sibling
+ if defined?(JRUBY_VERSION)
+ # https://github.com/sparklemotion/nokogiri/issues/1653
+ # HTML parser "fixes" "broken" markup in slightly different ways
+ assert_select "root > map > area + p"
+ else
+ assert_select "root > area + p"
+ end
+ end
+
+ def test_should_not_impose_childless_html_tags_in_xml
+ process :test_xml_output, params: { response_as: "application/xml" }
+
+ # <area> is not special, so the <p> is its child
+ assert_select "root > area > p"
+ end
+
+ def test_assert_generates
+ assert_generates "controller/action/5", controller: "controller", action: "action", id: "5"
+ assert_generates "controller/action/7", { id: "7" }, { controller: "controller", action: "action" }
+ assert_generates "controller/action/5", { controller: "controller", action: "action", id: "5", name: "bob" }, {}, { name: "bob" }
+ assert_generates "controller/action/7", { id: "7", name: "bob" }, { controller: "controller", action: "action" }, { name: "bob" }
+ assert_generates "controller/action/7", { id: "7" }, { controller: "controller", action: "action", name: "bob" }, {}
+ end
+
+ def test_assert_routing
+ assert_routing "content", controller: "content", action: "index"
+ end
+
+ def test_assert_routing_with_method
+ with_routing do |set|
+ set.draw { resources(:content) }
+ assert_routing({ method: "post", path: "content" }, { controller: "content", action: "create" })
+ end
+ end
+
+ def test_assert_routing_in_module
+ with_routing do |set|
+ set.draw do
+ namespace :admin do
+ get "user" => "user#index"
+ end
+ end
+
+ assert_routing "admin/user", controller: "admin/user", action: "index"
+ end
+ end
+
+ def test_assert_routing_with_glob
+ with_routing do |set|
+ set.draw { get("*path" => "pages#show") }
+ assert_routing("/company/about", controller: "pages", action: "show", path: "company/about")
+ end
+ end
+
+ def test_params_passing
+ get :test_params, params: {
+ page: {
+ name: "Page name",
+ month: "4",
+ year: "2004",
+ day: "6"
+ }
+ }
+ parsed_params = ::JSON.parse(@response.body)
+ assert_equal(
+ {
+ "controller" => "test_case_test/test", "action" => "test_params",
+ "page" => { "name" => "Page name", "month" => "4", "year" => "2004", "day" => "6" }
+ },
+ parsed_params
+ )
+ end
+
+ def test_nil_params
+ get :test_params, params: nil
+ parsed_params = JSON.parse(@response.body)
+ assert_equal(
+ {
+ "action" => "test_params",
+ "controller" => "test_case_test/test"
+ },
+ parsed_params
+ )
+ end
+
+ def test_query_param_named_action
+ get :test_query_parameters, params: { action: "foobar" }
+ parsed_params = JSON.parse(@response.body)
+ assert_equal({ "action" => "foobar" }, parsed_params)
+ end
+
+ def test_request_param_named_action
+ post :test_request_parameters, params: { action: "foobar" }
+ parsed_params = eval(@response.body)
+ assert_equal({ "action" => "foobar" }, parsed_params)
+ end
+
+ def test_kwarg_params_passing_with_session_and_flash
+ get :test_params, params: {
+ page: {
+ name: "Page name",
+ month: "4",
+ year: "2004",
+ day: "6"
+ }
+ }, session: { "foo" => "bar" }, flash: { notice: "created" }
+
+ parsed_params = ::JSON.parse(@response.body)
+ assert_equal(
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "page" => { "name" => "Page name", "month" => "4", "year" => "2004", "day" => "6" } },
+ parsed_params
+ )
+
+ assert_equal "bar", session[:foo]
+ assert_equal "created", flash[:notice]
+ end
+
+ def test_params_passing_with_integer
+ get :test_params, params: {
+ page: { name: "Page name", month: 4, year: 2004, day: 6 }
+ }
+ parsed_params = ::JSON.parse(@response.body)
+ assert_equal(
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "page" => { "name" => "Page name", "month" => "4", "year" => "2004", "day" => "6" } },
+ parsed_params
+ )
+ end
+
+ def test_params_passing_with_integers_when_not_html_request
+ get :test_params, params: { format: "json", count: 999 }
+ parsed_params = ::JSON.parse(@response.body)
+ assert_equal(
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "format" => "json", "count" => "999" },
+ parsed_params
+ )
+ end
+
+ def test_params_passing_path_parameter_is_string_when_not_html_request
+ get :test_params, params: { format: "json", id: 1 }
+ parsed_params = ::JSON.parse(@response.body)
+ assert_equal(
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "format" => "json", "id" => "1" },
+ parsed_params
+ )
+ end
+
+ def test_params_passing_with_frozen_values
+ assert_nothing_raised do
+ get :test_params, params: {
+ frozen: -"icy", frozens: [-"icy"].freeze, deepfreeze: { frozen: -"icy" }.freeze
+ }
+ end
+ parsed_params = ::JSON.parse(@response.body)
+ assert_equal(
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "frozen" => "icy", "frozens" => ["icy"], "deepfreeze" => { "frozen" => "icy" } },
+ parsed_params
+ )
+ end
+
+ def test_params_passing_doesnt_modify_in_place
+ page = { name: "Page name", month: 4, year: 2004, day: 6 }
+ get :test_params, params: { page: page }
+ assert_equal 2004, page[:year]
+ end
+
+ test "set additional HTTP headers" do
+ @request.headers["Referer"] = "http://nohost.com/home"
+ @request.headers["Content-Type"] = "application/rss+xml"
+ get :test_headers
+ parsed_env = ActiveSupport::JSON.decode(@response.body)
+ assert_equal "http://nohost.com/home", parsed_env["HTTP_REFERER"]
+ assert_equal "application/rss+xml", parsed_env["CONTENT_TYPE"]
+ end
+
+ test "set additional env variables" do
+ @request.headers["HTTP_REFERER"] = "http://example.com/about"
+ @request.headers["CONTENT_TYPE"] = "application/json"
+ get :test_headers
+ parsed_env = ActiveSupport::JSON.decode(@response.body)
+ assert_equal "http://example.com/about", parsed_env["HTTP_REFERER"]
+ assert_equal "application/json", parsed_env["CONTENT_TYPE"]
+ end
+
+ def test_using_as_json_sets_request_content_type_to_json
+ post :render_body, params: { bool_value: true, str_value: "string", num_value: 2 }, as: :json
+
+ assert_equal "application/json", @request.headers["CONTENT_TYPE"]
+ assert_equal true, @request.request_parameters[:bool_value]
+ assert_equal "string", @request.request_parameters[:str_value]
+ assert_equal 2, @request.request_parameters[:num_value]
+ end
+
+ def test_using_as_json_sets_format_json
+ post :render_body, params: { bool_value: true, str_value: "string", num_value: 2 }, as: :json
+ assert_equal "json", @request.format
+ end
+
+ def test_mutating_content_type_headers_for_plain_text_files_sets_the_header
+ @request.headers["Content-Type"] = "text/plain"
+ post :render_body, params: { name: "foo.txt" }
+
+ assert_equal "text/plain", @request.headers["Content-type"]
+ assert_equal "foo.txt", @request.request_parameters[:name]
+ assert_equal "render_body", @request.path_parameters[:action]
+ end
+
+ def test_mutating_content_type_headers_for_html_files_sets_the_header
+ @request.headers["Content-Type"] = "text/html"
+ post :render_body, params: { name: "foo.html" }
+
+ assert_equal "text/html", @request.headers["Content-type"]
+ assert_equal "foo.html", @request.request_parameters[:name]
+ assert_equal "render_body", @request.path_parameters[:action]
+ end
+
+ def test_mutating_content_type_headers_for_non_registered_mime_type_raises_an_error
+ assert_raises(RuntimeError) do
+ @request.headers["Content-Type"] = "type/fake"
+ post :render_body, params: { name: "foo.fake" }
+ end
+ end
+
+ def test_id_converted_to_string
+ get :test_params, params: {
+ id: 20, foo: Object.new
+ }
+ assert_kind_of String, @request.path_parameters[:id]
+ end
+
+ def test_array_path_parameter_handled_properly
+ with_routing do |set|
+ set.draw do
+ get "file/*path", to: "test_case_test/test#test_params"
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action"
+ end
+ end
+
+ get :test_params, params: { path: ["hello", "world"] }
+ assert_equal ["hello", "world"], @request.path_parameters[:path]
+ assert_equal "hello/world", @request.path_parameters[:path].to_param
+ end
+ end
+
+ def test_assert_realistic_path_parameters
+ get :test_params, params: { id: 20, foo: Object.new }
+
+ # All elements of path_parameters should use Symbol keys
+ @request.path_parameters.each_key do |key|
+ assert_kind_of Symbol, key
+ end
+ end
+
+ def test_with_routing_places_routes_back
+ assert @routes
+ routes_id = @routes.object_id
+
+ begin
+ with_routing { raise "fail" }
+ fail "Should not be here."
+ rescue RuntimeError
+ end
+
+ assert @routes
+ assert_equal routes_id, @routes.object_id
+ end
+
+ def test_remote_addr
+ get :test_remote_addr
+ assert_equal "0.0.0.0", @response.body
+
+ @request.remote_addr = "192.0.0.1"
+ get :test_remote_addr
+ assert_equal "192.0.0.1", @response.body
+ end
+
+ def test_header_properly_reset_after_remote_http_request
+ get :test_params, xhr: true
+ assert_nil @request.env["HTTP_X_REQUESTED_WITH"]
+ assert_nil @request.env["HTTP_ACCEPT"]
+ end
+
+ def test_xhr_with_params
+ get :test_params, params: { id: 1 }, xhr: true
+
+ assert_equal({ "id" => "1", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body))
+ end
+
+ def test_xhr_with_session
+ get :set_session, xhr: true
+
+ assert_equal "A wonder", session["string"], "A value stored in the session should be available by string key"
+ assert_equal "A wonder", session[:string], "Test session hash should allow indifferent access"
+ assert_equal "it works", session["symbol"], "Test session hash should allow indifferent access"
+ assert_equal "it works", session[:symbol], "Test session hash should allow indifferent access"
+ end
+
+ def test_params_reset_between_post_requests
+ post :no_op, params: { foo: "bar" }
+ assert_equal "bar", @request.params[:foo]
+
+ post :no_op
+ assert_predicate @request.params[:foo], :blank?
+ end
+
+ def test_filtered_parameters_reset_between_requests
+ get :no_op, params: { foo: "bar" }
+ assert_equal "bar", @request.filtered_parameters[:foo]
+
+ get :no_op, params: { foo: "baz" }
+ assert_equal "baz", @request.filtered_parameters[:foo]
+ end
+
+ def test_raw_post_reset_between_post_requests
+ post :no_op, params: { foo: "bar" }
+ assert_equal "foo=bar", @request.raw_post
+
+ post :no_op, params: { foo: "baz" }
+ assert_equal "foo=baz", @request.raw_post
+ end
+
+ def test_content_length_reset_after_post_request
+ post :no_op, params: { foo: "bar" }
+ assert_not_equal 0, @request.content_length
+
+ get :no_op
+ assert_equal 0, @request.content_length
+ end
+
+ def test_path_is_kept_after_the_request
+ get :test_params, params: { id: "foo" }
+ assert_equal "/test_case_test/test/test_params/foo", @request.path
+ end
+
+ def test_path_params_reset_between_request
+ get :test_params, params: { id: "foo" }
+ assert_equal "foo", @request.path_parameters[:id]
+
+ get :test_params
+ assert_nil @request.path_parameters[:id]
+ end
+
+ def test_request_protocol_is_reset_after_request
+ get :test_protocol
+ assert_equal "http://", @response.body
+
+ @request.env["HTTPS"] = "on"
+ get :test_protocol
+ assert_equal "https://", @response.body
+
+ @request.env.delete("HTTPS")
+ get :test_protocol
+ assert_equal "http://", @response.body
+ end
+
+ def test_request_format
+ get :test_format, params: { format: "html" }
+ assert_equal "text/html", @response.body
+
+ get :test_format, params: { format: "json" }
+ assert_equal "application/json", @response.body
+
+ get :test_format, params: { format: "xml" }
+ assert_equal "application/xml", @response.body
+
+ get :test_format
+ assert_equal "text/html", @response.body
+ end
+
+ def test_request_format_kwarg
+ get :test_format, format: "html"
+ assert_equal "text/html", @response.body
+
+ get :test_format, format: "json"
+ assert_equal "application/json", @response.body
+
+ get :test_format, format: "xml"
+ assert_equal "application/xml", @response.body
+
+ get :test_format
+ assert_equal "text/html", @response.body
+ end
+
+ def test_request_format_kwarg_overrides_params
+ get :test_format, format: "json", params: { format: "html" }
+ assert_equal "application/json", @response.body
+ end
+
+ def test_request_format_kwarg_doesnt_mutate_params
+ params = { foo: "bar" }.freeze
+
+ assert_nothing_raised do
+ get :test_format, format: "json", params: params
+ end
+ end
+
+ def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set
+ cookies["foo"] = "bar"
+ get :no_op
+ assert_equal "bar", cookies["foo"]
+ end
+
+ def test_cookies_should_be_escaped_properly
+ cookies["foo"] = "+"
+ get :render_cookie
+ assert_equal "+", @response.body
+ end
+
+ def test_should_detect_if_cookie_is_deleted
+ cookies["foo"] = "bar"
+ get :delete_cookie
+ assert_nil cookies["foo"]
+ end
+
+ def test_multiple_mixed_method_process_should_scrub_rack_input
+ post :test_params, params: { id: 1, foo: "an foo" }
+ assert_equal({ "id" => "1", "foo" => "an foo", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body))
+
+ get :test_params, params: { bar: "an bar" }
+ assert_equal({ "bar" => "an bar", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body))
+ end
+
+ %w(controller response request).each do |variable|
+ %w(get post put delete head process).each do |method|
+ define_method("test_#{variable}_missing_for_#{method}_raises_error") do
+ remove_instance_variable "@#{variable}"
+ begin
+ send(method, :test_remote_addr)
+ assert false, "expected RuntimeError, got nothing"
+ rescue RuntimeError => error
+ assert_match(%r{@#{variable} is nil}, error.message)
+ rescue => error
+ assert false, "expected RuntimeError, got #{error.class}"
+ end
+ end
+ end
+ end
+
+ FILES_DIR = File.expand_path("../fixtures/multipart", __dir__)
+
+ READ_BINARY = "rb:binary"
+ READ_PLAIN = "r:binary"
+
+ def test_test_uploaded_file
+ filename = "ruby_on_rails.jpg"
+ path = "#{FILES_DIR}/#{filename}"
+ content_type = "image/png"
+ expected = File.read(path)
+ expected.force_encoding(Encoding::BINARY)
+
+ file = Rack::Test::UploadedFile.new(path, content_type)
+ assert_equal filename, file.original_filename
+ assert_equal content_type, file.content_type
+ assert_equal file.path, file.local_path
+ assert_equal expected, file.read
+
+ new_content_type = "new content_type"
+ file.content_type = new_content_type
+ assert_equal new_content_type, file.content_type
+ end
+
+ def test_fixture_path_is_accessed_from_self_instead_of_active_support_test_case
+ TestCaseTest.stub :fixture_path, FILES_DIR do
+ uploaded_file = fixture_file_upload("/ruby_on_rails.jpg", "image/png")
+ assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
+ end
+ end
+
+ def test_test_uploaded_file_with_binary
+ filename = "ruby_on_rails.jpg"
+ path = "#{FILES_DIR}/#{filename}"
+ content_type = "image/png"
+
+ binary_uploaded_file = Rack::Test::UploadedFile.new(path, content_type, :binary)
+ assert_equal File.open(path, READ_BINARY).read, binary_uploaded_file.read
+
+ plain_uploaded_file = Rack::Test::UploadedFile.new(path, content_type)
+ assert_equal File.open(path, READ_PLAIN).read, plain_uploaded_file.read
+ end
+
+ def test_fixture_file_upload_with_binary
+ filename = "ruby_on_rails.jpg"
+ path = "#{FILES_DIR}/#{filename}"
+ content_type = "image/jpg"
+
+ binary_file_upload = fixture_file_upload(path, content_type, :binary)
+ assert_equal File.open(path, READ_BINARY).read, binary_file_upload.read
+
+ plain_file_upload = fixture_file_upload(path, content_type)
+ assert_equal File.open(path, READ_PLAIN).read, plain_file_upload.read
+ end
+
+ def test_fixture_file_upload_should_be_able_access_to_tempfile
+ file = fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg")
+ assert_respond_to file, :tempfile
+ end
+
+ def test_fixture_file_upload
+ post :test_file_upload,
+ params: {
+ file: fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg")
+ }
+ assert_equal "45142", @response.body
+ end
+
+ def test_fixture_file_upload_relative_to_fixture_path
+ TestCaseTest.stub :fixture_path, FILES_DIR do
+ uploaded_file = fixture_file_upload("ruby_on_rails.jpg", "image/jpg")
+ assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
+ end
+ end
+
+ def test_fixture_file_upload_ignores_fixture_path_given_full_path
+ TestCaseTest.stub :fixture_path, __dir__ do
+ uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg")
+ assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
+ end
+ end
+
+ def test_fixture_file_upload_ignores_nil_fixture_path
+ uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg")
+ assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
+ end
+
+ def test_action_dispatch_uploaded_file_upload
+ filename = "ruby_on_rails.jpg"
+ path = "#{FILES_DIR}/#{filename}"
+ post :test_file_upload, params: {
+ file: Rack::Test::UploadedFile.new(path, "image/jpg", true)
+ }
+ assert_equal "45142", @response.body
+ end
+
+ def test_test_uploaded_file_exception_when_file_doesnt_exist
+ assert_raise(RuntimeError) { Rack::Test::UploadedFile.new("non_existent_file") }
+ end
+
+ def test_redirect_url_only_cares_about_location_header
+ get :create
+ assert_response :created
+
+ # Redirect url doesn't care that it wasn't a :redirect response.
+ assert_equal "/resource", @response.redirect_url
+ assert_equal @response.redirect_url, redirect_to_url
+
+ # Must be a :redirect response.
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_redirected_to "/resource"
+ end
+ end
+
+ def test_exception_in_action_reaches_test
+ assert_raise(RuntimeError) do
+ process :boom, method: "GET"
+ end
+ end
+
+ def test_request_state_is_cleared_after_exception
+ assert_raise(RuntimeError) do
+ process :boom,
+ method: "GET",
+ params: { q: "test1" }
+ end
+
+ process :test_query_string,
+ method: "GET",
+ params: { q: "test2" }
+
+ assert_equal "q=test2", @response.body
+ end
+
+ def test_parsed_body_without_as_option
+ post :render_json, body: { foo: "heyo" }
+ assert_equal({ "foo" => "heyo" }, response.parsed_body)
+ end
+
+ def test_parsed_body_with_as_option
+ post :render_json, body: { foo: "heyo" }.to_json, as: :json
+ assert_equal({ "foo" => "heyo" }, response.parsed_body)
+ end
+end
+
+class ResponseDefaultHeadersTest < ActionController::TestCase
+ class TestController < ActionController::Base
+ def remove_header
+ headers.delete params[:header]
+ head :ok, "C" => "3"
+ end
+
+ # Render a head response, but don't touch default headers
+ def leave_alone
+ head :ok
+ end
+ end
+
+ def before_setup
+ @original = ActionDispatch::Response.default_headers
+ @defaults = { "A" => "1", "B" => "2" }
+ ActionDispatch::Response.default_headers = @defaults
+ super
+ end
+
+ teardown do
+ ActionDispatch::Response.default_headers = @original
+ end
+
+ def setup
+ super
+ @controller = TestController.new
+ @request.env["PATH_INFO"] = nil
+ @routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
+ r.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
+ end
+ end
+ end
+
+ test "response contains default headers" do
+ get :leave_alone
+
+ # Response headers start out with the defaults
+ assert_equal @defaults.merge("Content-Type" => "text/html"), response.headers
+ end
+
+ test "response deletes a default header" do
+ get :remove_header, params: { header: "A" }
+ assert_response :ok
+
+ # After a request, the response in the test case doesn't have the
+ # defaults merged on top again.
+ assert_not_includes response.headers, "A"
+ assert_includes response.headers, "B"
+ assert_includes response.headers, "C"
+ end
+end
+
+module EngineControllerTests
+ class Engine < ::Rails::Engine
+ isolate_namespace EngineControllerTests
+
+ routes.draw do
+ get "/" => "bar#index"
+ end
+ end
+
+ class BarController < ActionController::Base
+ def index
+ render plain: "bar"
+ end
+ end
+
+ class BarControllerTest < ActionController::TestCase
+ tests BarController
+
+ def test_engine_controller_route
+ get :index
+ assert_equal @response.body, "bar"
+ end
+ end
+
+ class BarControllerTestWithExplicitRouteSet < ActionController::TestCase
+ tests BarController
+
+ def setup
+ @routes = Engine.routes
+ end
+
+ def test_engine_controller_route
+ get :index
+ assert_equal @response.body, "bar"
+ end
+ end
+end
+
+class InferringClassNameTest < ActionController::TestCase
+ def test_determine_controller_class
+ assert_equal ContentController, determine_class("ContentControllerTest")
+ end
+
+ def test_determine_controller_class_with_nonsense_name
+ assert_nil determine_class("HelloGoodBye")
+ end
+
+ def test_determine_controller_class_with_sensible_name_where_no_controller_exists
+ assert_nil determine_class("NoControllerWithThisNameTest")
+ end
+
+ private
+ def determine_class(name)
+ ActionController::TestCase.determine_default_controller_class(name)
+ end
+end
+
+class CrazyNameTest < ActionController::TestCase
+ tests ContentController
+
+ def test_controller_class_can_be_set_manually_not_just_inferred
+ assert_equal ContentController, self.class.controller_class
+ end
+end
+
+class CrazySymbolNameTest < ActionController::TestCase
+ tests :content
+
+ def test_set_controller_class_using_symbol
+ assert_equal ContentController, self.class.controller_class
+ end
+end
+
+class CrazyStringNameTest < ActionController::TestCase
+ tests "content"
+
+ def test_set_controller_class_using_string
+ assert_equal ContentController, self.class.controller_class
+ end
+end
+
+class NamedRoutesControllerTest < ActionController::TestCase
+ tests ContentController
+
+ def test_should_be_able_to_use_named_routes_before_a_request_is_done
+ with_routing do |set|
+ set.draw { resources :contents }
+ assert_equal "http://test.host/contents/new", new_content_url
+ assert_equal "http://test.host/contents/1", content_url(id: 1)
+ end
+ end
+end
+
+class AnonymousControllerTest < ActionController::TestCase
+ def setup
+ @controller = Class.new(ActionController::Base) do
+ def index
+ render plain: params[:controller]
+ end
+ end.new
+
+ @routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
+ r.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
+ end
+ end
+ end
+
+ def test_controller_name
+ get :index
+ assert_equal "anonymous", @response.body
+ end
+end
+
+class RoutingDefaultsTest < ActionController::TestCase
+ def setup
+ @controller = Class.new(ActionController::Base) do
+ def post
+ render plain: request.fullpath
+ end
+
+ def project
+ render plain: request.fullpath
+ end
+ end.new
+
+ @routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
+ r.draw do
+ get "/posts/:id", to: "anonymous#post", bucket_type: "post"
+ get "/projects/:id", to: "anonymous#project", defaults: { bucket_type: "project" }
+ end
+ end
+ end
+
+ def test_route_option_can_be_passed_via_process
+ get :post, params: { id: 1, bucket_type: "post" }
+ assert_equal "/posts/1", @response.body
+ end
+
+ def test_route_default_is_not_required_for_building_request_uri
+ get :project, params: { id: 2 }
+ assert_equal "/projects/2", @response.body
+ end
+end
diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb
new file mode 100644
index 0000000000..a1521da702
--- /dev/null
+++ b/actionpack/test/controller/url_for_integration_test.rb
@@ -0,0 +1,192 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
+require "active_support/core_ext/object/with_options"
+
+module ActionPack
+ class URLForIntegrationTest < ActiveSupport::TestCase
+ include RoutingTestHelpers
+ include ActionDispatch::RoutingVerbs
+
+ Model = Struct.new(:to_param)
+
+ Mapping = lambda {
+ namespace :admin do
+ resources :users, :posts
+ end
+
+ namespace "api" do
+ root to: "users#index"
+ end
+
+ get "/blog(/:year(/:month(/:day)))" => "posts#show_date",
+ :constraints => {
+ year: /(19|20)\d\d/,
+ month: /[01]?\d/,
+ day: /[0-3]?\d/
+ },
+ :day => nil,
+ :month => nil
+
+ get "archive/:year", controller: "archive", action: "index",
+ defaults: { year: nil },
+ constraints: { year: /\d{4}/ },
+ as: "blog"
+
+ resources :people
+
+ get "symbols", controller: :symbols, action: :show, name: :as_symbol
+ get "id_default(/:id)" => "foo#id_default", :id => 1
+ match "get_or_post" => "foo#get_or_post", :via => [:get, :post]
+ get "optional/:optional" => "posts#index"
+ get "projects/:project_id" => "project#index", :as => "project"
+ get "clients" => "projects#index"
+
+ get "ignorecase/geocode/:postalcode" => "geocode#show", :postalcode => /hx\d\d-\d[a-z]{2}/i
+ get "extended/geocode/:postalcode" => "geocode#show", :constraints => {
+ postalcode: /# Postcode format
+ \d{5} #Prefix
+ (-\d{4})? #Suffix
+ /x
+ }, :as => "geocode"
+
+ get "news(.:format)" => "news#index"
+
+ ActiveSupport::Deprecation.silence {
+ get "comment/:id(/:action)" => "comments#show"
+ get "ws/:controller(/:action(/:id))", ws: true
+ get "account(/:action)" => "account#subscription"
+ get "pages/:page_id/:controller(/:action(/:id))"
+ get ":controller/ping", action: "ping"
+ get ":controller(/:action(/:id))(.:format)"
+ }
+
+ root to: "news#index"
+ }
+
+ attr_reader :routes
+ attr_accessor :controller
+
+ def setup
+ @routes = make_set false
+ @routes.draw(&Mapping)
+ end
+
+ [
+ ["/admin/users", [ { use_route: "admin_users" }]],
+ ["/admin/users", [ { controller: "admin/users" }]],
+ ["/admin/users", [ { controller: "admin/users", action: "index" }]],
+ ["/admin/users", [ { action: "index" }, { controller: "admin/users", action: "index" }, "/admin/users"]],
+ ["/admin/users", [ { controller: "users", action: "index" }, { controller: "admin/accounts", action: "show", id: "1" }, "/admin/accounts/show/1"]],
+ ["/people", [ { controller: "/people", action: "index" }, { controller: "admin/accounts", action: "foo", id: "bar" }, "/admin/accounts/foo/bar"]],
+
+ ["/admin/posts", [ { controller: "admin/posts" }]],
+ ["/admin/posts/new", [ { controller: "admin/posts", action: "new" }]],
+
+ ["/blog/2009", [ { controller: "posts", action: "show_date", year: 2009 }]],
+ ["/blog/2009/1", [ { controller: "posts", action: "show_date", year: 2009, month: 1 }]],
+ ["/blog/2009/1/1", [ { controller: "posts", action: "show_date", year: 2009, month: 1, day: 1 }]],
+
+ ["/archive/2010", [ { controller: "archive", action: "index", year: "2010" }]],
+ ["/archive", [ { controller: "archive", action: "index" }]],
+ ["/archive?year=january", [ { controller: "archive", action: "index", year: "january" }]],
+
+ ["/people", [ { controller: "people", action: "index" }]],
+ ["/people", [ { action: "index" }, { controller: "people", action: "index" }, "/people"]],
+ ["/people", [ { action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people", [ { controller: "people", action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people", [ {}, { controller: "people", action: "index" }, "/people"]],
+ ["/people/1", [ { controller: "people", action: "show" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people/new", [ { use_route: "new_person" }]],
+ ["/people/new", [ { controller: "people", action: "new" }]],
+ ["/people/1", [ { use_route: "person", id: "1" }]],
+ ["/people/1", [ { controller: "people", action: "show", id: "1" }]],
+ ["/people/1.xml", [ { controller: "people", action: "show", id: "1", format: "xml" }]],
+ ["/people/1", [ { controller: "people", action: "show", id: 1 }]],
+ ["/people/1", [ { controller: "people", action: "show", id: Model.new("1") }]],
+ ["/people/1", [ { action: "show", id: "1" }, { controller: "people", action: "index" }, "/people"]],
+ ["/people/1", [ { action: "show", id: 1 }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people", [ { controller: "people", action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people/1", [ {}, { controller: "people", action: "show", id: "1" }, "/people/show/1"]],
+ ["/people/1", [ { controller: "people", action: "show" }, { controller: "people", action: "index", id: "1" }, "/people/index/1"]],
+ ["/people/1/edit", [ { controller: "people", action: "edit", id: "1" }]],
+ ["/people/1/edit.xml", [ { controller: "people", action: "edit", id: "1", format: "xml" }]],
+ ["/people/1/edit", [ { use_route: "edit_person", id: "1" }]],
+ ["/people/1?legacy=true", [ { controller: "people", action: "show", id: "1", legacy: "true" }]],
+ ["/people?legacy=true", [ { controller: "people", action: "index", legacy: "true" }]],
+
+ ["/id_default/2", [ { controller: "foo", action: "id_default", id: "2" }]],
+ ["/id_default", [ { controller: "foo", action: "id_default", id: "1" }]],
+ ["/id_default", [ { controller: "foo", action: "id_default", id: 1 }]],
+ ["/id_default", [ { controller: "foo", action: "id_default" }]],
+ ["/optional/bar", [ { controller: "posts", action: "index", optional: "bar" }]],
+ ["/posts", [ { controller: "posts", action: "index" }]],
+
+ ["/project", [ { controller: "project", action: "index" }]],
+ ["/projects/1", [ { controller: "project", action: "index", project_id: "1" }]],
+ ["/projects/1", [ { controller: "project", action: "index" }, { project_id: "1", controller: "project", action: "index" }, "/projects/1"]],
+ ["/projects/1", [ { use_route: "project", controller: "project", action: "index", project_id: "1" }]],
+ ["/projects/1", [ { use_route: "project", controller: "project", action: "index" }, { controller: "project", action: "index", project_id: "1" }, "/projects/1"]],
+
+ ["/clients", [ { controller: "projects", action: "index" }]],
+ ["/clients?project_id=1", [ { controller: "projects", action: "index", project_id: "1" }]],
+ ["/clients", [ { controller: "projects", action: "index" }, { project_id: "1", controller: "project", action: "index" }, "/projects/1"]],
+
+ ["/comment/20", [ { id: 20 }, { controller: "comments", action: "show" }, "/comments/show"]],
+ ["/comment/20", [ { controller: "comments", id: 20, action: "show" }]],
+ ["/comments/boo", [ { controller: "comments", action: "boo" }]],
+
+ ["/ws/posts/show/1", [ { controller: "posts", action: "show", id: "1", ws: true }]],
+ ["/ws/posts", [ { controller: "posts", action: "index", ws: true }]],
+
+ ["/account", [ { controller: "account", action: "subscription" }]],
+ ["/account/billing", [ { controller: "account", action: "billing" }]],
+
+ ["/pages/1/notes/show/1", [ { page_id: "1", controller: "notes", action: "show", id: "1" }]],
+ ["/pages/1/notes/list", [ { page_id: "1", controller: "notes", action: "list" }]],
+ ["/pages/1/notes", [ { page_id: "1", controller: "notes", action: "index" }]],
+ ["/pages/1/notes", [ { page_id: "1", controller: "notes" }]],
+ ["/notes", [ { page_id: nil, controller: "notes" }]],
+ ["/notes", [ { controller: "notes" }]],
+ ["/notes/print", [ { controller: "notes", action: "print" }]],
+ ["/notes/print", [ {}, { controller: "notes", action: "print" }, "/notes/print"]],
+
+ ["/notes/index/1", [ { controller: "notes" }, { controller: "notes", action: "index", id: "1" }, "/notes/index/1"]],
+ ["/notes/index/1", [ { controller: "notes" }, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]],
+ ["/notes/index/1", [ { action: "index" }, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]],
+ ["/notes/index/1", [ {}, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]],
+ ["/notes/show/1", [ {}, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]],
+ ["/posts", [ { controller: "posts" }, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]],
+ ["/notes/list", [ { action: "list" }, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]],
+
+ ["/posts/ping", [ { controller: "posts", action: "ping" }]],
+ ["/posts/show/1", [ { controller: "posts", action: "show", id: "1" }]],
+ ["/posts/show/1", [ { controller: "posts", action: "show", id: "1", format: "" }]],
+ ["/posts", [ { controller: "posts" }]],
+ ["/posts", [ { controller: "posts", action: "index" }]],
+ ["/posts/create", [ { action: "create" }, { day: nil, month: nil, controller: "posts", action: "show_date" }, "/blog"]],
+ ["/posts?foo=bar", [ { controller: "posts", foo: "bar" }]],
+ ["/posts?foo%5B%5D=bar&foo%5B%5D=baz", [{ controller: "posts", foo: ["bar", "baz"] }]],
+ ["/posts?page=2", [{ controller: "posts", page: 2 }]],
+ ["/posts?q%5Bfoo%5D%5Ba%5D=b", [{ controller: "posts", q: { foo: { a: "b" } } }]],
+
+ ["/news.rss", [{ controller: "news", action: "index", format: "rss" }]],
+ ].each_with_index do |(url, params), i|
+ if params.length > 1
+ hash, path_params, route = *params
+ hash[:only_path] = true
+
+ define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do
+ get URI("http://test.host" + route.to_s)
+ assert_equal path_params, controller.request.path_parameters
+ assert_equal url, controller.url_for(hash), params.inspect
+ end
+ else
+ define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do
+ assert_equal url, url_for(@routes, params.first), params.inspect
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb
new file mode 100644
index 0000000000..e381abee36
--- /dev/null
+++ b/actionpack/test/controller/url_for_test.rb
@@ -0,0 +1,519 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module AbstractController
+ module Testing
+ class UrlForTest < ActionController::TestCase
+ class W
+ include ActionDispatch::Routing::RouteSet.new.tap { |r|
+ r.draw {
+ ActiveSupport::Deprecation.silence {
+ get ":controller(/:action(/:id(.:format)))"
+ }
+ }
+ }.url_helpers
+ end
+
+ def teardown
+ W.default_url_options.clear
+ end
+
+ def test_nested_optional
+ klass = Class.new {
+ include ActionDispatch::Routing::RouteSet.new.tap { |r|
+ r.draw {
+ get "/foo/(:bar/(:baz))/:zot", as: "fun",
+ controller: :articles,
+ action: :index
+ }
+ }.url_helpers
+ default_url_options[:host] = "example.com"
+ }
+
+ path = klass.new.fun_path(controller: :articles,
+ baz: "baz",
+ zot: "zot")
+ # :bar key isn't provided
+ assert_equal "/foo/zot", path
+ end
+
+ def add_host!(app = W)
+ app.default_url_options[:host] = "www.basecamphq.com"
+ end
+
+ def add_port!
+ W.default_url_options[:port] = 3000
+ end
+
+ def add_numeric_host!
+ W.default_url_options[:host] = "127.0.0.1"
+ end
+
+ def test_exception_is_thrown_without_host
+ assert_raise ArgumentError do
+ W.new.url_for controller: "c", action: "a", id: "i"
+ end
+ end
+
+ def test_anchor
+ assert_equal("/c/a#anchor",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: "anchor")
+ )
+ end
+
+ def test_nil_anchor
+ assert_equal(
+ "/c/a",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: nil)
+ )
+ end
+
+ def test_false_anchor
+ assert_equal(
+ "/c/a",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: false)
+ )
+ end
+
+ def test_anchor_should_call_to_param
+ assert_equal("/c/a#anchor",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: Struct.new(:to_param).new("anchor"))
+ )
+ end
+
+ def test_anchor_should_escape_unsafe_pchar
+ assert_equal("/c/a#%23anchor",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: Struct.new(:to_param).new("#anchor"))
+ )
+ end
+
+ def test_anchor_should_not_escape_safe_pchar
+ assert_equal("/c/a#name=user&email=user@domain.com",
+ W.new.url_for(only_path: true, controller: "c", action: "a", anchor: Struct.new(:to_param).new("name=user&email=user@domain.com"))
+ )
+ end
+
+ def test_default_host
+ add_host!
+ assert_equal("http://www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_host_may_be_overridden
+ add_host!
+ assert_equal("http://37signals.basecamphq.com/c/a/i",
+ W.new.url_for(host: "37signals.basecamphq.com", controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_subdomain_may_be_changed
+ add_host!
+ assert_equal("http://api.basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: "api", controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_subdomain_may_be_object
+ model = Class.new { def self.to_param; "api"; end }
+ add_host!
+ assert_equal("http://api.basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: model, controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_subdomain_may_be_removed
+ add_host!
+ assert_equal("http://basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: false, controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_subdomain_may_be_removed_with_blank_string
+ W.default_url_options[:host] = "api.basecamphq.com"
+ assert_equal("http://basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: "", controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_multiple_subdomains_may_be_removed
+ W.default_url_options[:host] = "mobile.www.api.basecamphq.com"
+ assert_equal("http://basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: false, controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_subdomain_may_be_accepted_with_numeric_host
+ add_numeric_host!
+ assert_equal("http://127.0.0.1/c/a/i",
+ W.new.url_for(subdomain: "api", controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_domain_may_be_changed
+ add_host!
+ assert_equal("http://www.37signals.com/c/a/i",
+ W.new.url_for(domain: "37signals.com", controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_tld_length_may_be_changed
+ add_host!
+ assert_equal("http://mobile.www.basecamphq.com/c/a/i",
+ W.new.url_for(subdomain: "mobile", tld_length: 2, controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_port
+ add_host!
+ assert_equal("http://www.basecamphq.com:3000/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", port: 3000)
+ )
+ end
+
+ def test_default_port
+ add_host!
+ add_port!
+ assert_equal("http://www.basecamphq.com:3000/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_protocol
+ add_host!
+ assert_equal("https://www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https")
+ )
+ end
+
+ def test_protocol_with_and_without_separators
+ add_host!
+ assert_equal("https://www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https")
+ )
+ assert_equal("https://www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https:")
+ )
+ assert_equal("https://www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https://")
+ )
+ end
+
+ def test_without_protocol
+ add_host!
+ assert_equal("//www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "//")
+ )
+ assert_equal("//www.basecamphq.com/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: false)
+ )
+ end
+
+ def test_without_protocol_and_with_port
+ add_host!
+ add_port!
+
+ assert_equal("//www.basecamphq.com:3000/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "//")
+ )
+ assert_equal("//www.basecamphq.com:3000/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: false)
+ )
+ end
+
+ def test_trailing_slash
+ add_host!
+ options = { controller: "foo", trailing_slash: true, action: "bar", id: "33" }
+ assert_equal("http://www.basecamphq.com/foo/bar/33/", W.new.url_for(options))
+ end
+
+ def test_trailing_slash_with_protocol
+ add_host!
+ options = { trailing_slash: true, protocol: "https", controller: "foo", action: "bar", id: "33" }
+ assert_equal("https://www.basecamphq.com/foo/bar/33/", W.new.url_for(options))
+ assert_equal "https://www.basecamphq.com/foo/bar/33/?query=string", W.new.url_for(options.merge(query: "string"))
+ end
+
+ def test_trailing_slash_with_only_path
+ options = { controller: "foo", trailing_slash: true }
+ assert_equal "/foo/", W.new.url_for(options.merge(only_path: true))
+ options.update(action: "bar", id: "33")
+ assert_equal "/foo/bar/33/", W.new.url_for(options.merge(only_path: true))
+ assert_equal "/foo/bar/33/?query=string", W.new.url_for(options.merge(query: "string", only_path: true))
+ end
+
+ def test_trailing_slash_with_anchor
+ options = { trailing_slash: true, controller: "foo", action: "bar", id: "33", only_path: true, anchor: "chapter7" }
+ assert_equal "/foo/bar/33/#chapter7", W.new.url_for(options)
+ assert_equal "/foo/bar/33/?query=string#chapter7", W.new.url_for(options.merge(query: "string"))
+ end
+
+ def test_trailing_slash_with_params
+ url = W.new.url_for(trailing_slash: true, only_path: true, controller: "cont", action: "act", p1: "cafe", p2: "link")
+ params = extract_params(url)
+ assert_equal({ p1: "cafe" }.to_query, params[0])
+ assert_equal({ p2: "link" }.to_query, params[1])
+ end
+
+ def test_relative_url_root_is_respected
+ add_host!
+ assert_equal("https://www.basecamphq.com/subdir/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https", script_name: "/subdir")
+ )
+ end
+
+ def test_relative_url_root_is_respected_with_environment_variable
+ # `config.relative_url_root` is set by ENV['RAILS_RELATIVE_URL_ROOT']
+ w = Class.new {
+ config = ActionDispatch::Routing::RouteSet::Config.new "/subdir"
+ r = ActionDispatch::Routing::RouteSet.new(config)
+ r.draw { ActiveSupport::Deprecation.silence { get ":controller(/:action(/:id(.:format)))" } }
+ include r.url_helpers
+ }
+ add_host!(w)
+ assert_equal("https://www.basecamphq.com/subdir/c/a/i",
+ w.new.url_for(controller: "c", action: "a", id: "i", protocol: "https")
+ )
+ end
+
+ def test_named_routes
+ with_routing do |set|
+ set.draw do
+ get "this/is/verbose", to: "home#index", as: :no_args
+ get "home/sweet/home/:user", to: "home#index", as: :home
+ end
+
+ # We need to create a new class in order to install the new named route.
+ kls = Class.new { include set.url_helpers }
+
+ controller = kls.new
+ assert_respond_to controller, :home_url
+ assert_equal "http://www.basecamphq.com/home/sweet/home/again",
+ controller.send(:home_url, host: "www.basecamphq.com", user: "again")
+
+ assert_equal("/home/sweet/home/alabama", controller.send(:home_path, user: "alabama", host: "unused"))
+ assert_equal("http://www.basecamphq.com/home/sweet/home/alabama", controller.send(:home_url, user: "alabama", host: "www.basecamphq.com"))
+ assert_equal("http://www.basecamphq.com/this/is/verbose", controller.send(:no_args_url, host: "www.basecamphq.com"))
+ end
+ end
+
+ def test_relative_url_root_is_respected_for_named_routes
+ with_routing do |set|
+ set.draw do
+ get "/home/sweet/home/:user", to: "home#index", as: :home
+ end
+
+ kls = Class.new { include set.url_helpers }
+ controller = kls.new
+
+ assert_equal "http://www.basecamphq.com/subdir/home/sweet/home/again",
+ controller.send(:home_url, host: "www.basecamphq.com", user: "again", script_name: "/subdir")
+ end
+ end
+
+ def test_using_nil_script_name_properly_concats_with_original_script_name
+ add_host!
+ assert_equal("https://www.basecamphq.com/subdir/c/a/i",
+ W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https", script_name: nil, original_script_name: "/subdir")
+ )
+ end
+
+ def test_only_path
+ with_routing do |set|
+ set.draw do
+ get "home/sweet/home/:user", to: "home#index", as: :home
+
+ ActiveSupport::Deprecation.silence do
+ get ":controller/:action/:id"
+ end
+ end
+
+ # We need to create a new class in order to install the new named route.
+ kls = Class.new { include set.url_helpers }
+ controller = kls.new
+ assert_respond_to controller, :home_url
+ assert_equal "/brave/new/world",
+ controller.url_for(controller: "brave", action: "new", id: "world", only_path: true)
+
+ assert_equal("/home/sweet/home/alabama", controller.home_path(user: "alabama", host: "unused"))
+ assert_equal("/home/sweet/home/alabama", controller.home_path("alabama"))
+ end
+ end
+
+ def test_one_parameter
+ assert_equal("/c/a?param=val",
+ W.new.url_for(only_path: true, controller: "c", action: "a", param: "val")
+ )
+ end
+
+ def test_two_parameters
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", p1: "X1", p2: "Y2")
+ params = extract_params(url)
+ assert_equal({ p1: "X1" }.to_query, params[0])
+ assert_equal({ p2: "Y2" }.to_query, params[1])
+ end
+
+ def test_hash_parameter
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", query: { name: "Bob", category: "prof" })
+ params = extract_params(url)
+ assert_equal({ "query[category]" => "prof" }.to_query, params[0])
+ assert_equal({ "query[name]" => "Bob" }.to_query, params[1])
+ end
+
+ def test_array_parameter
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", query: ["Bob", "prof"])
+ params = extract_params(url)
+ assert_equal({ "query[]" => "Bob" }.to_query, params[0])
+ assert_equal({ "query[]" => "prof" }.to_query, params[1])
+ end
+
+ def test_hash_recursive_parameters
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", query: { person: { name: "Bob", position: "prof" }, hobby: "piercing" })
+ params = extract_params(url)
+ assert_equal({ "query[hobby]" => "piercing" }.to_query, params[0])
+ assert_equal({ "query[person][name]" => "Bob" }.to_query, params[1])
+ assert_equal({ "query[person][position]" => "prof" }.to_query, params[2])
+ end
+
+ def test_hash_recursive_and_array_parameters
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", id: 101, query: { person: { name: "Bob", position: ["prof", "art director"] }, hobby: "piercing" })
+ assert_match(%r(^/c/a/101), url)
+ params = extract_params(url)
+ assert_equal({ "query[hobby]" => "piercing" }.to_query, params[0])
+ assert_equal({ "query[person][name]" => "Bob" }.to_query, params[1])
+ assert_equal({ "query[person][position][]" => "art director" }.to_query, params[2])
+ assert_equal({ "query[person][position][]" => "prof" }.to_query, params[3])
+ end
+
+ def test_url_action_controller_parameters
+ add_host!
+ assert_raise(ActionController::UnfilteredParameters) do
+ W.new.url_for(ActionController::Parameters.new(controller: "c", action: "a", protocol: "javascript", f: "%0Aeval(name)"))
+ end
+ end
+
+ def test_path_generation_for_symbol_parameter_keys
+ assert_generates("/image", controller: :image)
+ end
+
+ def test_named_routes_with_nil_keys
+ with_routing do |set|
+ set.draw do
+ get "posts.:format", to: "posts#index", as: :posts
+ get "/", to: "posts#index", as: :main
+ end
+
+ # We need to create a new class in order to install the new named route.
+ kls = Class.new { include set.url_helpers }
+ kls.default_url_options[:host] = "www.basecamphq.com"
+
+ controller = kls.new
+ params = { action: :index, controller: :posts, format: :xml }
+ assert_equal("http://www.basecamphq.com/posts.xml", controller.send(:url_for, params))
+ params[:format] = nil
+ assert_equal("http://www.basecamphq.com/", controller.send(:url_for, params))
+ end
+ end
+
+ def test_multiple_includes_maintain_distinct_options
+ first_class = Class.new { include ActionController::UrlFor }
+ second_class = Class.new { include ActionController::UrlFor }
+
+ first_host, second_host = "firsthost.com", "secondhost.com"
+
+ first_class.default_url_options[:host] = first_host
+ second_class.default_url_options[:host] = second_host
+
+ assert_equal first_host, first_class.default_url_options[:host]
+ assert_equal second_host, second_class.default_url_options[:host]
+ end
+
+ def test_with_stringified_keys
+ assert_equal("/c", W.new.url_for("controller" => "c", "only_path" => true))
+ assert_equal("/c/a", W.new.url_for("controller" => "c", "action" => "a", "only_path" => true))
+ end
+
+ def test_with_hash_with_indifferent_access
+ W.default_url_options[:controller] = "d"
+ W.default_url_options[:only_path] = false
+ assert_equal("/c", W.new.url_for(ActiveSupport::HashWithIndifferentAccess.new("controller" => "c", "only_path" => true)))
+
+ W.default_url_options[:action] = "b"
+ assert_equal("/c/a", W.new.url_for(ActiveSupport::HashWithIndifferentAccess.new("controller" => "c", "action" => "a", "only_path" => true)))
+ end
+
+ def test_url_params_with_nil_to_param_are_not_in_url
+ assert_equal("/c/a", W.new.url_for(only_path: true, controller: "c", action: "a", id: Struct.new(:to_param).new(nil)))
+ end
+
+ def test_false_url_params_are_included_in_query
+ assert_equal("/c/a?show=false", W.new.url_for(only_path: true, controller: "c", action: "a", show: false))
+ end
+
+ def test_url_generation_with_array_and_hash
+ with_routing do |set|
+ set.draw do
+ namespace :admin do
+ resources :posts
+ end
+ end
+
+ kls = Class.new { include set.url_helpers }
+ kls.default_url_options[:host] = "www.basecamphq.com"
+
+ controller = kls.new
+ assert_equal("http://www.basecamphq.com/admin/posts/new?param=value",
+ controller.send(:url_for, [:new, :admin, :post, { param: "value" }])
+ )
+ end
+ end
+
+ def test_url_for_with_array_is_unmodified
+ with_routing do |set|
+ set.draw do
+ namespace :admin do
+ resources :posts
+ end
+ end
+
+ kls = Class.new { include set.url_helpers }
+ kls.default_url_options[:host] = "www.basecamphq.com"
+
+ original_components = [:new, :admin, :post, { param: "value" }]
+ components = original_components.dup
+
+ kls.new.url_for(components)
+
+ assert_equal(original_components, components)
+ end
+ end
+
+ def test_default_params_first_empty
+ with_routing do |set|
+ set.draw do
+ get "(:param1)/test(/:param2)" => "index#index",
+ defaults: {
+ param1: 1,
+ param2: 2
+ },
+ constraints: {
+ param1: /\d*/,
+ param2: /\d+/
+ }
+ end
+
+ kls = Class.new { include set.url_helpers }
+ kls.default_url_options[:host] = "www.basecamphq.com"
+
+ assert_equal "http://www.basecamphq.com/test", kls.new.url_for(controller: "index", param1: "1")
+ end
+ end
+
+ private
+ def extract_params(url)
+ url.split("?", 2).last.split("&").sort
+ end
+ end
+ end
+end
diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb
new file mode 100644
index 0000000000..ca83b850d5
--- /dev/null
+++ b/actionpack/test/controller/url_rewriter_test.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_controllers"
+
+class UrlRewriterTests < ActionController::TestCase
+ class Rewriter
+ def initialize(request)
+ @options = {
+ host: request.host_with_port,
+ protocol: request.protocol
+ }
+ end
+
+ def rewrite(routes, options)
+ routes.url_for(@options.merge(options))
+ end
+ end
+
+ def setup
+ @params = {}
+ @rewriter = Rewriter.new(@request)
+ @routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
+ r.draw do
+ ActiveSupport::Deprecation.silence do
+ get ":controller(/:action(/:id))"
+ end
+ end
+ end
+ end
+
+ def test_port
+ assert_equal("http://test.host:1271/c/a/i",
+ @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", port: 1271)
+ )
+ end
+
+ def test_protocol_with_and_without_separator
+ assert_equal("https://test.host/c/a/i",
+ @rewriter.rewrite(@routes, protocol: "https", controller: "c", action: "a", id: "i")
+ )
+
+ assert_equal("https://test.host/c/a/i",
+ @rewriter.rewrite(@routes, protocol: "https://", controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_user_name_and_password
+ assert_equal(
+ "http://david:secret@test.host/c/a/i",
+ @rewriter.rewrite(@routes, user: "david", password: "secret", controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_user_name_and_password_with_escape_codes
+ assert_equal(
+ "http://openid.aol.com%2Fnextangler:one+two%3F@test.host/c/a/i",
+ @rewriter.rewrite(@routes, user: "openid.aol.com/nextangler", password: "one two?", controller: "c", action: "a", id: "i")
+ )
+ end
+
+ def test_anchor
+ assert_equal(
+ "http://test.host/c/a/i#anchor",
+ @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", anchor: "anchor")
+ )
+ end
+
+ def test_anchor_should_call_to_param
+ assert_equal(
+ "http://test.host/c/a/i#anchor",
+ @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", anchor: Struct.new(:to_param).new("anchor"))
+ )
+ end
+
+ def test_anchor_should_be_uri_escaped
+ assert_equal(
+ "http://test.host/c/a/i#anc/hor",
+ @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", anchor: Struct.new(:to_param).new("anc/hor"))
+ )
+ end
+
+ def test_trailing_slash
+ options = { controller: "foo", action: "bar", id: "3", only_path: true }
+ assert_equal "/foo/bar/3", @rewriter.rewrite(@routes, options)
+ assert_equal "/foo/bar/3?query=string", @rewriter.rewrite(@routes, options.merge(query: "string"))
+ options.update(trailing_slash: true)
+ assert_equal "/foo/bar/3/", @rewriter.rewrite(@routes, options)
+ options.update(query: "string")
+ assert_equal "/foo/bar/3/?query=string", @rewriter.rewrite(@routes, options)
+ end
+end
diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb
new file mode 100644
index 0000000000..4a10637b54
--- /dev/null
+++ b/actionpack/test/controller/webservice_test.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/json/decoding"
+
+class WebServiceTest < ActionDispatch::IntegrationTest
+ class TestController < ActionController::Base
+ def assign_parameters
+ if params[:full]
+ render plain: dump_params_keys
+ else
+ render plain: (params.keys - ["controller", "action"]).sort.join(", ")
+ end
+ end
+
+ def dump_params_keys(hash = params)
+ hash.keys.sort.inject("") do |s, k|
+ value = hash[k]
+
+ if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
+ value = "(#{dump_params_keys(value)})"
+ else
+ value = ""
+ end
+
+ s += ", " unless s.empty?
+ s += "#{k}#{value}"
+ end
+ end
+ end
+
+ def setup
+ @controller = TestController.new
+ @integration_session = nil
+ end
+
+ def test_check_parameters
+ with_test_route_set do
+ get "/"
+ assert_equal "", @controller.response.body
+ end
+ end
+
+ def test_post_json
+ with_test_route_set do
+ post "/",
+ params: '{"entry":{"summary":"content..."}}',
+ headers: { "CONTENT_TYPE" => "application/json" }
+
+ assert_equal "entry", @controller.response.body
+ assert @controller.params.has_key?(:entry)
+ assert_equal "content...", @controller.params["entry"]["summary"]
+ end
+ end
+
+ def test_put_json
+ with_test_route_set do
+ put "/",
+ params: '{"entry":{"summary":"content..."}}',
+ headers: { "CONTENT_TYPE" => "application/json" }
+
+ assert_equal "entry", @controller.response.body
+ assert @controller.params.has_key?(:entry)
+ assert_equal "content...", @controller.params["entry"]["summary"]
+ end
+ end
+
+ def test_register_and_use_json_simple
+ with_test_route_set do
+ with_params_parsers Mime[:json] => Proc.new { |data| ActiveSupport::JSON.decode(data)["request"].with_indifferent_access } do
+ post "/",
+ params: '{"request":{"summary":"content...","title":"JSON"}}',
+ headers: { "CONTENT_TYPE" => "application/json" }
+
+ assert_equal "summary, title", @controller.response.body
+ assert @controller.params.has_key?(:summary)
+ assert @controller.params.has_key?(:title)
+ assert_equal "content...", @controller.params["summary"]
+ assert_equal "JSON", @controller.params["title"]
+ end
+ end
+ end
+
+ def test_use_json_with_empty_request
+ with_test_route_set do
+ assert_nothing_raised { post "/", headers: { "CONTENT_TYPE" => "application/json" } }
+ assert_equal "", @controller.response.body
+ end
+ end
+
+ def test_dasherized_keys_as_json
+ with_test_route_set do
+ post "/?full=1",
+ params: '{"first-key":{"sub-key":"..."}}',
+ headers: { "CONTENT_TYPE" => "application/json" }
+ assert_equal "action, controller, first-key(sub-key), full", @controller.response.body
+ assert_equal "...", @controller.params["first-key"]["sub-key"]
+ end
+ end
+
+ def test_parsing_json_doesnot_rescue_exception
+ req = Class.new(ActionDispatch::Request) do
+ def params_parsers
+ { json: Proc.new { |data| raise Interrupt } }
+ end
+
+ def content_length; get_header("rack.input").length; end
+ end.new("rack.input" => StringIO.new('{"title":"JSON"}}'), "CONTENT_TYPE" => "application/json")
+
+ assert_raises(Interrupt) do
+ req.request_parameters
+ end
+ end
+
+ private
+ def with_params_parsers(parsers = {})
+ old_session = @integration_session
+ original_parsers = ActionDispatch::Request.parameter_parsers
+ ActionDispatch::Request.parameter_parsers = original_parsers.merge parsers
+ reset!
+ yield
+ ensure
+ ActionDispatch::Request.parameter_parsers = original_parsers
+ @integration_session = old_session
+ end
+
+ def with_test_route_set
+ with_routing do |set|
+ set.draw do
+ match "/", to: "web_service_test/test#assign_parameters", via: :all
+ end
+ yield
+ end
+ end
+end