diff options
Diffstat (limited to 'actionpack/test/controller')
89 files changed, 21191 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..f9a037e3cc --- /dev/null +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -0,0 +1,493 @@ +# 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 + begin + 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 + end + + def test_template_objects_exist + process :assign_this + assert !@controller.instance_variable_defined?(:"@hi") + assert @controller.instance_variable_get(:"@howdy") + end + + def test_template_objects_missing + process :nothing + assert !@controller.instance_variable_defined?(:@howdy) + end + + def test_empty_flash + process :flash_me_naked + assert flash.empty? + end + + def test_flash_exist + process :flash_me + assert flash.any? + assert flash["hello"].present? + end + + def test_flash_does_not_exist + process :nothing + assert flash.empty? + end + + def test_session_exist + process :session_stuffing + assert_equal "turkey", session["xmas"] + end + + def session_does_not_exist + process :nothing + assert session.empty? + 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 @response.server_error? + + process :response599 + assert @response.server_error? + + process :response404 + assert !@response.server_error? + end + + def test_missing_response_code + process :response404 + assert @response.not_found? + end + + def test_client_error_response_code + process :response404 + assert @response.client_error? + end + + def test_redirect_url_match + process :redirect_external + assert @response.redirect? + assert_match(/rubyonrails/, @response.redirect_url) + assert !/perloffrails/.match(@response.redirect_url) + end + + def test_redirection + process :redirect_internal + assert @response.redirect? + + process :redirect_external + assert @response.redirect? + + process :nothing + assert !@response.redirect? + end + + def test_successful_response_code + process :nothing + assert @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..fd1997f26c --- /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 @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..07459c3753 --- /dev/null +++ b/actionpack/test/controller/api/force_ssl_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ForceSSLApiController < ActionController::API + force_ssl + + 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..9ac82c0d65 --- /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 !@empty.performed? + @empty.response_body = ["sweet"] + assert @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, + params: { "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..e0300539c9 --- /dev/null +++ b/actionpack/test/controller/caching_test.rb @@ -0,0 +1,503 @@ +# 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 !@controller.fragment_exist?("other_name") + end + + def test_fragment_exist_with_caching_disabled + @controller.perform_caching = false + @store.write("views/name", "value") + assert !@controller.fragment_exist?("name") + assert !@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 !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 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 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 + + 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) + return 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 NoDependenciesController.new.view_cache_dependencies.empty? + 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..9f0a9dec7a --- /dev/null +++ b/actionpack/test/controller/filters_test.rb @@ -0,0 +1,1050 @@ +# 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 + + 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 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 !@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 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 + begin + test_process(controller, "raises_both") + rescue Before, After + end + end + assert_raise Before do + begin + test_process(controller, "raises_both") + rescue After + end + 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..f31a4d9329 --- /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 !@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 !@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 @hash.empty? + @hash["zomg"] = "bears" + assert !@hash.empty? + @hash.clear + assert @hash.empty? + 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..d92ae0b817 --- /dev/null +++ b/actionpack/test/controller/flash_test.rb @@ -0,0 +1,371 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/key_generator" + +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") + + 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 + + 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 + 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..84ac1fda3c --- /dev/null +++ b/actionpack/test/controller/force_ssl_test.rb @@ -0,0 +1,333 @@ +# 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 + force_ssl +end + +class ForceSSLCustomOptions < ForceSSLController + 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 + + 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 + force_ssl only: :cheeseburger +end + +class ForceSSLExceptAction < ForceSSLController + force_ssl except: :banana +end + +class ForceSSLIfCondition < ForceSSLController + force_ssl if: :use_force_ssl? + + def use_force_ssl? + action_name == "cheeseburger" + end +end + +class ForceSSLFlash < ForceSSLController + force_ssl except: [:banana, :set_flash, :use_flash] + + 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..76ff784926 --- /dev/null +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -0,0 +1,282 @@ +# 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 !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.merge!(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..672aa1351c --- /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 = "".dup + 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..fd1c5e693f --- /dev/null +++ b/actionpack/test/controller/integration_test.rb @@ -0,0 +1,1122 @@ +# 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 !@session.https? + @session.https! + assert @session.https? + @session.https! false + assert !@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 !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.__send__(: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 + refute_same previous_html_document, html_document + 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 + + refute_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 request.env["rack.input"].string.empty? + assert_equal "foo=bar", request.env["QUERY_STRING"] + assert_equal "foo=bar", request.query_string + assert_equal "bar", request.parameters["foo"] + assert 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 + + 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..1ccbee8a0f --- /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/"#{Digest::MD5.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..f0f106c8ba --- /dev/null +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -0,0 +1,371 @@ +# 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 + + def last_payload + @last_payload + end + 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 = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname("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..c235c9df86 --- /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_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] + + refute response_headers.key?("X-Frame-Options") + refute response_headers.key?("X-Content-Type-Options") + refute 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..f9ffd5f54c --- /dev/null +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -0,0 +1,843 @@ +# 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 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_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::UnknownFormat) 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::UnknownFormat) 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..b049022a06 --- /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 = "".dup + + 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..d9f200b2a7 --- /dev/null +++ b/actionpack/test/controller/new_base/base_test.rb @@ -0,0 +1,134 @@ +# 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 + # :api: plugin + test "simple dispatching" do + get "/dispatching/simple/index" + + assert_body "success" + assert_status 200 + assert_content_type "text/plain; charset=utf-8" + end + + # :api: plugin + test "directly modifying response body" do + get "/dispatching/simple/modify_response_body" + + assert_body "success" + end + + # :api: plugin + 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..07fbadae9f --- /dev/null +++ b/actionpack/test/controller/new_base/render_context_test.rb @@ -0,0 +1,54 @@ +# 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 + + # 3) Set view_context to self + private 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 "<p>hello world</p>" + 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..e33a99068f --- /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 ERB::Util.h(nil).blank? + end + + test "escapeHTML should escape strings" do + assert_equal "<>"", 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..43cabae7d2 --- /dev/null +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -0,0 +1,279 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" +require "active_support/core_ext/hash/transform_values" + +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 @params[:person].permitted? + assert @params[:person][:name].permitted? + end + + test "[] retains unpermitted status" do + assert_not @params[:person].permitted? + assert_not @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_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 "empty? returns true when params contains no key/value pairs" do + params = ActionController::Parameters.new + assert params.empty? + end + + test "empty? returns false when any params are present" do + refute @params.empty? + end + + test "except retains permitted status" do + @params.permit! + assert @params.except(:person).permitted? + assert @params[:person].except(:name).permitted? + end + + test "except retains unpermitted status" do + assert_not @params.except(:person).permitted? + assert_not @params[:person].except(:name).permitted? + end + + test "fetch retains permitted status" do + @params.permit! + assert @params.fetch(:person).permitted? + assert @params[:person].fetch(:name).permitted? + end + + test "fetch retains unpermitted status" do + assert_not @params.fetch(:person).permitted? + assert_not @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 + refute @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") + refute 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 + refute @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 + refute @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 @params.reject { |k| k == "person" }.permitted? + end + + test "reject retains unpermitted status" do + @params.permit! + assert @params.reject { |k| k == "person" }.permitted? + end + + test "select retains permitted status" do + @params.permit! + assert @params.select { |k| k == "person" }.permitted? + end + + test "select retains unpermitted status" do + assert_not @params.select { |k| k == "person" }.permitted? + end + + test "slice retains permitted status" do + @params.permit! + assert @params.slice(:person).permitted? + end + + test "slice retains unpermitted status" do + assert_not @params.slice(:person).permitted? + end + + test "transform_keys retains permitted status" do + @params.permit! + assert @params.transform_keys { |k| k }.permitted? + end + + test "transform_keys retains unpermitted status" do + assert_not @params.transform_keys { |k| k }.permitted? + end + + test "transform_values retains permitted status" do + @params.permit! + assert @params.transform_values { |v| v }.permitted? + end + + test "transform_values retains unpermitted status" do + assert_not @params.transform_values { |v| v }.permitted? + 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") + refute 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 @params.values_at(:person).first.permitted? + assert @params[:person].values_at(:name).first.permitted? + end + + test "values_at retains unpermitted status" do + assert_not @params.values_at(:person).first.permitted? + assert_not @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 + + if Hash.method_defined?(:dig) + 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 + else + test "ActionController::Parameters does not respond to #dig on Ruby 2.2" do + assert_not ActionController::Parameters.method_defined?(:dig) + assert_not @params.respond_to?(:dig) + end + 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..1e8b71d789 --- /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 "permits parameters that are whitelisted" do + params = ActionController::Parameters.new( + book: { pages: 65 }, + format: "json") + permitted = params.permit book: [:pages] + assert 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..f5833aff46 --- /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 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 @params.permitted? + end + + test "deep_dup @permitted is being copied" do + @params.permit! + assert @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..dcf848a620 --- /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 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..49dede03c2 --- /dev/null +++ b/actionpack/test/controller/parameters/mutators_test.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" +require "active_support/core_ext/hash/transform_values" + +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 @params.delete(:person).permitted? + end + + test "delete retains unpermitted status" do + assert_not @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 @params.delete_if { |k| k == "person" }.permitted? + end + + test "delete_if retains unpermitted status" do + assert_not @params.delete_if { |k| k == "person" }.permitted? + end + + test "extract! retains permitted status" do + @params.permit! + assert @params.extract!(:person).permitted? + end + + test "extract! retains unpermitted status" do + assert_not @params.extract!(:person).permitted? + end + + test "keep_if retains permitted status" do + @params.permit! + assert @params.keep_if { |k, v| k == "person" }.permitted? + end + + test "keep_if retains unpermitted status" do + assert_not @params.keep_if { |k, v| k == "person" }.permitted? + end + + test "reject! retains permitted status" do + @params.permit! + assert @params.reject! { |k| k == "person" }.permitted? + end + + test "reject! retains unpermitted status" do + assert_not @params.reject! { |k| k == "person" }.permitted? + end + + test "select! retains permitted status" do + @params.permit! + assert @params.select! { |k| k != "person" }.permitted? + end + + test "select! retains unpermitted status" do + assert_not @params.select! { |k| k != "person" }.permitted? + end + + test "slice! retains permitted status" do + @params.permit! + assert @params.slice!(:person).permitted? + end + + test "slice! retains unpermitted status" do + assert_not @params.slice!(:person).permitted? + end + + test "transform_keys! retains permitted status" do + @params.permit! + assert @params.transform_keys! { |k| k }.permitted? + end + + test "transform_keys! retains unpermitted status" do + assert_not @params.transform_keys! { |k| k }.permitted? + end + + test "transform_values! retains permitted status" do + @params.permit! + assert @params.transform_values! { |v| v }.permitted? + end + + test "transform_values! retains unpermitted status" do + assert_not @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..c9fcc483ee --- /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 !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 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..ebdaca0162 --- /dev/null +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -0,0 +1,513 @@ +# 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 !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 permitted.permitted? + assert permitted.empty? + end + + test "key: permitted scalar values" do + values = ["a", :a, nil] + values += [0, 1.0, 2**128, BigDecimal.new(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 !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 !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 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 !@params.merge(a: "b").permitted? + end + + test "permitted is sticky beyond merges" do + @params.permit! + assert @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 @params.merge!(a: "b").permitted? + end + + test "permitted is sticky beyond merge!" do + @params.permit! + assert @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] + refute_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] + refute_predicate merged_params[:person], :empty? + end + + test "not permitted is sticky beyond reverse_merge" do + refute_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] + refute_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] + refute_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.permit! + assert @params.permitted? + assert @params[:person].permitted? + assert @params[:person][:name].permitted? + assert @params[:person][:addresses][0].permitted? + end + + test "permitted takes a default value when Parameters.permit_all_parameters is set" do + begin + ActionController::Parameters.permit_all_parameters = true + params = ActionController::Parameters.new(person: { + age: "32", name: { first: "David", last: "Heinemeier Hansson" } + }) + + assert params.slice(:person).permitted? + assert params[:person][:name].permitted? + ensure + ActionController::Parameters.permit_all_parameters = false + end + 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 + begin + 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 + 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 + begin + 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 + 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) + refute params.permit(foo: []).has_key?(:foo) + refute params.permit(foo: [:bar]).has_key?(:foo) + refute 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..823f01d82a --- /dev/null +++ b/actionpack/test/controller/parameters/serialization_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" +require "active_support/core_ext/string/strip" + +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 roundtripped.permitted? + end + + test "yaml backwardscompatible with psych 2.0.8 format" do + params = YAML.load <<-end_of_yaml.strip_heredoc + --- !ruby/hash:ActionController::Parameters + key: :value + end_of_yaml + + assert_equal :value, params[:key] + assert_not params.permitted? + end + + test "yaml backwardscompatible with psych 2.0.9+ format" do + params = YAML.load(<<-end_of_yaml.strip_heredoc) + --- !ruby/hash-with-ivars:ActionController::Parameters + elements: + key: :value + ivars: + :@permitted: false + end_of_yaml + + assert_equal :value, params[:key] + assert_not params.permitted? + 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..df68ef25a3 --- /dev/null +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -0,0 +1,408 @@ +# 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" => "☃", "username" => "sikachu" } + assert_parameters("authenticity_token" => "pwned", "_method" => "put", "utf8" => "☃", "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 +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..7546b36bc3 --- /dev/null +++ b/actionpack/test/controller/redirect_test.rb @@ -0,0 +1,358 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class Workshop + extend ActiveModel::Naming + include ActiveModel::Conversion + 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 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_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_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_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_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..3619afc513 --- /dev/null +++ b/actionpack/test/controller/render_test.rb @@ -0,0 +1,810 @@ +# 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_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_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 + + 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_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_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 @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 @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 @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 @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 @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 @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) + %("#{Digest::MD5.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 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::UnknownFormat) 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 @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 @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 @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 @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 @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 @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 @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 +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 @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 @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..12ae95d602 --- /dev/null +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -0,0 +1,998 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/log_subscriber/test_helper" + +# 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_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_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}<=\?" + assert_match(/#{regexp}/, @response.body) + end + end +end + +class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase + class NullSessionDummyKeyGenerator + def generate_key(secret) + "03312270731a2ed0d11ed091c2338a06" + end + end + + def setup + @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.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 @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..07f8c9dd8a --- /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 ActionView::TemplateError do + render plain: "action_view templater error" + end + + rescue_from IOError do + render plain: "io error" + end + + before_action(only: :before_action_raises) { raise "umm nice" } + + def before_action_raises + end + + def raises + render plain: "already rendered" + raise "don't panic!" + end + + def method_not_allowed + raise ActionController::MethodNotAllowed.new(:get, :head, :put) + end + + def not_implemented + raise ActionController::NotImplemented.new(:get, :put) + 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 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 +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 + + def b00m + raise "b00m" + 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) + get "b00m", to: ::RescueTest::TestController.action(:b00m) + 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..871fba7e73 --- /dev/null +++ b/actionpack/test/controller/resources_test.rb @@ -0,0 +1,1369 @@ +# 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_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.merge!(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.merge!(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..fefb84e095 --- /dev/null +++ b/actionpack/test/controller/routing_test.rb @@ -0,0 +1,2092 @@ +# 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.unpack("H2").first.upcase } + + @segment = "#{safe.join}#{unsafe.join}".freeze + @escaped = "#{safe.join}#{hex.join}".freeze + 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_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_precidence + 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-facebook", controller: "content" }, rs.recognize_path("/content/auth-facebook")) + + assert_equal "/content/auth_google", url_for(rs, controller: "content", action: "auth_google") + assert_equal "/content/auth-facebook", url_for(rs, controller: "content", action: "auth-facebook") + 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".dup # '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 !Object.const_defined?("Profiler__"), "Profiler should not be loaded" + set.draw do + get "/profile" => "profile#index" + end + + request_path_params("/profile") rescue nil + + assert !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..a96c9c519b --- /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 runner.respond_to?(:hi) + assert runner.respond_to?(: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..fd2399e433 --- /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"', 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"', 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.mpg" => "video/mpeg", + "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..92b1c75443 --- /dev/null +++ b/actionpack/test/controller/test_case_test.rb @@ -0,0 +1,1120 @@ +# 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 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_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 + assert_select "root > area + p" + 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_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".freeze, frozens: ["icy".freeze].freeze, deepfreeze: { frozen: "icy".freeze }.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 @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_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_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 file.respond_to?(:tempfile), "expected tempfile should respond on fixture file object, got nothing" + 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 +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..a7c7356921 --- /dev/null +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -0,0 +1,193 @@ +# 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 + #match '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 { + 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..cf11227897 --- /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 controller.respond_to?(: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..0f79c83b6d --- /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) #.new(@request, @params) + @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 |