diff options
Diffstat (limited to 'actionpack/test')
297 files changed, 41008 insertions, 0 deletions
diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb new file mode 100644 index 0000000000..4512ea27b3 --- /dev/null +++ b/actionpack/test/abstract/callbacks_test.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module AbstractController + module Testing + class ControllerWithCallbacks < AbstractController::Base + include AbstractController::Callbacks + end + + class Callback1 < ControllerWithCallbacks + set_callback :process_action, :before, :first + + def first + @text = "Hello world" + end + + def index + self.response_body = @text + end + end + + class TestCallbacks1 < ActiveSupport::TestCase + test "basic callbacks work" do + controller = Callback1.new + controller.process(:index) + assert_equal "Hello world", controller.response_body + end + end + + class Callback2 < ControllerWithCallbacks + before_action :first + after_action :second + around_action :aroundz + + def first + @text = "Hello world" + end + + def second + @second = "Goodbye" + end + + def aroundz + @aroundz = "FIRST" + yield + @aroundz += "SECOND" + end + + def index + @text ||= nil + self.response_body = @text.to_s + end + end + + class Callback2Overwrite < Callback2 + before_action :first, except: :index + end + + class TestCallbacks2 < ActiveSupport::TestCase + def setup + @controller = Callback2.new + end + + test "before_action works" do + @controller.process(:index) + assert_equal "Hello world", @controller.response_body + end + + test "after_action works" do + @controller.process(:index) + assert_equal "Goodbye", @controller.instance_variable_get("@second") + end + + test "around_action works" do + @controller.process(:index) + assert_equal "FIRSTSECOND", @controller.instance_variable_get("@aroundz") + end + + test "before_action with overwritten condition" do + @controller = Callback2Overwrite.new + @controller.process(:index) + assert_equal "", @controller.response_body + end + end + + class Callback3 < ControllerWithCallbacks + before_action do |c| + c.instance_variable_set("@text", "Hello world") + end + + after_action do |c| + c.instance_variable_set("@second", "Goodbye") + end + + def index + self.response_body = @text + end + end + + class TestCallbacks3 < ActiveSupport::TestCase + def setup + @controller = Callback3.new + end + + test "before_action works with procs" do + @controller.process(:index) + assert_equal "Hello world", @controller.response_body + end + + test "after_action works with procs" do + @controller.process(:index) + assert_equal "Goodbye", @controller.instance_variable_get("@second") + end + end + + class CallbacksWithConditions < ControllerWithCallbacks + before_action :list, only: :index + before_action :authenticate, except: :index + + def index + self.response_body = @list.join(", ") + end + + def sekrit_data + self.response_body = (@list + [@authenticated]).join(", ") + end + + private + def list + @list = ["Hello", "World"] + end + + def authenticate + @list ||= [] + @authenticated = "true" + end + end + + class TestCallbacksWithConditions < ActiveSupport::TestCase + def setup + @controller = CallbacksWithConditions.new + end + + test "when :only is specified, a before action is triggered on that action" do + @controller.process(:index) + assert_equal "Hello, World", @controller.response_body + end + + test "when :only is specified, a before action is not triggered on other actions" do + @controller.process(:sekrit_data) + assert_equal "true", @controller.response_body + end + + test "when :except is specified, an after action is not triggered on that action" do + @controller.process(:index) + assert_not @controller.instance_variable_defined?("@authenticated") + end + end + + class CallbacksWithArrayConditions < ControllerWithCallbacks + before_action :list, only: [:index, :listy] + before_action :authenticate, except: [:index, :listy] + + def index + self.response_body = @list.join(", ") + end + + def sekrit_data + self.response_body = (@list + [@authenticated]).join(", ") + end + + private + def list + @list = ["Hello", "World"] + end + + def authenticate + @list = [] + @authenticated = "true" + end + end + + class TestCallbacksWithArrayConditions < ActiveSupport::TestCase + def setup + @controller = CallbacksWithArrayConditions.new + end + + test "when :only is specified with an array, a before action is triggered on that action" do + @controller.process(:index) + assert_equal "Hello, World", @controller.response_body + end + + test "when :only is specified with an array, a before action is not triggered on other actions" do + @controller.process(:sekrit_data) + assert_equal "true", @controller.response_body + end + + test "when :except is specified with an array, an after action is not triggered on that action" do + @controller.process(:index) + assert_not @controller.instance_variable_defined?("@authenticated") + end + end + + class ChangedConditions < Callback2 + before_action :first, only: :index + + def not_index + @text ||= nil + self.response_body = @text.to_s + end + end + + class TestCallbacksWithChangedConditions < ActiveSupport::TestCase + def setup + @controller = ChangedConditions.new + end + + test "when a callback is modified in a child with :only, it works for the :only action" do + @controller.process(:index) + assert_equal "Hello world", @controller.response_body + end + + test "when a callback is modified in a child with :only, it does not work for other actions" do + @controller.process(:not_index) + assert_equal "", @controller.response_body + end + end + + class SetsResponseBody < ControllerWithCallbacks + before_action :set_body + + def index + self.response_body = "Fail" + end + + def set_body + self.response_body = "Success" + end + end + + class TestHalting < ActiveSupport::TestCase + test "when a callback sets the response body, the action should not be invoked" do + controller = SetsResponseBody.new + controller.process(:index) + assert_equal "Success", controller.response_body + end + end + + class CallbacksWithArgs < ControllerWithCallbacks + set_callback :process_action, :before, :first + + def first + @text = "Hello world" + end + + def index(text) + self.response_body = @text + text + end + end + + class TestCallbacksWithArgs < ActiveSupport::TestCase + test "callbacks still work when invoking process with multiple arguments" do + controller = CallbacksWithArgs.new + controller.process(:index, " Howdy!") + assert_equal "Hello world Howdy!", controller.response_body + end + end + end +end diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb new file mode 100644 index 0000000000..6db045fcd7 --- /dev/null +++ b/actionpack/test/abstract/collector_test.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module AbstractController + module Testing + class MyCollector + include AbstractController::Collector + attr_accessor :responses + + def initialize + @responses = [] + end + + def custom(mime, *args, &block) + @responses << [mime, args, block] + end + end + + class TestCollector < ActiveSupport::TestCase + test "responds to default mime types" do + collector = MyCollector.new + assert_respond_to collector, :html + assert_respond_to collector, :text + end + + test "does not respond to unknown mime types" do + collector = MyCollector.new + assert_not_respond_to collector, :unknown + end + + test "register mime types on method missing" do + AbstractController::Collector.remove_method :js + begin + collector = MyCollector.new + assert_not_respond_to collector, :js + collector.js + assert_respond_to collector, :js + ensure + unless AbstractController::Collector.method_defined? :js + AbstractController::Collector.generate_method_for_mime :js + end + end + end + + test "does not register unknown mime types" do + collector = MyCollector.new + assert_raise NoMethodError do + collector.unknown + end + end + + test "generated methods call custom with arguments received" do + collector = MyCollector.new + collector.html + collector.text(:foo) + collector.js(:bar) { :baz } + assert_equal [Mime[:html], [], nil], collector.responses[0] + assert_equal [Mime[:text], [:foo], nil], collector.responses[1] + assert_equal [Mime[:js], [:bar]], collector.responses[2][0, 2] + assert_equal :baz, collector.responses[2][2].call + end + end + end +end diff --git a/actionpack/test/abstract/translation_test.rb b/actionpack/test/abstract/translation_test.rb new file mode 100644 index 0000000000..7138044c03 --- /dev/null +++ b/actionpack/test/abstract/translation_test.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module AbstractController + module Testing + class TranslationController < AbstractController::Base + include AbstractController::Translation + end + + class TranslationControllerTest < ActiveSupport::TestCase + def setup + @controller = TranslationController.new + I18n.backend.store_translations(:en, + one: { + two: "bar", + }, + abstract_controller: { + testing: { + translation: { + index: { + foo: "bar", + }, + no_action: "no_action_tr", + }, + }, + }) + end + + def test_action_controller_base_responds_to_translate + assert_respond_to @controller, :translate + end + + def test_action_controller_base_responds_to_t + assert_respond_to @controller, :t + end + + def test_action_controller_base_responds_to_localize + assert_respond_to @controller, :localize + end + + def test_action_controller_base_responds_to_l + assert_respond_to @controller, :l + end + + def test_lazy_lookup + @controller.stub :action_name, :index do + assert_equal "bar", @controller.t(".foo") + end + end + + def test_lazy_lookup_with_symbol + @controller.stub :action_name, :index do + assert_equal "bar", @controller.t(:'.foo') + end + end + + def test_lazy_lookup_fallback + @controller.stub :action_name, :index do + assert_equal "no_action_tr", @controller.t(:'.no_action') + end + end + + def test_default_translation + @controller.stub :action_name, :index do + assert_equal "bar", @controller.t("one.two") + assert_equal "baz", @controller.t(".twoz", default: ["baz", :twoz]) + end + end + + def test_localize + time, expected = Time.gm(2000), "Sat, 01 Jan 2000 00:00:00 +0000" + I18n.stub :localize, expected do + assert_equal expected, @controller.l(time) + end + end + end + end +end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb new file mode 100644 index 0000000000..f23151e518 --- /dev/null +++ b/actionpack/test/abstract_unit.rb @@ -0,0 +1,384 @@ +# frozen_string_literal: true + +$:.unshift File.expand_path("lib", __dir__) +$:.unshift File.expand_path("fixtures/helpers", __dir__) +$:.unshift File.expand_path("fixtures/alternate_helpers", __dir__) + +require "active_support/core_ext/kernel/reporting" + +# These are the normal settings that will be set up by Railties +# TODO: Have these tests support other combinations of these values +silence_warnings do + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 +end + +if ENV["TRAVIS"] + PROCESS_COUNT = 0 +else + PROCESS_COUNT = (ENV["N"] || 4).to_i +end + +require "active_support/testing/autorun" +require "abstract_controller" +require "abstract_controller/railties/routes_helpers" +require "action_controller" +require "action_view" +require "action_view/testing/resolvers" +require "action_dispatch" +require "active_support/dependencies" +require "active_model" + +require "pp" # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late + +module Rails + class << self + def env + @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test") + end + + def root; end + end +end + +ActiveSupport::Dependencies.hook! + +Thread.abort_on_exception = true + +# Show backtraces for deprecated behavior for quicker cleanup. +ActiveSupport::Deprecation.debug = true + +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + +FIXTURE_LOAD_PATH = File.join(__dir__, "fixtures") + +SharedTestRoutes = ActionDispatch::Routing::RouteSet.new + +SharedTestRoutes.draw do + ActiveSupport::Deprecation.silence do + get ":controller(/:action)" + end +end + +module ActionDispatch + module SharedRoutes + def before_setup + @routes = SharedTestRoutes + super + end + end +end + +module ActiveSupport + class TestCase + if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0 + parallelize(workers: PROCESS_COUNT) + end + end +end + +class RoutedRackApp + attr_reader :routes + + def initialize(routes, &blk) + @routes = routes + @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes) + end + + def call(env) + @stack.call(env) + end +end + +class ActionDispatch::IntegrationTest < ActiveSupport::TestCase + def self.build_app(routes = nil) + RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| + middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") + middleware.use ActionDispatch::DebugExceptions + middleware.use ActionDispatch::Callbacks + middleware.use ActionDispatch::Cookies + middleware.use ActionDispatch::Flash + middleware.use Rack::MethodOverride + middleware.use Rack::Head + yield(middleware) if block_given? + end + end + + self.app = build_app + + app.routes.draw do + ActiveSupport::Deprecation.silence do + get ":controller(/:action)" + end + end + + class DeadEndRoutes < ActionDispatch::Routing::RouteSet + # Stub Rails dispatcher so it does not get controller references and + # simply return the controller#action as Rack::Body. + class NullController < ::ActionController::Metal + def self.dispatch(action, req, res) + [200, { "Content-Type" => "text/html" }, ["#{req.params[:controller]}##{action}"]] + end + end + + class NullControllerRequest < ActionDispatch::Request + def controller_class + NullController + end + end + + def make_request(env) + NullControllerRequest.new env + end + end + + def self.stub_controllers(config = ActionDispatch::Routing::RouteSet::DEFAULT_CONFIG) + yield DeadEndRoutes.new(config) + end + + def with_routing(&block) + temporary_routes = ActionDispatch::Routing::RouteSet.new + old_app, self.class.app = self.class.app, self.class.build_app(temporary_routes) + old_routes = SharedTestRoutes + silence_warnings { Object.const_set(:SharedTestRoutes, temporary_routes) } + + yield temporary_routes + ensure + self.class.app = old_app + remove! + silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) } + end + + def with_autoload_path(path) + path = File.join(__dir__, "fixtures", path) + if ActiveSupport::Dependencies.autoload_paths.include?(path) + yield + else + begin + ActiveSupport::Dependencies.autoload_paths << path + yield + ensure + ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path } + ActiveSupport::Dependencies.clear + end + end + end +end + +# Temporary base class +class Rack::TestCase < ActionDispatch::IntegrationTest + def self.testing(klass = nil) + if klass + @testing = "/#{klass.name.underscore}".sub(/_controller$/, "") + else + @testing + end + end + + def get(thing, *args) + if thing.is_a?(Symbol) + super("#{self.class.testing}/#{thing}", *args) + else + super + end + end + + def assert_body(body) + assert_equal body, Array(response.body).join + end + + def assert_status(code) + assert_equal code, response.status + end + + def assert_response(body, status = 200, headers = {}) + assert_body body + assert_status status + headers.each do |header, value| + assert_header header, value + end + end + + def assert_content_type(type) + assert_equal type, response.headers["Content-Type"] + end + + def assert_header(name, value) + assert_equal value, response.headers[name] + end +end + +module ActionController + class API + extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes) + end + + class Base + # This stub emulates the Railtie including the URL helpers from a Rails application + extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes) + include SharedTestRoutes.mounted_helpers + + self.view_paths = FIXTURE_LOAD_PATH + + def self.test_routes(&block) + routes = ActionDispatch::Routing::RouteSet.new + routes.draw(&block) + include routes.url_helpers + routes + end + end + + class TestCase + include ActionDispatch::TestProcess + include ActionDispatch::SharedRoutes + end +end + +class ::ApplicationController < ActionController::Base +end + +module ActionDispatch + class DebugExceptions + private + remove_method :stderr_logger + # Silence logger + def stderr_logger + nil + end + end +end + +module ActionDispatch + module RoutingVerbs + def send_request(uri_or_host, method, path) + host = uri_or_host.host unless path + path ||= uri_or_host.path + + params = { "PATH_INFO" => path, + "REQUEST_METHOD" => method, + "HTTP_HOST" => host } + + routes.call(params) + end + + def request_path_params(path, options = {}) + method = options[:method] || "GET" + resp = send_request URI("http://localhost" + path), method.to_s.upcase, nil + status = resp.first + if status == 404 + raise ActionController::RoutingError, "No route matches #{path.inspect}" + end + controller.request.path_parameters + end + + def get(uri_or_host, path = nil) + send_request(uri_or_host, "GET", path)[2].join + end + + def post(uri_or_host, path = nil) + send_request(uri_or_host, "POST", path)[2].join + end + + def put(uri_or_host, path = nil) + send_request(uri_or_host, "PUT", path)[2].join + end + + def delete(uri_or_host, path = nil) + send_request(uri_or_host, "DELETE", path)[2].join + end + + def patch(uri_or_host, path = nil) + send_request(uri_or_host, "PATCH", path)[2].join + end + end +end + +module RoutingTestHelpers + def url_for(set, options) + route_name = options.delete :use_route + set.url_for options.merge(only_path: true), route_name + end + + def make_set(strict = true) + tc = self + TestSet.new ->(c) { tc.controller = c }, strict + end + + class TestSet < ActionDispatch::Routing::RouteSet + class Request < DelegateClass(ActionDispatch::Request) + def initialize(target, helpers, block, strict) + super(target) + @helpers = helpers + @block = block + @strict = strict + end + + def controller_class + helpers = @helpers + block = @block + Class.new(@strict ? super : ActionController::Base) { + include helpers + define_method(:process) { |name| block.call(self) } + def to_a; [200, {}, []]; end + } + end + end + + attr_reader :strict + + def initialize(block, strict = false) + @block = block + @strict = strict + super() + end + + private + + def make_request(env) + Request.new super, url_helpers, @block, strict + end + end +end + +class ResourcesController < ActionController::Base + def index() head :ok end + alias_method :show, :index +end + +class CommentsController < ResourcesController; end +class AccountsController < ResourcesController; end +class ImagesController < ResourcesController; end + +require "active_support/testing/method_call_assertions" + +class ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions + + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end +end + +class DrivenByRackTest < ActionDispatch::SystemTestCase + driven_by :rack_test +end + +class DrivenBySeleniumWithChrome < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome +end + +class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_chrome +end + +class DrivenBySeleniumWithHeadlessFirefox < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_firefox +end diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb new file mode 100644 index 0000000000..261579dce5 --- /dev/null +++ b/actionpack/test/assertions/response_assertions_test.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_dispatch/testing/assertions/response" + +module ActionDispatch + module Assertions + class ResponseAssertionsTest < ActiveSupport::TestCase + include ResponseAssertions + + FakeResponse = Struct.new(:response_code, :location, :body) do + def initialize(*) + super + self.location ||= "http://test.example.com/posts" + self.body ||= "" + end + + [:successful, :not_found, :redirection, :server_error].each do |sym| + define_method("#{sym}?") do + sym == response_code + end + end + end + + def setup + @controller = nil + @request = nil + end + + def test_assert_response_predicate_methods + [:success, :missing, :redirect, :error].each do |sym| + @response = FakeResponse.new RESPONSE_PREDICATES[sym].to_s.sub(/\?/, "").to_sym + assert_response sym + + assert_raises(Minitest::Assertion) { + assert_response :unauthorized + } + end + end + + def test_assert_response_integer + @response = FakeResponse.new 400 + assert_response 400 + + assert_raises(Minitest::Assertion) { + assert_response :unauthorized + } + + assert_raises(Minitest::Assertion) { + assert_response 500 + } + end + + def test_assert_response_sym_status + @response = FakeResponse.new 401 + assert_response :unauthorized + + assert_raises(Minitest::Assertion) { + assert_response :ok + } + + assert_raises(Minitest::Assertion) { + assert_response :success + } + end + + def test_assert_response_sym_typo + @response = FakeResponse.new 200 + + assert_raises(ArgumentError) { + assert_response :succezz + } + end + + def test_error_message_shows_404_when_404_asserted_for_success + @response = ActionDispatch::Response.new + @response.status = 404 + + error = assert_raises(Minitest::Assertion) { assert_response :success } + expected = "Expected response to be a <2XX: success>,"\ + " but was a <404: Not Found>" + assert_match expected, error.message + end + + def test_error_message_shows_404_when_asserted_for_200 + @response = ActionDispatch::Response.new + @response.status = 404 + + error = assert_raises(Minitest::Assertion) { assert_response 200 } + expected = "Expected response to be a <200: OK>,"\ + " but was a <404: Not Found>" + assert_match expected, error.message + end + + def test_error_message_shows_302_redirect_when_302_asserted_for_success + @response = ActionDispatch::Response.new + @response.status = 302 + @response.location = "http://test.host/posts/redirect/1" + + error = assert_raises(Minitest::Assertion) { assert_response :success } + expected = "Expected response to be a <2XX: success>,"\ + " but was a <302: Found>" \ + " redirect to <http://test.host/posts/redirect/1>" + assert_match expected, error.message + end + + def test_error_message_shows_302_redirect_when_302_asserted_for_301 + @response = ActionDispatch::Response.new + @response.status = 302 + @response.location = "http://test.host/posts/redirect/2" + + error = assert_raises(Minitest::Assertion) { assert_response 301 } + expected = "Expected response to be a <301: Moved Permanently>,"\ + " but was a <302: Found>" \ + " redirect to <http://test.host/posts/redirect/2>" + assert_match expected, error.message + end + + def test_error_message_shows_short_response_body + @response = ActionDispatch::Response.new + @response.status = 400 + @response.body = "not too long" + error = assert_raises(Minitest::Assertion) { assert_response 200 } + expected = "Expected response to be a <200: OK>,"\ + " but was a <400: Bad Request>" \ + "\nResponse body: not too long" + assert_match expected, error.message + end + + def test_error_message_does_not_show_long_response_body + @response = ActionDispatch::Response.new + @response.status = 400 + @response.body = "not too long" * 50 + error = assert_raises(Minitest::Assertion) { assert_response 200 } + expected = "Expected response to be a <200: OK>,"\ + " but was a <400: Bad Request>" + assert_match expected, error.message + end + end + end +end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb new file mode 100644 index 0000000000..ecb8c37e6b --- /dev/null +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -0,0 +1,491 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_controllers" + +class ActionPackAssertionsController < ActionController::Base + def nothing() head :ok end + + def hello_xml_world() render template: "test/hello_xml_world"; end + + def hello_xml_world_pdf + self.content_type = "application/pdf" + render template: "test/hello_xml_world" + end + + def hello_xml_world_pdf_header + response.headers["Content-Type"] = "application/pdf; charset=utf-8" + render template: "test/hello_xml_world" + end + + def redirect_internal() redirect_to "/nothing"; end + + def redirect_to_action() redirect_to action: "flash_me", id: 1, params: { "panda" => "fun" }; end + + def redirect_to_controller() redirect_to controller: "elsewhere", action: "flash_me"; end + + def redirect_to_controller_with_symbol() redirect_to controller: :elsewhere, action: :flash_me; end + + def redirect_to_path() redirect_to "/some/path" end + + def redirect_invalid_external_route() redirect_to "ht_tp://www.rubyonrails.org" end + + def redirect_to_named_route() redirect_to route_one_url end + + def redirect_external() redirect_to "http://www.rubyonrails.org"; end + + def redirect_external_protocol_relative() redirect_to "//www.rubyonrails.org"; end + + def response404() head "404 AWOL" end + + def response500() head "500 Sorry" end + + def response599() head "599 Whoah!" end + + def flash_me + flash["hello"] = "my name is inigo montoya..." + render plain: "Inconceivable!" + end + + def flash_me_naked + flash.clear + render plain: "wow!" + end + + def assign_this + @howdy = "ho" + render inline: "Mr. Henke" + end + + def render_based_on_parameters + render plain: "Mr. #{params[:name]}" + end + + def render_url + render html: "<div>#{url_for(action: 'flash_me', only_path: true)}</div>" + end + + def render_text_with_custom_content_type + render body: "Hello!", content_type: Mime[:rss] + end + + def session_stuffing + session["xmas"] = "turkey" + render plain: "ho ho ho" + end + + def raise_exception_on_get + raise "get" if request.get? + render plain: "request method: #{request.env['REQUEST_METHOD']}" + end + + def raise_exception_on_post + raise "post" if request.post? + render plain: "request method: #{request.env['REQUEST_METHOD']}" + end + + def render_file_absolute_path + render file: File.expand_path("../../README.rdoc", __dir__) + end + + def render_file_relative_path + render file: "README.rdoc" + end +end + +# Used to test that assert_response includes the exception message +# in the failure message when an action raises and assert_response +# is expecting something other than an error. +class AssertResponseWithUnexpectedErrorController < ActionController::Base + def index + raise "FAIL" + end + + def show + render plain: "Boom", status: 500 + end +end + +module Admin + class InnerModuleController < ActionController::Base + def index + head :ok + end + + def redirect_to_index + redirect_to admin_inner_module_path + end + + def redirect_to_absolute_controller + redirect_to controller: "/content" + end + + def redirect_to_fellow_controller + redirect_to controller: "user" + end + + def redirect_to_top_level_named_route + redirect_to top_level_url(id: "foo") + end + end +end + +class ApiOnlyController < ActionController::API + def nothing + head :ok + end + + def redirect_to_new_route + redirect_to new_route_url + end +end + +class ActionPackAssertionsControllerTest < ActionController::TestCase + def test_render_file_absolute_path + get :render_file_absolute_path + assert_match(/\A= Action Pack/, @response.body) + end + + def test_render_file_relative_path + get :render_file_relative_path + assert_match(/\A= Action Pack/, @response.body) + end + + def test_get_request + assert_raise(RuntimeError) { get :raise_exception_on_get } + get :raise_exception_on_post + assert_equal "request method: GET", @response.body + end + + def test_post_request + assert_raise(RuntimeError) { post :raise_exception_on_post } + post :raise_exception_on_get + assert_equal "request method: POST", @response.body + end + + def test_get_post_request_switch + post :raise_exception_on_get + assert_equal "request method: POST", @response.body + get :raise_exception_on_post + assert_equal "request method: GET", @response.body + post :raise_exception_on_get + assert_equal "request method: POST", @response.body + get :raise_exception_on_post + assert_equal "request method: GET", @response.body + end + + def test_string_constraint + with_routing do |set| + set.draw do + get "photos", to: "action_pack_assertions#nothing", constraints: { subdomain: "admin" } + end + end + end + + def test_with_routing_works_with_api_only_controllers + @controller = ApiOnlyController.new + + with_routing do |set| + set.draw do + get "new_route", to: "api_only#nothing" + get "redirect_to_new_route", to: "api_only#redirect_to_new_route" + end + + process :redirect_to_new_route + assert_redirected_to "http://test.host/new_route" + end + end + + def test_assert_redirect_to_named_route_failure + with_routing do |set| + set.draw do + get "route_one", to: "action_pack_assertions#nothing", as: :route_one + get "route_two", to: "action_pack_assertions#nothing", id: "two", as: :route_two + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + process :redirect_to_named_route + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_redirected_to "http://test.host/route_two" + end + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_redirected_to %r(^http://test.host/route_two) + end + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_redirected_to controller: "action_pack_assertions", action: "nothing", id: "two" + end + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_redirected_to route_two_url + end + end + end + + def test_assert_redirect_to_nested_named_route + @controller = Admin::InnerModuleController.new + + with_routing do |set| + set.draw do + get "admin/inner_module", to: "admin/inner_module#index", as: :admin_inner_module + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + process :redirect_to_index + # redirection is <{"action"=>"index", "controller"=>"admin/admin/inner_module"}> + assert_redirected_to admin_inner_module_path + end + end + + def test_assert_redirected_to_top_level_named_route_from_nested_controller + @controller = Admin::InnerModuleController.new + + with_routing do |set| + set.draw do + get "/action_pack_assertions/:id", to: "action_pack_assertions#index", as: :top_level + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + process :redirect_to_top_level_named_route + # assert_redirected_to "http://test.host/action_pack_assertions/foo" would pass because of exact match early return + assert_redirected_to "/action_pack_assertions/foo" + assert_redirected_to %r(/action_pack_assertions/foo) + end + end + + def test_assert_redirected_to_top_level_named_route_with_same_controller_name_in_both_namespaces + @controller = Admin::InnerModuleController.new + + with_routing do |set| + set.draw do + # this controller exists in the admin namespace as well which is the only difference from previous test + get "/user/:id", to: "user#index", as: :top_level + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + process :redirect_to_top_level_named_route + # assert_redirected_to top_level_url('foo') would pass because of exact match early return + assert_redirected_to top_level_path("foo") + end + end + + def test_assert_redirect_failure_message_with_protocol_relative_url + process :redirect_external_protocol_relative + assert_redirected_to "/foo" + rescue ActiveSupport::TestCase::Assertion => ex + assert_no_match( + /#{request.protocol}#{request.host}\/\/www.rubyonrails.org/, + ex.message, + "protocol relative url was incorrectly normalized" + ) + end + + def test_template_objects_exist + process :assign_this + assert_not @controller.instance_variable_defined?(:"@hi") + assert @controller.instance_variable_get(:"@howdy") + end + + def test_template_objects_missing + process :nothing + assert_not @controller.instance_variable_defined?(:@howdy) + end + + def test_empty_flash + process :flash_me_naked + assert_empty flash + end + + def test_flash_exist + process :flash_me + assert_predicate flash, :any? + assert_predicate flash["hello"], :present? + end + + def test_flash_does_not_exist + process :nothing + assert_empty flash + end + + def test_session_exist + process :session_stuffing + assert_equal "turkey", session["xmas"] + end + + def session_does_not_exist + process :nothing + assert_empty session + end + + def test_redirection_location + process :redirect_internal + assert_equal "http://test.host/nothing", @response.redirect_url + + process :redirect_external + assert_equal "http://www.rubyonrails.org", @response.redirect_url + + process :redirect_external_protocol_relative + assert_equal "//www.rubyonrails.org", @response.redirect_url + end + + def test_no_redirect_url + process :nothing + assert_nil @response.redirect_url + end + + def test_server_error_response_code + process :response500 + assert_predicate @response, :server_error? + + process :response599 + assert_predicate @response, :server_error? + + process :response404 + assert_not_predicate @response, :server_error? + end + + def test_missing_response_code + process :response404 + assert_predicate @response, :not_found? + end + + def test_client_error_response_code + process :response404 + assert_predicate @response, :client_error? + end + + def test_redirect_url_match + process :redirect_external + assert_predicate @response, :redirect? + assert_match(/rubyonrails/, @response.redirect_url) + assert_no_match(/perloffrails/, @response.redirect_url) + end + + def test_redirection + process :redirect_internal + assert_predicate @response, :redirect? + + process :redirect_external + assert_predicate @response, :redirect? + + process :nothing + assert_not_predicate @response, :redirect? + end + + def test_successful_response_code + process :nothing + assert_predicate @response, :successful? + end + + def test_response_object + process :nothing + assert_kind_of ActionDispatch::TestResponse, @response + end + + def test_render_based_on_parameters + process :render_based_on_parameters, + method: "GET", + params: { name: "David" } + assert_equal "Mr. David", @response.body + end + + def test_assert_redirection_fails_with_incorrect_controller + process :redirect_to_controller + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_redirected_to controller: "action_pack_assertions", action: "flash_me" + end + end + + def test_assert_redirection_with_extra_controller_option + get :redirect_to_action + assert_redirected_to controller: "action_pack_assertions", action: "flash_me", id: 1, params: { panda: "fun" } + end + + def test_redirected_to_url_leading_slash + process :redirect_to_path + assert_redirected_to "/some/path" + end + + def test_redirected_to_url_no_leading_slash_fails + process :redirect_to_path + assert_raise ActiveSupport::TestCase::Assertion do + assert_redirected_to "some/path" + end + end + + def test_redirect_invalid_external_route + process :redirect_invalid_external_route + assert_redirected_to "http://test.hostht_tp://www.rubyonrails.org" + end + + def test_redirected_to_url_full_url + process :redirect_to_path + assert_redirected_to "http://test.host/some/path" + end + + def test_assert_redirection_with_symbol + process :redirect_to_controller_with_symbol + assert_nothing_raised { + assert_redirected_to controller: "elsewhere", action: "flash_me" + } + process :redirect_to_controller_with_symbol + assert_nothing_raised { + assert_redirected_to controller: :elsewhere, action: :flash_me + } + end + + def test_redirected_to_with_nested_controller + @controller = Admin::InnerModuleController.new + get :redirect_to_absolute_controller + assert_redirected_to controller: "/content" + + get :redirect_to_fellow_controller + assert_redirected_to controller: "admin/user" + end + + def test_assert_response_uses_exception_message + @controller = AssertResponseWithUnexpectedErrorController.new + e = assert_raise RuntimeError, "Expected non-success response" do + get :index + end + assert_response :success + assert_includes "FAIL", e.message + end + + def test_assert_response_failure_response_with_no_exception + @controller = AssertResponseWithUnexpectedErrorController.new + get :show + assert_response 500 + assert_equal "Boom", response.body + end +end + +class ActionPackHeaderTest < ActionController::TestCase + tests ActionPackAssertionsController + + def test_rendering_xml_sets_content_type + process :hello_xml_world + assert_equal("application/xml; charset=utf-8", @response.headers["Content-Type"]) + end + + def test_rendering_xml_respects_content_type + process :hello_xml_world_pdf + assert_equal("application/pdf; charset=utf-8", @response.headers["Content-Type"]) + end + + def test_rendering_xml_respects_content_type_when_set_in_the_header + process :hello_xml_world_pdf_header + assert_equal("application/pdf; charset=utf-8", @response.headers["Content-Type"]) + end + + def test_render_text_with_custom_content_type + get :render_text_with_custom_content_type + assert_equal "application/rss+xml; charset=utf-8", @response.headers["Content-Type"] + end +end diff --git a/actionpack/test/controller/api/conditional_get_test.rb b/actionpack/test/controller/api/conditional_get_test.rb new file mode 100644 index 0000000000..e366ce9532 --- /dev/null +++ b/actionpack/test/controller/api/conditional_get_test.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/integer/time" +require "active_support/core_ext/numeric/time" + +class ConditionalGetApiController < ActionController::API + before_action :handle_last_modified_and_etags, only: :two + + def one + if stale?(last_modified: Time.now.utc.beginning_of_day, etag: [:foo, 123]) + render plain: "Hi!" + end + end + + def two + render plain: "Hi!" + end + + private + + def handle_last_modified_and_etags + fresh_when(last_modified: Time.now.utc.beginning_of_day, etag: [ :foo, 123 ]) + end +end + +class ConditionalGetApiTest < ActionController::TestCase + tests ConditionalGetApiController + + def setup + @last_modified = Time.now.utc.beginning_of_day.httpdate + end + + def test_request_gets_last_modified + get :two + assert_equal @last_modified, @response.headers["Last-Modified"] + assert_response :success + end + + def test_request_obeys_last_modified + @request.if_modified_since = @last_modified + get :two + assert_response :not_modified + end + + def test_last_modified_works_with_less_than_too + @request.if_modified_since = 5.years.ago.httpdate + get :two + assert_response :success + end + + def test_request_not_modified + @request.if_modified_since = @last_modified + get :one + assert_equal 304, @response.status.to_i + assert_predicate @response.body, :blank? + assert_equal @last_modified, @response.headers["Last-Modified"] + end +end diff --git a/actionpack/test/controller/api/data_streaming_test.rb b/actionpack/test/controller/api/data_streaming_test.rb new file mode 100644 index 0000000000..6446ff9e40 --- /dev/null +++ b/actionpack/test/controller/api/data_streaming_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module TestApiFileUtils + def file_path() __FILE__ end + def file_data() @data ||= File.open(file_path, "rb") { |f| f.read } end +end + +class DataStreamingApiController < ActionController::API + include TestApiFileUtils + + def one; end + def two + send_data(file_data, {}) + end +end + +class DataStreamingApiTest < ActionController::TestCase + include TestApiFileUtils + tests DataStreamingApiController + + def test_data + response = process("two") + assert_kind_of String, response.body + assert_equal file_data, response.body + end +end diff --git a/actionpack/test/controller/api/force_ssl_test.rb b/actionpack/test/controller/api/force_ssl_test.rb new file mode 100644 index 0000000000..8191578eb0 --- /dev/null +++ b/actionpack/test/controller/api/force_ssl_test.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ForceSSLApiController < ActionController::API + ActiveSupport::Deprecation.silence do + force_ssl + end + + def one; end + def two + head :ok + end +end + +class ForceSSLApiTest < ActionController::TestCase + tests ForceSSLApiController + + def test_redirects_to_https + get :two + assert_response 301 + assert_equal "https://test.host/force_ssl_api/two", redirect_to_url + end +end diff --git a/actionpack/test/controller/api/implicit_render_test.rb b/actionpack/test/controller/api/implicit_render_test.rb new file mode 100644 index 0000000000..288fb333b0 --- /dev/null +++ b/actionpack/test/controller/api/implicit_render_test.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ImplicitRenderAPITestController < ActionController::API + def empty_action + end +end + +class ImplicitRenderAPITest < ActionController::TestCase + tests ImplicitRenderAPITestController + + def test_implicit_no_content_response + get :empty_action + assert_response :no_content + end +end diff --git a/actionpack/test/controller/api/params_wrapper_test.rb b/actionpack/test/controller/api/params_wrapper_test.rb new file mode 100644 index 0000000000..814c24bfd8 --- /dev/null +++ b/actionpack/test/controller/api/params_wrapper_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ParamsWrapperForApiTest < ActionController::TestCase + class UsersController < ActionController::API + attr_accessor :last_parameters + + wrap_parameters :person, format: [:json] + + def test + self.last_parameters = params.except(:controller, :action).to_unsafe_h + head :ok + end + end + + class Person; end + + tests UsersController + + def test_specify_wrapper_name + @request.env["CONTENT_TYPE"] = "application/json" + post :test, params: { "username" => "sikachu" } + + expected = { "username" => "sikachu", "person" => { "username" => "sikachu" } } + assert_equal expected, @controller.last_parameters + end +end diff --git a/actionpack/test/controller/api/redirect_to_test.rb b/actionpack/test/controller/api/redirect_to_test.rb new file mode 100644 index 0000000000..f8230dd6a9 --- /dev/null +++ b/actionpack/test/controller/api/redirect_to_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class RedirectToApiController < ActionController::API + def one + redirect_to action: "two" + end + + def two; end +end + +class RedirectToApiTest < ActionController::TestCase + tests RedirectToApiController + + def test_redirect_to + get :one + assert_response :redirect + assert_equal "http://test.host/redirect_to_api/two", redirect_to_url + end +end diff --git a/actionpack/test/controller/api/renderers_test.rb b/actionpack/test/controller/api/renderers_test.rb new file mode 100644 index 0000000000..e7a9a4b2da --- /dev/null +++ b/actionpack/test/controller/api/renderers_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/hash/conversions" + +class RenderersApiController < ActionController::API + class Model + def to_json(options = {}) + { a: "b" }.to_json(options) + end + + def to_xml(options = {}) + { a: "b" }.to_xml(options) + end + end + + def one + render json: Model.new + end + + def two + render xml: Model.new + end + + def plain + render plain: "Hi from plain", status: 500 + end +end + +class RenderersApiTest < ActionController::TestCase + tests RenderersApiController + + def test_render_json + get :one + assert_response :success + assert_equal({ a: "b" }.to_json, @response.body) + end + + def test_render_xml + get :two + assert_response :success + assert_equal({ a: "b" }.to_xml, @response.body) + end + + def test_render_plain + get :plain + assert_response :internal_server_error + assert_equal("Hi from plain", @response.body) + end +end diff --git a/actionpack/test/controller/api/url_for_test.rb b/actionpack/test/controller/api/url_for_test.rb new file mode 100644 index 0000000000..aa3428bc85 --- /dev/null +++ b/actionpack/test/controller/api/url_for_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class UrlForApiController < ActionController::API + def one; end + def two; end +end + +class UrlForApiTest < ActionController::TestCase + tests UrlForApiController + + def setup + super + @request.host = "www.example.com" + end + + def test_url_for + get :one + assert_equal "http://www.example.com/url_for_api/one", @controller.url_for + end +end diff --git a/actionpack/test/controller/api/with_cookies_test.rb b/actionpack/test/controller/api/with_cookies_test.rb new file mode 100644 index 0000000000..1a6e12a4f3 --- /dev/null +++ b/actionpack/test/controller/api/with_cookies_test.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class WithCookiesController < ActionController::API + include ActionController::Cookies + + def with_cookies + render plain: cookies[:foobar] + end +end + +class WithCookiesTest < ActionController::TestCase + tests WithCookiesController + + def test_with_cookies + request.cookies[:foobar] = "bazbang" + + get :with_cookies + + assert_equal "bazbang", response.body + end +end diff --git a/actionpack/test/controller/api/with_helpers_test.rb b/actionpack/test/controller/api/with_helpers_test.rb new file mode 100644 index 0000000000..00179d3505 --- /dev/null +++ b/actionpack/test/controller/api/with_helpers_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ApiWithHelper + def my_helper + "helper" + end +end + +class WithHelpersController < ActionController::API + include ActionController::Helpers + helper ApiWithHelper + + def with_helpers + render plain: self.class.helpers.my_helper + end +end + +class SubclassWithHelpersController < WithHelpersController + def with_helpers + render plain: self.class.helpers.my_helper + end +end + +class WithHelpersTest < ActionController::TestCase + tests WithHelpersController + + def test_with_helpers + get :with_helpers + + assert_equal "helper", response.body + end +end + +class SubclassWithHelpersTest < ActionController::TestCase + tests WithHelpersController + + def test_with_helpers + get :with_helpers + + assert_equal "helper", response.body + end +end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb new file mode 100644 index 0000000000..558e710df9 --- /dev/null +++ b/actionpack/test/controller/base_test.rb @@ -0,0 +1,323 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/logger" +require "controller/fake_models" + +# Provide some controller to run the tests on. +module Submodule + class ContainedEmptyController < ActionController::Base + end +end + +class EmptyController < ActionController::Base +end + +class SimpleController < ActionController::Base + def hello + self.response_body = "hello" + end +end + +class NonEmptyController < ActionController::Base + def public_action + head :ok + end +end + +class DefaultUrlOptionsController < ActionController::Base + def from_view + render inline: "<%= #{params[:route]} %>" + end + + def default_url_options + { host: "www.override.com", action: "new", locale: "en" } + end +end + +class OptionalDefaultUrlOptionsController < ActionController::Base + def show + head :ok + end + + def default_url_options + { format: "atom", id: "default-id" } + end +end + +class UrlOptionsController < ActionController::Base + def from_view + render inline: "<%= #{params[:route]} %>" + end + + def url_options + super.merge(host: "www.override.com") + end +end + +class RecordIdentifierIncludedController < ActionController::Base + include ActionView::RecordIdentifier +end + +class ActionMissingController < ActionController::Base + def action_missing(action) + render plain: "Response for #{action}" + end +end + +class ControllerClassTests < ActiveSupport::TestCase + def test_controller_path + assert_equal "empty", EmptyController.controller_path + assert_equal EmptyController.controller_path, EmptyController.new.controller_path + assert_equal "submodule/contained_empty", Submodule::ContainedEmptyController.controller_path + assert_equal Submodule::ContainedEmptyController.controller_path, Submodule::ContainedEmptyController.new.controller_path + end + + def test_controller_name + assert_equal "empty", EmptyController.controller_name + assert_equal "contained_empty", Submodule::ContainedEmptyController.controller_name + end + + def test_no_deprecation_when_action_view_record_identifier_is_included + record = Comment.new + record.save + + dom_id = nil + assert_not_deprecated do + dom_id = RecordIdentifierIncludedController.new.dom_id(record) + end + + assert_equal "comment_1", dom_id + + dom_class = nil + assert_not_deprecated do + dom_class = RecordIdentifierIncludedController.new.dom_class(record) + end + assert_equal "comment", dom_class + end +end + +class ControllerInstanceTests < ActiveSupport::TestCase + def setup + @empty = EmptyController.new + @empty.set_request!(ActionDispatch::Request.empty) + @empty.set_response!(EmptyController.make_response!(@empty.request)) + @contained = Submodule::ContainedEmptyController.new + @empty_controllers = [@empty, @contained] + end + + def test_performed? + assert_not_predicate @empty, :performed? + @empty.response_body = ["sweet"] + assert_predicate @empty, :performed? + end + + def test_action_methods + @empty_controllers.each do |c| + assert_equal Set.new, c.class.action_methods, "#{c.controller_path} should be empty!" + end + end + + def test_temporary_anonymous_controllers + name = "ExamplesController" + klass = Class.new(ActionController::Base) + Object.const_set(name, klass) + + controller = klass.new + assert_equal "examples", controller.controller_path + end + + def test_response_has_default_headers + original_default_headers = ActionDispatch::Response.default_headers + + ActionDispatch::Response.default_headers = { + "X-Frame-Options" => "DENY", + "X-Content-Type-Options" => "nosniff", + "X-XSS-Protection" => "1;" + } + + response_headers = SimpleController.action("hello").call( + "REQUEST_METHOD" => "GET", + "rack.input" => -> { } + )[1] + + assert response_headers.key?("X-Frame-Options") + assert response_headers.key?("X-Content-Type-Options") + assert response_headers.key?("X-XSS-Protection") + ensure + ActionDispatch::Response.default_headers = original_default_headers + end +end + +class PerformActionTest < ActionController::TestCase + def use_controller(controller_class) + @controller = controller_class.new + + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + @controller.logger = ActiveSupport::Logger.new(nil) + + @request.host = "www.nextangle.com" + end + + def test_process_should_be_precise + use_controller EmptyController + exception = assert_raise AbstractController::ActionNotFound do + get :non_existent + end + assert_equal "The action 'non_existent' could not be found for EmptyController", exception.message + end + + def test_action_missing_should_work + use_controller ActionMissingController + get :arbitrary_action + assert_equal "Response for arbitrary_action", @response.body + end +end + +class UrlOptionsTest < ActionController::TestCase + tests UrlOptionsController + + def setup + super + @request.host = "www.example.com" + end + + def test_url_for_query_params_included + rs = ActionDispatch::Routing::RouteSet.new + rs.draw do + get "home" => "pages#home" + end + + options = { + action: "home", + controller: "pages", + only_path: true, + token: "secret" + } + + assert_equal "/home?token=secret", rs.url_for(options) + end + + def test_url_options_override + with_routing do |set| + set.draw do + get "from_view", to: "url_options#from_view", as: :from_view + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + + get :from_view, params: { route: "from_view_url" } + + assert_equal "http://www.override.com/from_view", @response.body + assert_equal "http://www.override.com/from_view", @controller.send(:from_view_url) + assert_equal "http://www.override.com/default_url_options/index", @controller.url_for(controller: "default_url_options") + end + end + + def test_url_helpers_does_not_become_actions + with_routing do |set| + set.draw do + get "account/overview" + end + + assert_not_includes @controller.class.action_methods, "account_overview_path" + end + end +end + +class DefaultUrlOptionsTest < ActionController::TestCase + tests DefaultUrlOptionsController + + def setup + super + @request.host = "www.example.com" + end + + def test_default_url_options_override + with_routing do |set| + set.draw do + get "from_view", to: "default_url_options#from_view", as: :from_view + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + + get :from_view, params: { route: "from_view_url" } + + assert_equal "http://www.override.com/from_view?locale=en", @response.body + assert_equal "http://www.override.com/from_view?locale=en", @controller.send(:from_view_url) + assert_equal "http://www.override.com/default_url_options/new?locale=en", @controller.url_for(controller: "default_url_options") + end + end + + def test_default_url_options_are_used_in_non_positional_parameters + with_routing do |set| + set.draw do + scope("/:locale") do + resources :descriptions + end + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + + get :from_view, params: { route: "description_path(1)" } + + assert_equal "/en/descriptions/1", @response.body + assert_equal "/en/descriptions", @controller.send(:descriptions_path) + assert_equal "/pl/descriptions", @controller.send(:descriptions_path, "pl") + assert_equal "/pl/descriptions", @controller.send(:descriptions_path, locale: "pl") + assert_equal "/pl/descriptions.xml", @controller.send(:descriptions_path, "pl", "xml") + assert_equal "/en/descriptions.xml", @controller.send(:descriptions_path, format: "xml") + assert_equal "/en/descriptions/1", @controller.send(:description_path, 1) + assert_equal "/pl/descriptions/1", @controller.send(:description_path, "pl", 1) + assert_equal "/pl/descriptions/1", @controller.send(:description_path, 1, locale: "pl") + assert_equal "/pl/descriptions/1.xml", @controller.send(:description_path, "pl", 1, "xml") + assert_equal "/en/descriptions/1.xml", @controller.send(:description_path, 1, format: "xml") + end + end +end + +class OptionalDefaultUrlOptionsControllerTest < ActionController::TestCase + def test_default_url_options_override_missing_positional_arguments + with_routing do |set| + set.draw do + get "/things/:id(.:format)" => "things#show", :as => :thing + end + assert_equal "/things/1.atom", thing_path("1") + assert_equal "/things/default-id.atom", thing_path + end + end +end + +class EmptyUrlOptionsTest < ActionController::TestCase + tests NonEmptyController + + def setup + super + @request.host = "www.example.com" + end + + def test_ensure_url_for_works_as_expected_when_called_with_no_options_if_default_url_options_is_not_set + get :public_action + assert_equal "http://www.example.com/non_empty/public_action", @controller.url_for + end + + def test_named_routes_with_path_without_doing_a_request_first + @controller = EmptyController.new + @controller.request = @request + + with_routing do |set| + set.draw do + resources :things + end + + assert_equal "/things", @controller.send(:things_path) + end + end +end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb new file mode 100644 index 0000000000..6fe036dd15 --- /dev/null +++ b/actionpack/test/controller/caching_test.rb @@ -0,0 +1,511 @@ +# frozen_string_literal: true + +require "fileutils" +require "abstract_unit" +require "lib/controller/fake_models" + +CACHE_DIR = "test_cache" +# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed +FILE_STORE_PATH = File.join(__dir__, "../temp/", CACHE_DIR) + +class FragmentCachingMetalTestController < ActionController::Metal + abstract! + + include ActionController::Caching + + def some_action; end +end + +class FragmentCachingMetalTest < ActionController::TestCase + def setup + super + @store = ActiveSupport::Cache::MemoryStore.new + @controller = FragmentCachingMetalTestController.new + @controller.perform_caching = true + @controller.cache_store = @store + @params = { controller: "posts", action: "index" } + @controller.params = @params + @controller.request = @request + @controller.response = @response + end +end + +class CachingController < ActionController::Base + abstract! + + self.cache_store = :file_store, FILE_STORE_PATH +end + +class FragmentCachingTestController < CachingController + def some_action; end +end + +class FragmentCachingTest < ActionController::TestCase + ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) + + def setup + super + @store = ActiveSupport::Cache::MemoryStore.new + @controller = FragmentCachingTestController.new + @controller.perform_caching = true + @controller.cache_store = @store + @params = { controller: "posts", action: "index" } + @controller.params = @params + @controller.request = @request + @controller.response = @response + + @m1v1 = ModelWithKeyAndVersion.new("model/1", "1") + @m1v2 = ModelWithKeyAndVersion.new("model/1", "2") + @m2v1 = ModelWithKeyAndVersion.new("model/2", "1") + @m2v2 = ModelWithKeyAndVersion.new("model/2", "2") + end + + def test_fragment_cache_key + assert_deprecated do + assert_equal "views/what a key", @controller.fragment_cache_key("what a key") + assert_equal "views/test.host/fragment_caching_test/some_action", + @controller.fragment_cache_key(controller: "fragment_caching_test", action: "some_action") + end + end + + def test_combined_fragment_cache_key + assert_equal [ :views, "what a key" ], @controller.combined_fragment_cache_key("what a key") + assert_equal [ :views, "test.host/fragment_caching_test/some_action" ], + @controller.combined_fragment_cache_key(controller: "fragment_caching_test", action: "some_action") + end + + def test_read_fragment_with_caching_enabled + @store.write("views/name", "value") + assert_equal "value", @controller.read_fragment("name") + end + + def test_read_fragment_with_caching_disabled + @controller.perform_caching = false + @store.write("views/name", "value") + assert_nil @controller.read_fragment("name") + end + + def test_read_fragment_with_versioned_model + @controller.write_fragment([ "stuff", @m1v1 ], "hello") + assert_equal "hello", @controller.read_fragment([ "stuff", @m1v1 ]) + assert_nil @controller.read_fragment([ "stuff", @m1v2 ]) + end + + def test_fragment_exist_with_caching_enabled + @store.write("views/name", "value") + assert @controller.fragment_exist?("name") + assert_not @controller.fragment_exist?("other_name") + end + + def test_fragment_exist_with_caching_disabled + @controller.perform_caching = false + @store.write("views/name", "value") + assert_not @controller.fragment_exist?("name") + assert_not @controller.fragment_exist?("other_name") + end + + def test_write_fragment_with_caching_enabled + assert_nil @store.read("views/name") + assert_equal "value", @controller.write_fragment("name", "value") + assert_equal "value", @store.read("views/name") + end + + def test_write_fragment_with_caching_disabled + assert_nil @store.read("views/name") + @controller.perform_caching = false + assert_equal "value", @controller.write_fragment("name", "value") + assert_nil @store.read("views/name") + end + + def test_expire_fragment_with_simple_key + @store.write("views/name", "value") + @controller.expire_fragment "name" + assert_nil @store.read("views/name") + end + + def test_expire_fragment_with_regexp + @store.write("views/name", "value") + @store.write("views/another_name", "another_value") + @store.write("views/primalgrasp", "will not expire ;-)") + + @controller.expire_fragment(/name/) + + assert_nil @store.read("views/name") + assert_nil @store.read("views/another_name") + assert_equal "will not expire ;-)", @store.read("views/primalgrasp") + end + + def test_fragment_for + @store.write("views/expensive", "fragment content") + fragment_computed = false + + view_context = @controller.view_context + + buffer = "generated till now -> ".html_safe + buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true } + + assert_not fragment_computed + assert_equal "generated till now -> fragment content", buffer + end + + def test_html_safety + assert_nil @store.read("views/name") + content = "value".html_safe + assert_equal content, @controller.write_fragment("name", content) + + cached = @store.read("views/name") + assert_equal content, cached + assert_equal String, cached.class + + html_safe = @controller.read_fragment("name") + assert_equal content, html_safe + assert_predicate html_safe, :html_safe? + end +end + +class FunctionalCachingController < CachingController + def fragment_cached + end + + def html_fragment_cached_with_partial + respond_to do |format| + format.html + end + end + + def xml_fragment_cached_with_html_partial + end + + def formatted_fragment_cached + respond_to do |format| + format.html + format.xml + end + end + + def formatted_fragment_cached_with_variant + request.variant = :phone if params[:v] == "phone" + + respond_to do |format| + format.html.phone + format.html + end + end + + def fragment_cached_without_digest + end + + def fragment_cached_with_options + end +end + +class FunctionalFragmentCachingTest < ActionController::TestCase + def setup + super + @store = ActiveSupport::Cache::MemoryStore.new + @controller = FunctionalCachingController.new + @controller.perform_caching = true + @controller.cache_store = @store + @controller.enable_fragment_cache_logging = true + end + + def test_fragment_caching + get :fragment_cached + assert_response :success + expected_body = <<-CACHED +Hello +This bit's fragment cached +Ciao +CACHED + assert_equal expected_body, @response.body + + assert_equal "This bit's fragment cached", + @store.read("views/functional_caching/fragment_cached:#{template_digest("functional_caching/fragment_cached")}/fragment") + end + + def test_fragment_caching_in_partials + get :html_fragment_cached_with_partial + assert_response :success + assert_match(/Old fragment caching in a partial/, @response.body) + + assert_match("Old fragment caching in a partial", + @store.read("views/functional_caching/_partial:#{template_digest("functional_caching/_partial")}/test.host/functional_caching/html_fragment_cached_with_partial")) + end + + def test_skipping_fragment_cache_digesting + get :fragment_cached_without_digest, format: "html" + assert_response :success + expected_body = "<body>\n<p>ERB</p>\n</body>\n" + + assert_equal expected_body, @response.body + assert_equal "<p>ERB</p>", @store.read("views/nodigest") + end + + def test_fragment_caching_with_options + time = Time.now + get :fragment_cached_with_options + assert_response :success + expected_body = "<body>\n<p>ERB</p>\n</body>\n" + + assert_equal expected_body, @response.body + Time.stub(:now, time + 11) do + assert_nil @store.read("views/with_options") + end + end + + def test_render_inline_before_fragment_caching + get :inline_fragment_cached + assert_response :success + assert_match(/Some inline content/, @response.body) + assert_match(/Some cached content/, @response.body) + assert_match("Some cached content", + @store.read("views/functional_caching/inline_fragment_cached:#{template_digest("functional_caching/inline_fragment_cached")}/test.host/functional_caching/inline_fragment_cached")) + end + + def test_fragment_cache_instrumentation + payload = nil + + subscriber = proc do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + payload = event.payload + end + + ActiveSupport::Notifications.subscribed(subscriber, "read_fragment.action_controller") do + get :inline_fragment_cached + end + + assert_equal "functional_caching", payload[:controller] + assert_equal "inline_fragment_cached", payload[:action] + end + + def test_html_formatted_fragment_caching + get :formatted_fragment_cached, format: "html" + assert_response :success + expected_body = "<body>\n<p>ERB</p>\n</body>\n" + + assert_equal expected_body, @response.body + + assert_equal "<p>ERB</p>", + @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment") + end + + def test_xml_formatted_fragment_caching + get :formatted_fragment_cached, format: "xml" + assert_response :success + expected_body = "<body>\n <p>Builder</p>\n</body>\n" + + assert_equal expected_body, @response.body + + assert_equal " <p>Builder</p>\n", + @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment") + end + + def test_fragment_caching_with_variant + get :formatted_fragment_cached_with_variant, format: "html", params: { v: :phone } + assert_response :success + expected_body = "<body>\n<p>PHONE</p>\n</body>\n" + + assert_equal expected_body, @response.body + + assert_equal "<p>PHONE</p>", + @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment") + end + + def test_fragment_caching_with_html_partials_in_xml + get :xml_fragment_cached_with_html_partial, format: "*/*" + assert_response :success + end + + private + def template_digest(name) + ActionView::Digestor.digest(name: name, finder: @controller.lookup_context) + end +end + +class CacheHelperOutputBufferTest < ActionController::TestCase + class MockController + def read_fragment(name, options) + false + end + + def write_fragment(name, fragment, options) + fragment + end + end + + def setup + super + end + + def test_output_buffer + output_buffer = ActionView::OutputBuffer.new + controller = MockController.new + cache_helper = Class.new do + def self.controller; end + def self.output_buffer; end + def self.output_buffer=; end + end + cache_helper.extend(ActionView::Helpers::CacheHelper) + + cache_helper.stub :controller, controller do + cache_helper.stub :output_buffer, output_buffer do + assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do + assert_nothing_raised do + cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil } + end + end + end + end + end + + def test_safe_buffer + output_buffer = ActiveSupport::SafeBuffer.new + controller = MockController.new + cache_helper = Class.new do + def self.controller; end + def self.output_buffer; end + def self.output_buffer=; end + end + cache_helper.extend(ActionView::Helpers::CacheHelper) + + cache_helper.stub :controller, controller do + cache_helper.stub :output_buffer, output_buffer do + assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do + assert_nothing_raised do + cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil } + end + end + end + end + end +end + +class ViewCacheDependencyTest < ActionController::TestCase + class NoDependenciesController < ActionController::Base + end + + class HasDependenciesController < ActionController::Base + view_cache_dependency { "trombone" } + view_cache_dependency { "flute" } + end + + def test_view_cache_dependencies_are_empty_by_default + assert_empty NoDependenciesController.new.view_cache_dependencies + end + + def test_view_cache_dependencies_are_listed_in_declaration_order + assert_equal %w(trombone flute), HasDependenciesController.new.view_cache_dependencies + end +end + +class CollectionCacheController < ActionController::Base + attr_accessor :partial_rendered_times + + def index + @customers = [Customer.new("david", params[:id] || 1)] + end + + def index_ordered + @customers = [Customer.new("david", 1), Customer.new("david", 2), Customer.new("david", 3)] + render "index" + end + + def index_explicit_render_in_controller + @customers = [Customer.new("david", 1)] + render partial: "customers/customer", collection: @customers, cached: true + end + + def index_with_comment + @customers = [Customer.new("david", 1)] + render partial: "customers/commented_customer", collection: @customers, as: :customer, cached: true + end + + def index_with_callable_cache_key + @customers = [Customer.new("david", 1)] + render partial: "customers/customer", collection: @customers, cached: -> customer { "cached_david" } + end +end + +class CollectionCacheTest < ActionController::TestCase + def setup + super + @controller = CollectionCacheController.new + @controller.perform_caching = true + @controller.partial_rendered_times = 0 + @controller.cache_store = ActiveSupport::Cache::MemoryStore.new + ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new + end + + def test_collection_fetches_cached_views + get :index + assert_equal 1, @controller.partial_rendered_times + assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/david/1") + + get :index + assert_equal 1, @controller.partial_rendered_times + end + + def test_preserves_order_when_reading_from_cache_plus_rendering + get :index, params: { id: 2 } + assert_equal 1, @controller.partial_rendered_times + assert_select ":root", "david, 2" + + get :index_ordered + assert_equal 3, @controller.partial_rendered_times + assert_select ":root", "david, 1\n david, 2\n david, 3" + end + + def test_explicit_render_call_with_options + get :index_explicit_render_in_controller + + assert_select ":root", "david, 1" + end + + def test_caching_works_with_beginning_comment + get :index_with_comment + assert_equal 1, @controller.partial_rendered_times + + get :index_with_comment + assert_equal 1, @controller.partial_rendered_times + end + + def test_caching_with_callable_cache_key + get :index_with_callable_cache_key + assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/cached_david") + end +end + +class FragmentCacheKeyTestController < CachingController + attr_accessor :account_id + + fragment_cache_key "v1" + fragment_cache_key { account_id } +end + +class FragmentCacheKeyTest < ActionController::TestCase + def setup + super + @store = ActiveSupport::Cache::MemoryStore.new + @controller = FragmentCacheKeyTestController.new + @controller.perform_caching = true + @controller.cache_store = @store + end + + def test_combined_fragment_cache_key + @controller.account_id = "123" + assert_equal [ :views, "v1", "123", "what a key" ], @controller.combined_fragment_cache_key("what a key") + + @controller.account_id = nil + assert_equal [ :views, "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + end + + def test_combined_fragment_cache_key_with_envs + ENV["RAILS_APP_VERSION"] = "55" + assert_equal [ :views, "55", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + + ENV["RAILS_CACHE_ID"] = "66" + assert_equal [ :views, "66", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + ensure + ENV["RAILS_CACHE_ID"] = ENV["RAILS_APP_VERSION"] = nil + end +end diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb new file mode 100644 index 0000000000..636b025f2c --- /dev/null +++ b/actionpack/test/controller/content_type_test.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class OldContentTypeController < ActionController::Base + # :ported: + def render_content_type_from_body + response.content_type = Mime[:rss] + render body: "hello world!" + end + + # :ported: + def render_defaults + render body: "hello world!" + end + + # :ported: + def render_content_type_from_render + render body: "hello world!", content_type: Mime[:rss] + end + + # :ported: + def render_charset_from_body + response.charset = "utf-16" + render body: "hello world!" + end + + # :ported: + def render_nil_charset_from_body + response.charset = nil + render body: "hello world!" + end + + def render_default_for_erb + end + + def render_default_for_builder + end + + def render_change_for_builder + response.content_type = Mime[:html] + render action: "render_default_for_builder" + end + + def render_default_content_types_for_respond_to + respond_to do |format| + format.html { render body: "hello world!" } + format.xml { render action: "render_default_content_types_for_respond_to" } + format.js { render body: "hello world!" } + format.rss { render body: "hello world!", content_type: Mime[:xml] } + end + end +end + +class ContentTypeTest < ActionController::TestCase + tests OldContentTypeController + + def setup + super + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + @controller.logger = ActiveSupport::Logger.new(nil) + end + + # :ported: + def test_render_defaults + get :render_defaults + assert_equal "utf-8", @response.charset + assert_equal Mime[:text], @response.content_type + end + + def test_render_changed_charset_default + with_default_charset "utf-16" do + get :render_defaults + assert_equal "utf-16", @response.charset + assert_equal Mime[:text], @response.content_type + end + end + + # :ported: + def test_content_type_from_body + get :render_content_type_from_body + assert_equal Mime[:rss], @response.content_type + assert_equal "utf-8", @response.charset + end + + # :ported: + def test_content_type_from_render + get :render_content_type_from_render + assert_equal Mime[:rss], @response.content_type + assert_equal "utf-8", @response.charset + end + + # :ported: + def test_charset_from_body + get :render_charset_from_body + assert_equal Mime[:text], @response.content_type + assert_equal "utf-16", @response.charset + end + + # :ported: + def test_nil_charset_from_body + get :render_nil_charset_from_body + assert_equal Mime[:text], @response.content_type + assert_equal "utf-8", @response.charset, @response.headers.inspect + end + + def test_nil_default_for_erb + with_default_charset nil do + get :render_default_for_erb + assert_equal Mime[:html], @response.content_type + assert_nil @response.charset, @response.headers.inspect + end + end + + def test_default_for_erb + get :render_default_for_erb + assert_equal Mime[:html], @response.content_type + assert_equal "utf-8", @response.charset + end + + def test_default_for_builder + get :render_default_for_builder + assert_equal Mime[:xml], @response.content_type + assert_equal "utf-8", @response.charset + end + + def test_change_for_builder + get :render_change_for_builder + assert_equal Mime[:html], @response.content_type + assert_equal "utf-8", @response.charset + end + + private + + def with_default_charset(charset) + old_default_charset = ActionDispatch::Response.default_charset + ActionDispatch::Response.default_charset = charset + yield + ensure + ActionDispatch::Response.default_charset = old_default_charset + end +end + +class AcceptBasedContentTypeTest < ActionController::TestCase + tests OldContentTypeController + + def test_render_default_content_types_for_respond_to + @request.accept = Mime[:html].to_s + get :render_default_content_types_for_respond_to + assert_equal Mime[:html], @response.content_type + + @request.accept = Mime[:js].to_s + get :render_default_content_types_for_respond_to + assert_equal Mime[:js], @response.content_type + end + + def test_render_default_content_types_for_respond_to_with_template + @request.accept = Mime[:xml].to_s + get :render_default_content_types_for_respond_to + assert_equal Mime[:xml], @response.content_type + end + + def test_render_default_content_types_for_respond_to_with_overwrite + @request.accept = Mime[:rss].to_s + get :render_default_content_types_for_respond_to + assert_equal Mime[:xml], @response.content_type + end +end diff --git a/actionpack/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb b/actionpack/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionpack/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb diff --git a/actionpack/test/controller/controller_fixtures/app/controllers/user_controller.rb b/actionpack/test/controller/controller_fixtures/app/controllers/user_controller.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionpack/test/controller/controller_fixtures/app/controllers/user_controller.rb diff --git a/actionpack/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb b/actionpack/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actionpack/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb diff --git a/actionpack/test/controller/default_url_options_with_before_action_test.rb b/actionpack/test/controller/default_url_options_with_before_action_test.rb new file mode 100644 index 0000000000..fc5b8288cd --- /dev/null +++ b/actionpack/test/controller/default_url_options_with_before_action_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ControllerWithBeforeActionAndDefaultUrlOptions < ActionController::Base + before_action { I18n.locale = params[:locale] } + after_action { I18n.locale = "en" } + + def target + render plain: "final response" + end + + def redirect + redirect_to action: "target" + end + + def default_url_options + { locale: "de" } + end +end + +class ControllerWithBeforeActionAndDefaultUrlOptionsTest < ActionController::TestCase + # This test has its roots in issue #1872 + test "should redirect with correct locale :de" do + get :redirect, params: { locale: "de" } + assert_redirected_to "/controller_with_before_action_and_default_url_options/target?locale=de" + end +end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb new file mode 100644 index 0000000000..104c9eeade --- /dev/null +++ b/actionpack/test/controller/filters_test.rb @@ -0,0 +1,1048 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ActionController::Base + class << self + %w(append_around_action prepend_after_action prepend_around_action prepend_before_action skip_after_action skip_before_action).each do |pending| + define_method(pending) do |*args| + $stderr.puts "#{pending} unimplemented: #{args.inspect}" + end unless method_defined?(pending) + end + + def before_actions + filters = _process_action_callbacks.select { |c| c.kind == :before } + filters.map!(&:raw_filter) + end + end +end + +class FilterTest < ActionController::TestCase + class TestController < ActionController::Base + before_action :ensure_login + after_action :clean_up + + def show + render inline: "ran action" + end + + private + def ensure_login + @ran_filter ||= [] + @ran_filter << "ensure_login" + end + + def clean_up + @ran_after_action ||= [] + @ran_after_action << "clean_up" + end + end + + class ChangingTheRequirementsController < TestController + before_action :ensure_login, except: [:go_wild] + + def go_wild + render plain: "gobble" + end + end + + class TestMultipleFiltersController < ActionController::Base + before_action :try_1 + before_action :try_2 + before_action :try_3 + + (1..3).each do |i| + define_method "fail_#{i}" do + render plain: i.to_s + end + end + + private + (1..3).each do |i| + define_method "try_#{i}" do + instance_variable_set :@try, i + if action_name == "fail_#{i}" + head(404) + end + end + end + end + + class RenderingController < ActionController::Base + before_action :before_action_rendering + after_action :unreached_after_action + + def show + @ran_action = true + render inline: "ran action" + end + + private + def before_action_rendering + @ran_filter ||= [] + @ran_filter << "before_action_rendering" + render inline: "something else" + end + + def unreached_after_action + @ran_filter << "unreached_after_action_after_render" + end + end + + class RenderingForPrependAfterActionController < RenderingController + prepend_after_action :unreached_prepend_after_action + + private + def unreached_prepend_after_action + @ran_filter << "unreached_preprend_after_action_after_render" + end + end + + class BeforeActionRedirectionController < ActionController::Base + before_action :before_action_redirects + after_action :unreached_after_action + + def show + @ran_action = true + render inline: "ran show action" + end + + def target_of_redirection + @ran_target_of_redirection = true + render inline: "ran target_of_redirection action" + end + + private + def before_action_redirects + @ran_filter ||= [] + @ran_filter << "before_action_redirects" + redirect_to(action: "target_of_redirection") + end + + def unreached_after_action + @ran_filter << "unreached_after_action_after_redirection" + end + end + + class BeforeActionRedirectionForPrependAfterActionController < BeforeActionRedirectionController + prepend_after_action :unreached_prepend_after_action_after_redirection + + private + def unreached_prepend_after_action_after_redirection + @ran_filter << "unreached_prepend_after_action_after_redirection" + end + end + + class ConditionalFilterController < ActionController::Base + def show + render inline: "ran action" + end + + def another_action + render inline: "ran action" + end + + def show_without_action + render inline: "ran action without action" + end + + private + def ensure_login + @ran_filter ||= [] + @ran_filter << "ensure_login" + end + + def clean_up_tmp + @ran_filter ||= [] + @ran_filter << "clean_up_tmp" + end + end + + class ConditionalCollectionFilterController < ConditionalFilterController + before_action :ensure_login, except: [ :show_without_action, :another_action ] + end + + class OnlyConditionSymController < ConditionalFilterController + before_action :ensure_login, only: :show + end + + class ExceptConditionSymController < ConditionalFilterController + before_action :ensure_login, except: :show_without_action + end + + class BeforeAndAfterConditionController < ConditionalFilterController + before_action :ensure_login, only: :show + after_action :clean_up_tmp, only: :show + end + + class OnlyConditionProcController < ConditionalFilterController + before_action(only: :show) { |c| c.instance_variable_set(:"@ran_proc_action", true) } + end + + class ExceptConditionProcController < ConditionalFilterController + before_action(except: :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action", true) } + end + + class ConditionalClassFilter + def self.before(controller) controller.instance_variable_set(:"@ran_class_action", true) end + end + + class OnlyConditionClassController < ConditionalFilterController + before_action ConditionalClassFilter, only: :show + end + + class ExceptConditionClassController < ConditionalFilterController + before_action ConditionalClassFilter, except: :show_without_action + end + + class AnomolousYetValidConditionController < ConditionalFilterController + before_action(ConditionalClassFilter, :ensure_login, Proc.new { |c| c.instance_variable_set(:"@ran_proc_action1", true) }, except: :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action2", true) } + end + + class OnlyConditionalOptionsFilter < ConditionalFilterController + before_action :ensure_login, only: :index, if: Proc.new { |c| c.instance_variable_set(:"@ran_conditional_index_proc", true) } + end + + class ConditionalOptionsFilter < ConditionalFilterController + before_action :ensure_login, if: Proc.new { |c| true } + before_action :clean_up_tmp, if: Proc.new { |c| false } + end + + class ConditionalOptionsSkipFilter < ConditionalFilterController + before_action :ensure_login + before_action :clean_up_tmp + + skip_before_action :ensure_login, if: -> { false } + skip_before_action :clean_up_tmp, if: -> { true } + end + + class SkipFilterUsingOnlyAndIf < ConditionalFilterController + before_action :clean_up_tmp + before_action :ensure_login + + skip_before_action :ensure_login, only: :login, if: -> { false } + skip_before_action :clean_up_tmp, only: :login, if: -> { true } + + def login + render plain: "ok" + end + end + + class SkipFilterUsingIfAndExcept < ConditionalFilterController + before_action :clean_up_tmp + before_action :ensure_login + + skip_before_action :ensure_login, if: -> { false }, except: :login + skip_before_action :clean_up_tmp, if: -> { true }, except: :login + + def login + render plain: "ok" + end + end + + class ClassController < ConditionalFilterController + before_action ConditionalClassFilter + end + + class PrependingController < TestController + prepend_before_action :wonderful_life + # skip_before_action :fire_flash + + private + def wonderful_life + @ran_filter ||= [] + @ran_filter << "wonderful_life" + end + end + + class SkippingAndLimitedController < TestController + skip_before_action :ensure_login + before_action :ensure_login, only: :index + + def index + render plain: "ok" + end + + def public + render plain: "ok" + end + end + + class SkippingAndReorderingController < TestController + skip_before_action :ensure_login + before_action :find_record + before_action :ensure_login + + def index + render plain: "ok" + end + + private + def find_record + @ran_filter ||= [] + @ran_filter << "find_record" + end + end + + class ConditionalSkippingController < TestController + skip_before_action :ensure_login, only: [ :login ] + skip_after_action :clean_up, only: [ :login ] + + before_action :find_user, only: [ :change_password ] + + def login + render inline: "ran action" + end + + def change_password + render inline: "ran action" + end + + private + def find_user + @ran_filter ||= [] + @ran_filter << "find_user" + end + end + + class ConditionalParentOfConditionalSkippingController < ConditionalFilterController + before_action :conditional_in_parent_before, only: [:show, :another_action] + after_action :conditional_in_parent_after, only: [:show, :another_action] + + private + + def conditional_in_parent_before + @ran_filter ||= [] + @ran_filter << "conditional_in_parent_before" + end + + def conditional_in_parent_after + @ran_filter ||= [] + @ran_filter << "conditional_in_parent_after" + end + end + + class ChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController + skip_before_action :conditional_in_parent_before, only: :another_action + skip_after_action :conditional_in_parent_after, only: :another_action + end + + class AnotherChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController + skip_before_action :conditional_in_parent_before, only: :show + end + + class ProcController < PrependingController + before_action(proc { |c| c.instance_variable_set(:"@ran_proc_action", true) }) + end + + class ImplicitProcController < PrependingController + before_action { |c| c.instance_variable_set(:"@ran_proc_action", true) } + end + + class AuditFilter + def self.before(controller) + controller.instance_variable_set(:"@was_audited", true) + end + end + + class AroundFilter + def before(controller) + @execution_log = "before" + controller.class.execution_log += " before aroundfilter " if controller.respond_to? :execution_log + controller.instance_variable_set(:"@before_ran", true) + end + + def after(controller) + controller.instance_variable_set(:"@execution_log", @execution_log + " and after") + controller.instance_variable_set(:"@after_ran", true) + controller.class.execution_log << " after aroundfilter " if controller.respond_to? :execution_log + end + + def around(controller) + before(controller) + yield + after(controller) + end + end + + class AppendedAroundFilter + def before(controller) + controller.class.execution_log << " before appended aroundfilter " + end + + def after(controller) + controller.class.execution_log << " after appended aroundfilter " + end + + def around(controller) + before(controller) + yield + after(controller) + end + end + + class AuditController < ActionController::Base + before_action(AuditFilter) + + def show + render plain: "hello" + end + end + + class AroundFilterController < PrependingController + around_action AroundFilter.new + end + + class BeforeAfterClassFilterController < PrependingController + begin + filter = AroundFilter.new + before_action filter + after_action filter + end + end + + class MixedFilterController < PrependingController + cattr_accessor :execution_log + + def initialize + @@execution_log = "" + super() + end + + before_action { |c| c.class.execution_log << " before procfilter " } + prepend_around_action AroundFilter.new + + after_action { |c| c.class.execution_log << " after procfilter " } + append_around_action AppendedAroundFilter.new + end + + class MixedSpecializationController < ActionController::Base + class OutOfOrder < StandardError; end + + before_action :first + before_action :second, only: :foo + + def foo + render plain: "foo" + end + + def bar + render plain: "bar" + end + + private + def first + @first = true + end + + def second + raise OutOfOrder unless @first + end + end + + class DynamicDispatchController < ActionController::Base + before_action :choose + + %w(foo bar baz).each do |action| + define_method(action) { render plain: action } + end + + private + def choose + self.action_name = params[:choose] + end + end + + class PrependingBeforeAndAfterController < ActionController::Base + prepend_before_action :before_all + prepend_after_action :after_all + before_action :between_before_all_and_after_all + after_action :between_before_all_and_after_all + + def before_all + @ran_filter ||= [] + @ran_filter << "before_all" + end + + def after_all + @ran_filter ||= [] + @ran_filter << "after_all" + end + + def between_before_all_and_after_all + @ran_filter ||= [] + @ran_filter << "between_before_all_and_after_all" + end + + def show + render plain: "hello" + end + end + + class ErrorToRescue < Exception; end + + class RescuingAroundFilterWithBlock + def around(controller) + yield + rescue ErrorToRescue => ex + controller.__send__ :render, plain: "I rescued this: #{ex.inspect}" + end + end + + class RescuedController < ActionController::Base + around_action RescuingAroundFilterWithBlock.new + + def show + raise ErrorToRescue.new("Something made the bad noise.") + end + end + + class NonYieldingAroundFilterController < ActionController::Base + before_action :filter_one + around_action :non_yielding_action + before_action :action_two + after_action :action_three + + def index + render inline: "index" + end + + private + + def filter_one + @filters ||= [] + @filters << "filter_one" + end + + def action_two + @filters << "action_two" + end + + def non_yielding_action + @filters << "it didn't yield" + end + + def action_three + @filters << "action_three" + end + end + + class ImplicitActionsController < ActionController::Base + before_action :find_only, only: :edit + before_action :find_except, except: :edit + + private + + def find_only + @only = "Only" + end + + def find_except + @except = "Except" + end + end + + def test_non_yielding_around_actions_do_not_raise + controller = NonYieldingAroundFilterController.new + assert_nothing_raised do + test_process(controller, "index") + end + end + + def test_after_actions_are_not_run_if_around_action_does_not_yield + controller = NonYieldingAroundFilterController.new + test_process(controller, "index") + assert_equal ["filter_one", "it didn't yield"], controller.instance_variable_get(:@filters) + end + + def test_added_action_to_inheritance_graph + assert_equal [ :ensure_login ], TestController.before_actions + end + + def test_base_class_in_isolation + assert_equal [ ], ActionController::Base.before_actions + end + + def test_prepending_action + assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_actions + end + + def test_running_actions + test_process(PrependingController) + assert_equal %w( wonderful_life ensure_login ), + @controller.instance_variable_get(:@ran_filter) + end + + def test_running_actions_with_proc + test_process(ProcController) + assert @controller.instance_variable_get(:@ran_proc_action) + end + + def test_running_actions_with_implicit_proc + test_process(ImplicitProcController) + assert @controller.instance_variable_get(:@ran_proc_action) + end + + def test_running_actions_with_class + test_process(AuditController) + assert @controller.instance_variable_get(:@was_audited) + end + + def test_running_anomalous_yet_valid_condition_actions + test_process(AnomolousYetValidConditionController) + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) + assert @controller.instance_variable_get(:@ran_class_action) + assert @controller.instance_variable_get(:@ran_proc_action1) + assert @controller.instance_variable_get(:@ran_proc_action2) + + test_process(AnomolousYetValidConditionController, "show_without_action") + assert_not @controller.instance_variable_defined?(:@ran_filter) + assert_not @controller.instance_variable_defined?(:@ran_class_action) + assert_not @controller.instance_variable_defined?(:@ran_proc_action1) + assert_not @controller.instance_variable_defined?(:@ran_proc_action2) + end + + def test_running_conditional_options + test_process(ConditionalOptionsFilter) + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) + end + + def test_running_conditional_skip_options + test_process(ConditionalOptionsSkipFilter) + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) + end + + def test_if_is_ignored_when_used_with_only + test_process(SkipFilterUsingOnlyAndIf, "login") + assert_not @controller.instance_variable_defined?(:@ran_filter) + end + + def test_except_is_ignored_when_used_with_if + test_process(SkipFilterUsingIfAndExcept, "login") + assert_equal %w(ensure_login), @controller.instance_variable_get(:@ran_filter) + end + + def test_skipping_class_actions + test_process(ClassController) + assert_equal true, @controller.instance_variable_get(:@ran_class_action) + + skipping_class_controller = Class.new(ClassController) do + skip_before_action ConditionalClassFilter + end + + test_process(skipping_class_controller) + assert_not @controller.instance_variable_defined?(:@ran_class_action) + end + + def test_running_collection_condition_actions + test_process(ConditionalCollectionFilterController) + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) + test_process(ConditionalCollectionFilterController, "show_without_action") + assert_not @controller.instance_variable_defined?(:@ran_filter) + test_process(ConditionalCollectionFilterController, "another_action") + assert_not @controller.instance_variable_defined?(:@ran_filter) + end + + def test_running_only_condition_actions + test_process(OnlyConditionSymController) + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) + test_process(OnlyConditionSymController, "show_without_action") + assert_not @controller.instance_variable_defined?(:@ran_filter) + + test_process(OnlyConditionProcController) + assert @controller.instance_variable_get(:@ran_proc_action) + test_process(OnlyConditionProcController, "show_without_action") + assert_not @controller.instance_variable_defined?(:@ran_proc_action) + + test_process(OnlyConditionClassController) + assert @controller.instance_variable_get(:@ran_class_action) + test_process(OnlyConditionClassController, "show_without_action") + assert_not @controller.instance_variable_defined?(:@ran_class_action) + end + + def test_running_except_condition_actions + test_process(ExceptConditionSymController) + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) + test_process(ExceptConditionSymController, "show_without_action") + assert_not @controller.instance_variable_defined?(:@ran_filter) + + test_process(ExceptConditionProcController) + assert @controller.instance_variable_get(:@ran_proc_action) + test_process(ExceptConditionProcController, "show_without_action") + assert_not @controller.instance_variable_defined?(:@ran_proc_action) + + test_process(ExceptConditionClassController) + assert @controller.instance_variable_get(:@ran_class_action) + test_process(ExceptConditionClassController, "show_without_action") + assert_not @controller.instance_variable_defined?(:@ran_class_action) + end + + def test_running_only_condition_and_conditional_options + test_process(OnlyConditionalOptionsFilter, "show") + assert_not @controller.instance_variable_defined?(:@ran_conditional_index_proc) + end + + def test_running_before_and_after_condition_actions + test_process(BeforeAndAfterConditionController) + assert_equal %w( ensure_login clean_up_tmp), @controller.instance_variable_get(:@ran_filter) + test_process(BeforeAndAfterConditionController, "show_without_action") + assert_not @controller.instance_variable_defined?(:@ran_filter) + end + + def test_around_action + test_process(AroundFilterController) + assert @controller.instance_variable_get(:@before_ran) + assert @controller.instance_variable_get(:@after_ran) + end + + def test_before_after_class_action + test_process(BeforeAfterClassFilterController) + assert @controller.instance_variable_get(:@before_ran) + assert @controller.instance_variable_get(:@after_ran) + end + + def test_having_properties_in_around_action + test_process(AroundFilterController) + assert_equal "before and after", @controller.instance_variable_get(:@execution_log) + end + + def test_prepending_and_appending_around_action + test_process(MixedFilterController) + assert_equal " before aroundfilter before procfilter before appended aroundfilter " \ + " after appended aroundfilter after procfilter after aroundfilter ", + MixedFilterController.execution_log + end + + def test_rendering_breaks_actioning_chain + response = test_process(RenderingController) + assert_equal "something else", response.body + assert_not @controller.instance_variable_defined?(:@ran_action) + end + + def test_before_action_rendering_breaks_actioning_chain_for_after_action + test_process(RenderingController) + assert_equal %w( before_action_rendering ), @controller.instance_variable_get(:@ran_filter) + assert_not @controller.instance_variable_defined?(:@ran_action) + end + + def test_before_action_redirects_breaks_actioning_chain_for_after_action + test_process(BeforeActionRedirectionController) + assert_response :redirect + assert_equal "http://test.host/filter_test/before_action_redirection/target_of_redirection", redirect_to_url + assert_equal %w( before_action_redirects ), @controller.instance_variable_get(:@ran_filter) + end + + def test_before_action_rendering_breaks_actioning_chain_for_preprend_after_action + test_process(RenderingForPrependAfterActionController) + assert_equal %w( before_action_rendering ), @controller.instance_variable_get(:@ran_filter) + assert_not @controller.instance_variable_defined?(:@ran_action) + end + + def test_before_action_redirects_breaks_actioning_chain_for_preprend_after_action + test_process(BeforeActionRedirectionForPrependAfterActionController) + assert_response :redirect + assert_equal "http://test.host/filter_test/before_action_redirection_for_prepend_after_action/target_of_redirection", redirect_to_url + assert_equal %w( before_action_redirects ), @controller.instance_variable_get(:@ran_filter) + end + + def test_actions_with_mixed_specialization_run_in_order + assert_nothing_raised do + response = test_process(MixedSpecializationController, "bar") + assert_equal "bar", response.body + end + + assert_nothing_raised do + response = test_process(MixedSpecializationController, "foo") + assert_equal "foo", response.body + end + end + + def test_dynamic_dispatch + %w(foo bar baz).each do |action| + @request.query_parameters[:choose] = action + response = DynamicDispatchController.action(action).call(@request.env).last + assert_equal action, response.body + end + end + + def test_running_prepended_before_and_after_action + test_process(PrependingBeforeAndAfterController) + assert_equal %w( before_all between_before_all_and_after_all between_before_all_and_after_all after_all ), @controller.instance_variable_get(:@ran_filter) + end + + def test_skipping_and_limiting_controller + test_process(SkippingAndLimitedController, "index") + assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) + test_process(SkippingAndLimitedController, "public") + assert_not @controller.instance_variable_defined?(:@ran_filter) + end + + def test_skipping_and_reordering_controller + test_process(SkippingAndReorderingController, "index") + assert_equal %w( find_record ensure_login ), @controller.instance_variable_get(:@ran_filter) + end + + def test_conditional_skipping_of_actions + test_process(ConditionalSkippingController, "login") + assert_not @controller.instance_variable_defined?(:@ran_filter) + test_process(ConditionalSkippingController, "change_password") + assert_equal %w( ensure_login find_user ), @controller.instance_variable_get(:@ran_filter) + + test_process(ConditionalSkippingController, "login") + assert_not @controller.instance_variable_defined?("@ran_after_action") + test_process(ConditionalSkippingController, "change_password") + assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_action") + end + + def test_conditional_skipping_of_actions_when_parent_action_is_also_conditional + test_process(ChildOfConditionalParentController) + assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) + test_process(ChildOfConditionalParentController, "another_action") + assert_not @controller.instance_variable_defined?(:@ran_filter) + end + + def test_condition_skipping_of_actions_when_siblings_also_have_conditions + test_process(ChildOfConditionalParentController) + assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) + test_process(AnotherChildOfConditionalParentController) + assert_equal %w( conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) + test_process(ChildOfConditionalParentController) + assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), @controller.instance_variable_get(:@ran_filter) + end + + def test_changing_the_requirements + test_process(ChangingTheRequirementsController, "go_wild") + assert_not @controller.instance_variable_defined?(:@ran_filter) + end + + def test_a_rescuing_around_action + response = nil + assert_nothing_raised do + response = test_process(RescuedController) + end + + assert_predicate response, :successful? + assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body) + end + + def test_actions_obey_only_and_except_for_implicit_actions + test_process(ImplicitActionsController, "show") + assert_equal "Except", @controller.instance_variable_get(:@except) + assert_not @controller.instance_variable_defined?(:@only) + assert_equal "show", response.body + + test_process(ImplicitActionsController, "edit") + assert_equal "Only", @controller.instance_variable_get(:@only) + assert_not @controller.instance_variable_defined?(:@except) + assert_equal "edit", response.body + end + + private + def test_process(controller, action = "show") + @controller = controller.is_a?(Class) ? controller.new : controller + + process(action) + end +end + +class PostsController < ActionController::Base + module AroundExceptions + class Error < StandardError ; end + class Before < Error ; end + class After < Error ; end + end + include AroundExceptions + + class DefaultFilter + include AroundExceptions + end + + module_eval %w(raises_before raises_after raises_both no_raise no_action).map { |action| "def #{action}; default_action end" }.join("\n") + + private + def default_action + render inline: "#{action_name} called" + end +end + +class ControllerWithSymbolAsFilter < PostsController + around_action :raise_before, only: :raises_before + around_action :raise_after, only: :raises_after + around_action :without_exception, only: :no_raise + + private + def raise_before + raise Before + yield + end + + def raise_after + yield + raise After + end + + def without_exception + # Do stuff... + wtf = 1 + 1 + + yield + + # Do stuff... + wtf += 1 + end +end + +class ControllerWithFilterClass < PostsController + class YieldingFilter < DefaultFilter + def self.around(controller) + yield + raise After + end + end + + around_action YieldingFilter, only: :raises_after +end + +class ControllerWithFilterInstance < PostsController + class YieldingFilter < DefaultFilter + def around(controller) + yield + raise After + end + end + + around_action YieldingFilter.new, only: :raises_after +end + +class ControllerWithProcFilter < PostsController + around_action(only: :no_raise) do |c, b| + c.instance_variable_set(:"@before", true) + b.call + c.instance_variable_set(:"@after", true) + end +end + +class ControllerWithNestedFilters < ControllerWithSymbolAsFilter + around_action :raise_before, :raise_after, :without_exception, only: :raises_both +end + +class ControllerWithAllTypesOfFilters < PostsController + before_action :before + around_action :around + after_action :after + around_action :around_again + + private + def before + @ran_filter ||= [] + @ran_filter << "before" + end + + def around + @ran_filter << "around (before yield)" + yield + @ran_filter << "around (after yield)" + end + + def after + @ran_filter << "after" + end + + def around_again + @ran_filter << "around_again (before yield)" + yield + @ran_filter << "around_again (after yield)" + end +end + +class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters + skip_around_action :around_again + skip_after_action :after +end + +class YieldingAroundFiltersTest < ActionController::TestCase + include PostsController::AroundExceptions + + def test_base + controller = PostsController + assert_nothing_raised { test_process(controller, "no_raise") } + assert_nothing_raised { test_process(controller, "raises_before") } + assert_nothing_raised { test_process(controller, "raises_after") } + assert_nothing_raised { test_process(controller, "no_action") } + end + + def test_with_symbol + controller = ControllerWithSymbolAsFilter + assert_nothing_raised { test_process(controller, "no_raise") } + assert_raise(Before) { test_process(controller, "raises_before") } + assert_raise(After) { test_process(controller, "raises_after") } + assert_nothing_raised { test_process(controller, "no_raise") } + end + + def test_with_class + controller = ControllerWithFilterClass + assert_nothing_raised { test_process(controller, "no_raise") } + assert_raise(After) { test_process(controller, "raises_after") } + end + + def test_with_instance + controller = ControllerWithFilterInstance + assert_nothing_raised { test_process(controller, "no_raise") } + assert_raise(After) { test_process(controller, "raises_after") } + end + + def test_with_proc + test_process(ControllerWithProcFilter, "no_raise") + assert @controller.instance_variable_get(:@before) + assert @controller.instance_variable_get(:@after) + end + + def test_nested_actions + controller = ControllerWithNestedFilters + assert_nothing_raised do + test_process(controller, "raises_both") + rescue Before, After + end + assert_raise Before do + test_process(controller, "raises_both") + rescue After + end + end + + def test_action_order_with_all_action_types + test_process(ControllerWithAllTypesOfFilters, "no_raise") + assert_equal "before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)", @controller.instance_variable_get(:@ran_filter).join(" ") + end + + def test_action_order_with_skip_action_method + test_process(ControllerWithTwoLessFilters, "no_raise") + assert_equal "before around (before yield) around (after yield)", @controller.instance_variable_get(:@ran_filter).join(" ") + end + + def test_first_action_in_multiple_before_action_chain_halts + controller = ::FilterTest::TestMultipleFiltersController.new + response = test_process(controller, "fail_1") + assert_equal "", response.body + assert_equal 1, controller.instance_variable_get(:@try) + end + + def test_second_action_in_multiple_before_action_chain_halts + controller = ::FilterTest::TestMultipleFiltersController.new + response = test_process(controller, "fail_2") + assert_equal "", response.body + assert_equal 2, controller.instance_variable_get(:@try) + end + + def test_last_action_in_multiple_before_action_chain_halts + controller = ::FilterTest::TestMultipleFiltersController.new + response = test_process(controller, "fail_3") + assert_equal "", response.body + assert_equal 3, controller.instance_variable_get(:@try) + end + + private + def test_process(controller, action = "show") + @controller = controller.is_a?(Class) ? controller.new : controller + process(action) + end +end diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb new file mode 100644 index 0000000000..e3ec5bb7fc --- /dev/null +++ b/actionpack/test/controller/flash_hash_test.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + class FlashHashTest < ActiveSupport::TestCase + def setup + @hash = Flash::FlashHash.new + end + + def test_set_get + @hash[:foo] = "zomg" + assert_equal "zomg", @hash[:foo] + end + + def test_keys + assert_equal [], @hash.keys + + @hash["foo"] = "zomg" + assert_equal ["foo"], @hash.keys + + @hash["bar"] = "zomg" + assert_equal ["foo", "bar"].sort, @hash.keys.sort + end + + def test_update + @hash["foo"] = "bar" + @hash.update("foo" => "baz", "hello" => "world") + + assert_equal "baz", @hash["foo"] + assert_equal "world", @hash["hello"] + end + + def test_key + @hash["foo"] = "bar" + + assert @hash.key?("foo") + assert @hash.key?(:foo) + assert_not @hash.key?("bar") + assert_not @hash.key?(:bar) + end + + def test_delete + @hash["foo"] = "bar" + @hash.delete "foo" + + assert_not @hash.key?("foo") + assert_nil @hash["foo"] + end + + def test_to_hash + @hash["foo"] = "bar" + assert_equal({ "foo" => "bar" }, @hash.to_hash) + + @hash.to_hash["zomg"] = "aaron" + assert_not @hash.key?("zomg") + assert_equal({ "foo" => "bar" }, @hash.to_hash) + end + + def test_to_session_value + @hash["foo"] = "bar" + assert_equal({ "discard" => [], "flashes" => { "foo" => "bar" } }, @hash.to_session_value) + + @hash.now["qux"] = 1 + assert_equal({ "flashes" => { "foo" => "bar" }, "discard" => [] }, @hash.to_session_value) + + @hash.discard("foo") + assert_nil(@hash.to_session_value) + + @hash.sweep + assert_nil(@hash.to_session_value) + end + + def test_from_session_value + # {"session_id"=>"f8e1b8152ba7609c28bbb17ec9263ba7", "flash"=>#<ActionDispatch::Flash::FlashHash:0x00000000000000 @used=#<Set: {"farewell"}>, @closed=false, @flashes={"greeting"=>"Hello", "farewell"=>"Goodbye"}, @now=nil>} + rails_3_2_cookie = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY4ZTFiODE1MmJhNzYwOWMyOGJiYjE3ZWM5MjYzYmE3BjsAVEkiCmZsYXNoBjsARm86JUFjdGlvbkRpc3BhdGNoOjpGbGFzaDo6Rmxhc2hIYXNoCToKQHVzZWRvOghTZXQGOgpAaGFzaHsGSSINZmFyZXdlbGwGOwBUVDoMQGNsb3NlZEY6DUBmbGFzaGVzewdJIg1ncmVldGluZwY7AFRJIgpIZWxsbwY7AFRJIg1mYXJld2VsbAY7AFRJIgxHb29kYnllBjsAVDoJQG5vdzA=" + session = Marshal.load(Base64.decode64(rails_3_2_cookie)) + hash = Flash::FlashHash.from_session_value(session["flash"]) + assert_equal({ "greeting" => "Hello" }, hash.to_hash) + assert_nil(hash.to_session_value) + end + + def test_from_session_value_on_json_serializer + decrypted_data = "{ \"session_id\":\"d98bdf6d129618fc2548c354c161cfb5\", \"flash\":{\"discard\":[\"farewell\"], \"flashes\":{\"greeting\":\"Hello\",\"farewell\":\"Goodbye\"}} }" + session = ActionDispatch::Cookies::JsonSerializer.load(decrypted_data) + hash = Flash::FlashHash.from_session_value(session["flash"]) + + assert_equal({ "greeting" => "Hello" }, hash.to_hash) + assert_nil(hash.to_session_value) + assert_equal "Hello", hash[:greeting] + assert_equal "Hello", hash["greeting"] + end + + def test_empty? + assert_empty @hash + @hash["zomg"] = "bears" + assert_not_empty @hash + @hash.clear + assert_empty @hash + end + + def test_each + @hash["hello"] = "world" + @hash["foo"] = "bar" + + things = [] + @hash.each do |k, v| + things << [k, v] + end + + assert_equal([%w{ hello world }, %w{ foo bar }].sort, things.sort) + end + + def test_replace + @hash["hello"] = "world" + @hash.replace("omg" => "aaron") + assert_equal({ "omg" => "aaron" }, @hash.to_hash) + end + + def test_discard_no_args + @hash["hello"] = "world" + @hash.discard + + @hash.sweep + assert_equal({}, @hash.to_hash) + end + + def test_discard_one_arg + @hash["hello"] = "world" + @hash["omg"] = "world" + @hash.discard "hello" + + @hash.sweep + assert_equal({ "omg" => "world" }, @hash.to_hash) + end + + def test_keep_sweep + @hash["hello"] = "world" + + @hash.sweep + assert_equal({ "hello" => "world" }, @hash.to_hash) + end + + def test_update_sweep + @hash["hello"] = "world" + @hash.update("hi" => "mom") + + @hash.sweep + assert_equal({ "hello" => "world", "hi" => "mom" }, @hash.to_hash) + end + + def test_update_delete_sweep + @hash["hello"] = "world" + @hash.delete "hello" + @hash.update("hello" => "mom") + + @hash.sweep + assert_equal({ "hello" => "mom" }, @hash.to_hash) + end + + def test_delete_sweep + @hash["hello"] = "world" + @hash["hi"] = "mom" + @hash.delete "hi" + + @hash.sweep + assert_equal({ "hello" => "world" }, @hash.to_hash) + end + + def test_clear_sweep + @hash["hello"] = "world" + @hash.clear + + @hash.sweep + assert_equal({}, @hash.to_hash) + end + + def test_replace_sweep + @hash["hello"] = "world" + @hash.replace("hi" => "mom") + + @hash.sweep + assert_equal({ "hi" => "mom" }, @hash.to_hash) + end + + def test_discard_then_add + @hash["hello"] = "world" + @hash["omg"] = "world" + @hash.discard "hello" + @hash["hello"] = "world" + + @hash.sweep + assert_equal({ "omg" => "world", "hello" => "world" }, @hash.to_hash) + end + + def test_keep_all_sweep + @hash["hello"] = "world" + @hash["omg"] = "world" + @hash.discard "hello" + @hash.keep + + @hash.sweep + assert_equal({ "omg" => "world", "hello" => "world" }, @hash.to_hash) + end + + def test_double_sweep + @hash["hello"] = "world" + @hash.sweep + + assert_equal({ "hello" => "world" }, @hash.to_hash) + + @hash.sweep + assert_equal({}, @hash.to_hash) + end + end +end diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb new file mode 100644 index 0000000000..409a4ec2e6 --- /dev/null +++ b/actionpack/test/controller/flash_test.rb @@ -0,0 +1,388 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/messages/rotation_configuration" + +class FlashTest < ActionController::TestCase + class TestController < ActionController::Base + def set_flash + flash["that"] = "hello" + render inline: "hello" + end + + def set_flash_now + flash.now["that"] = "hello" + flash.now["foo"] ||= "bar" + flash.now["foo"] ||= "err" + @flashy = flash.now["that"] + @flash_copy = {}.update flash + render inline: "hello" + end + + def attempt_to_use_flash_now + @flash_copy = {}.update flash + @flashy = flash["that"] + render inline: "hello" + end + + def use_flash + @flash_copy = {}.update flash + @flashy = flash["that"] + render inline: "hello" + end + + def use_flash_and_keep_it + @flash_copy = {}.update flash + @flashy = flash["that"] + flash.keep + render inline: "hello" + end + + def use_flash_and_update_it + flash.update("this" => "hello again") + @flash_copy = {}.update flash + render inline: "hello" + end + + def use_flash_after_reset_session + flash["that"] = "hello" + @flashy_that = flash["that"] + reset_session + @flashy_that_reset = flash["that"] + flash["this"] = "good-bye" + @flashy_this = flash["this"] + render inline: "hello" + end + + # methods for test_sweep_after_halted_action_chain + before_action :halt_and_redir, only: "filter_halting_action" + + def std_action + @flash_copy = {}.update(flash) + head :ok + end + + def filter_halting_action + @flash_copy = {}.update(flash) + end + + def halt_and_redir + flash["foo"] = "bar" + redirect_to action: "std_action" + @flash_copy = {}.update(flash) + end + + def redirect_with_alert + redirect_to "/nowhere", alert: "Beware the nowheres!" + end + + def redirect_with_notice + redirect_to "/somewhere", notice: "Good luck in the somewheres!" + end + + def render_with_flash_now_alert + flash.now.alert = "Beware the nowheres now!" + render inline: "hello" + end + + def render_with_flash_now_notice + flash.now.notice = "Good luck in the somewheres now!" + render inline: "hello" + end + + def redirect_with_other_flashes + redirect_to "/wonderland", flash: { joyride: "Horses!" } + end + + def redirect_with_foo_flash + redirect_to "/wonderland", foo: "for great justice" + end + end + + tests TestController + + def test_flash + get :set_flash + + get :use_flash + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "hello", @controller.instance_variable_get(:@flashy) + + get :use_flash + assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On second flash" + end + + def test_keep_flash + get :set_flash + + get :use_flash_and_keep_it + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "hello", @controller.instance_variable_get(:@flashy) + + get :use_flash + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"], "On second flash" + + get :use_flash + assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On third flash" + end + + def test_flash_now + get :set_flash_now + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"] + assert_equal "hello", @controller.instance_variable_get(:@flashy) + + get :attempt_to_use_flash_now + assert_nil @controller.instance_variable_get(:@flash_copy)["that"] + assert_nil @controller.instance_variable_get(:@flash_copy)["foo"] + assert_nil @controller.instance_variable_get(:@flashy) + end + + def test_update_flash + get :set_flash + get :use_flash_and_update_it + assert_equal "hello", @controller.instance_variable_get(:@flash_copy)["that"] + assert_equal "hello again", @controller.instance_variable_get(:@flash_copy)["this"] + get :use_flash + assert_nil @controller.instance_variable_get(:@flash_copy)["that"], "On second flash" + assert_equal "hello again", + @controller.instance_variable_get(:@flash_copy)["this"], "On second flash" + end + + def test_flash_after_reset_session + get :use_flash_after_reset_session + assert_equal "hello", @controller.instance_variable_get(:@flashy_that) + assert_equal "good-bye", @controller.instance_variable_get(:@flashy_this) + assert_nil @controller.instance_variable_get(:@flashy_that_reset) + end + + def test_does_not_set_the_session_if_the_flash_is_empty + get :std_action + assert_nil session["flash"] + end + + def test_sweep_after_halted_action_chain + get :std_action + assert_nil @controller.instance_variable_get(:@flash_copy)["foo"] + get :filter_halting_action + assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"] + get :std_action # follow redirection + assert_equal "bar", @controller.instance_variable_get(:@flash_copy)["foo"] + get :std_action + assert_nil @controller.instance_variable_get(:@flash_copy)["foo"] + end + + def test_keep_and_discard_return_values + flash = ActionDispatch::Flash::FlashHash.new + flash.update(foo: :foo_indeed, bar: :bar_indeed) + + assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed + assert_nil flash.discard(:unknown) # non existent key passed + assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.discard().to_hash) # nothing passed + assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.discard(nil).to_hash) # nothing passed + + assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed + assert_nil flash.keep(:unknown) # non existent key passed + assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.keep().to_hash) # nothing passed + assert_equal({ "foo" => :foo_indeed, "bar" => :bar_indeed }, flash.keep(nil).to_hash) # nothing passed + end + + def test_redirect_to_with_alert + get :redirect_with_alert + assert_equal "Beware the nowheres!", @controller.send(:flash)[:alert] + end + + def test_redirect_to_with_notice + get :redirect_with_notice + assert_equal "Good luck in the somewheres!", @controller.send(:flash)[:notice] + end + + def test_render_with_flash_now_alert + get :render_with_flash_now_alert + assert_equal "Beware the nowheres now!", @controller.send(:flash)[:alert] + end + + def test_render_with_flash_now_notice + get :render_with_flash_now_notice + assert_equal "Good luck in the somewheres now!", @controller.send(:flash)[:notice] + end + + def test_redirect_to_with_other_flashes + get :redirect_with_other_flashes + assert_equal "Horses!", @controller.send(:flash)[:joyride] + end + + def test_redirect_to_with_adding_flash_types + original_controller = @controller + test_controller_with_flash_type_foo = Class.new(TestController) do + add_flash_types :foo + end + @controller = test_controller_with_flash_type_foo.new + get :redirect_with_foo_flash + assert_equal "for great justice", @controller.send(:flash)[:foo] + ensure + @controller = original_controller + end + + def test_add_flash_type_to_subclasses + test_controller_with_flash_type_foo = Class.new(TestController) do + add_flash_types :foo + end + subclass_controller_with_no_flash_type = Class.new(test_controller_with_flash_type_foo) + assert_includes subclass_controller_with_no_flash_type._flash_types, :foo + end + + def test_does_not_add_flash_type_to_parent_class + Class.new(TestController) do + add_flash_types :bar + end + assert_not TestController._flash_types.include?(:bar) + end +end + +class FlashIntegrationTest < ActionDispatch::IntegrationTest + SessionKey = "_myapp_session" + Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33") + Rotations = ActiveSupport::Messages::RotationConfiguration.new + + class TestController < ActionController::Base + add_flash_types :bar + + def set_flash + flash["that"] = "hello" + head :ok + end + + def set_flash_now + flash.now["that"] = "hello" + head :ok + end + + def use_flash + render inline: "flash: #{flash["that"]}" + end + + def set_bar + flash[:bar] = "for great justice" + head :ok + end + + def set_flash_optionally + flash.now.notice = params[:flash] + if stale? etag: "abe" + render inline: "maybe flash" + end + end + end + + def test_flash + with_test_route_set do + get "/set_flash" + assert_response :success + assert_equal "hello", @request.flash["that"] + + get "/use_flash" + assert_response :success + assert_equal "flash: hello", @response.body + end + end + + def test_just_using_flash_does_not_stream_a_cookie_back + with_test_route_set do + get "/use_flash" + assert_response :success + assert_nil @response.headers["Set-Cookie"] + assert_equal "flash: ", @response.body + end + end + + def test_setting_flash_does_not_raise_in_following_requests + with_test_route_set do + env = { "action_dispatch.request.flash_hash" => ActionDispatch::Flash::FlashHash.new } + get "/set_flash", env: env + get "/set_flash", env: env + end + end + + def test_setting_flash_now_does_not_raise_in_following_requests + with_test_route_set do + env = { "action_dispatch.request.flash_hash" => ActionDispatch::Flash::FlashHash.new } + get "/set_flash_now", env: env + get "/set_flash_now", env: env + end + end + + def test_added_flash_types_method + with_test_route_set do + get "/set_bar" + assert_response :success + assert_equal "for great justice", @controller.bar + end + end + + def test_flash_factored_into_etag + with_test_route_set do + get "/set_flash_optionally" + no_flash_etag = response.etag + + get "/set_flash_optionally", params: { flash: "hello!" } + hello_flash_etag = response.etag + + assert_not_equal no_flash_etag, hello_flash_etag + + get "/set_flash_optionally", params: { flash: "hello!" } + another_hello_flash_etag = response.etag + + assert_equal another_hello_flash_etag, hello_flash_etag + + get "/set_flash_optionally", params: { flash: "goodbye!" } + goodbye_flash_etag = response.etag + + assert_not_equal another_hello_flash_etag, goodbye_flash_etag + end + end + + def test_flash_usable_in_metal_without_helper + controller_class = nil + + assert_nothing_raised do + controller_class = Class.new(ActionController::Metal) do + include ActionController::Flash + end + end + + controller = controller_class.new + + assert_respond_to controller, :alert + assert_respond_to controller, :notice + end + + private + + # Overwrite get to send SessionSecret in env hash + def get(path, *args) + args[0] ||= {} + args[0][:env] ||= {} + args[0][:env]["action_dispatch.key_generator"] ||= Generator + args[0][:env]["action_dispatch.cookies_rotations"] = Rotations + super(path, *args) + end + + def with_test_route_set + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + get ":action", to: FlashIntegrationTest::TestController + end + end + + @app = self.class.build_app(set) do |middleware| + middleware.use ActionDispatch::Session::CookieStore, key: SessionKey + middleware.use ActionDispatch::Flash + middleware.delete ActionDispatch::ShowExceptions + end + + yield + end + end +end diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb new file mode 100644 index 0000000000..7f59f6acaf --- /dev/null +++ b/actionpack/test/controller/force_ssl_test.rb @@ -0,0 +1,345 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ForceSSLController < ActionController::Base + def banana + render plain: "monkey" + end + + def cheeseburger + render plain: "sikachu" + end +end + +class ForceSSLControllerLevel < ForceSSLController + ActiveSupport::Deprecation.silence do + force_ssl + end +end + +class ForceSSLCustomOptions < ForceSSLController + ActiveSupport::Deprecation.silence do + force_ssl host: "secure.example.com", only: :redirect_host + force_ssl port: 8443, only: :redirect_port + force_ssl subdomain: "secure", only: :redirect_subdomain + force_ssl domain: "secure.com", only: :redirect_domain + force_ssl path: "/foo", only: :redirect_path + force_ssl status: :found, only: :redirect_status + force_ssl flash: { message: "Foo, Bar!" }, only: :redirect_flash + force_ssl alert: "Foo, Bar!", only: :redirect_alert + force_ssl notice: "Foo, Bar!", only: :redirect_notice + end + + def force_ssl_action + render plain: action_name + end + + alias_method :redirect_host, :force_ssl_action + alias_method :redirect_port, :force_ssl_action + alias_method :redirect_subdomain, :force_ssl_action + alias_method :redirect_domain, :force_ssl_action + alias_method :redirect_path, :force_ssl_action + alias_method :redirect_status, :force_ssl_action + alias_method :redirect_flash, :force_ssl_action + alias_method :redirect_alert, :force_ssl_action + alias_method :redirect_notice, :force_ssl_action + + def use_flash + render plain: flash[:message] + end + + def use_alert + render plain: flash[:alert] + end + + def use_notice + render plain: flash[:notice] + end +end + +class ForceSSLOnlyAction < ForceSSLController + ActiveSupport::Deprecation.silence do + force_ssl only: :cheeseburger + end +end + +class ForceSSLExceptAction < ForceSSLController + ActiveSupport::Deprecation.silence do + force_ssl except: :banana + end +end + +class ForceSSLIfCondition < ForceSSLController + ActiveSupport::Deprecation.silence do + force_ssl if: :use_force_ssl? + end + + def use_force_ssl? + action_name == "cheeseburger" + end +end + +class ForceSSLFlash < ForceSSLController + ActiveSupport::Deprecation.silence do + force_ssl except: [:banana, :set_flash, :use_flash] + end + + def set_flash + flash["that"] = "hello" + redirect_to "/force_ssl_flash/cheeseburger" + end + + def use_flash + @flash_copy = {}.update flash + @flashy = flash["that"] + render inline: "hello" + end +end + +class RedirectToSSL < ForceSSLController + def banana + force_ssl_redirect || render(plain: "monkey") + end + def cheeseburger + force_ssl_redirect("secure.cheeseburger.host") || render(plain: "ihaz") + end +end + +class ForceSSLControllerLevelTest < ActionController::TestCase + def test_banana_redirects_to_https + get :banana + assert_response 301 + assert_equal "https://test.host/force_ssl_controller_level/banana", redirect_to_url + end + + def test_banana_redirects_to_https_with_extra_params + get :banana, params: { token: "secret" } + assert_response 301 + assert_equal "https://test.host/force_ssl_controller_level/banana?token=secret", redirect_to_url + end + + def test_cheeseburger_redirects_to_https + get :cheeseburger + assert_response 301 + assert_equal "https://test.host/force_ssl_controller_level/cheeseburger", redirect_to_url + end +end + +class ForceSSLCustomOptionsTest < ActionController::TestCase + def setup + @request.env["HTTP_HOST"] = "www.example.com:80" + end + + def test_redirect_to_custom_host + get :redirect_host + assert_response 301 + assert_equal "https://secure.example.com/force_ssl_custom_options/redirect_host", redirect_to_url + end + + def test_redirect_to_custom_port + get :redirect_port + assert_response 301 + assert_equal "https://www.example.com:8443/force_ssl_custom_options/redirect_port", redirect_to_url + end + + def test_redirect_to_custom_subdomain + get :redirect_subdomain + assert_response 301 + assert_equal "https://secure.example.com/force_ssl_custom_options/redirect_subdomain", redirect_to_url + end + + def test_redirect_to_custom_domain + get :redirect_domain + assert_response 301 + assert_equal "https://www.secure.com/force_ssl_custom_options/redirect_domain", redirect_to_url + end + + def test_redirect_to_custom_path + get :redirect_path + assert_response 301 + assert_equal "https://www.example.com/foo", redirect_to_url + end + + def test_redirect_to_custom_status + get :redirect_status + assert_response 302 + assert_equal "https://www.example.com/force_ssl_custom_options/redirect_status", redirect_to_url + end + + def test_redirect_to_custom_flash + get :redirect_flash + assert_response 301 + assert_equal "https://www.example.com/force_ssl_custom_options/redirect_flash", redirect_to_url + + get :use_flash + assert_response 200 + assert_equal "Foo, Bar!", @response.body + end + + def test_redirect_to_custom_alert + get :redirect_alert + assert_response 301 + assert_equal "https://www.example.com/force_ssl_custom_options/redirect_alert", redirect_to_url + + get :use_alert + assert_response 200 + assert_equal "Foo, Bar!", @response.body + end + + def test_redirect_to_custom_notice + get :redirect_notice + assert_response 301 + assert_equal "https://www.example.com/force_ssl_custom_options/redirect_notice", redirect_to_url + + get :use_notice + assert_response 200 + assert_equal "Foo, Bar!", @response.body + end +end + +class ForceSSLOnlyActionTest < ActionController::TestCase + def test_banana_not_redirects_to_https + get :banana + assert_response 200 + end + + def test_cheeseburger_redirects_to_https + get :cheeseburger + assert_response 301 + assert_equal "https://test.host/force_ssl_only_action/cheeseburger", redirect_to_url + end +end + +class ForceSSLExceptActionTest < ActionController::TestCase + def test_banana_not_redirects_to_https + get :banana + assert_response 200 + end + + def test_cheeseburger_redirects_to_https + get :cheeseburger + assert_response 301 + assert_equal "https://test.host/force_ssl_except_action/cheeseburger", redirect_to_url + end +end + +class ForceSSLIfConditionTest < ActionController::TestCase + def test_banana_not_redirects_to_https + get :banana + assert_response 200 + end + + def test_cheeseburger_redirects_to_https + get :cheeseburger + assert_response 301 + assert_equal "https://test.host/force_ssl_if_condition/cheeseburger", redirect_to_url + end +end + +class ForceSSLFlashTest < ActionController::TestCase + def test_cheeseburger_redirects_to_https + get :set_flash + assert_response 302 + assert_equal "http://test.host/force_ssl_flash/cheeseburger", redirect_to_url + + @request.env.delete("PATH_INFO") + + get :cheeseburger + assert_response 301 + assert_equal "https://test.host/force_ssl_flash/cheeseburger", redirect_to_url + + @request.env.delete("PATH_INFO") + + get :use_flash + assert_equal "hello", @controller.instance_variable_get("@flash_copy")["that"] + assert_equal "hello", @controller.instance_variable_get("@flashy") + end +end + +class ForceSSLDuplicateRoutesTest < ActionController::TestCase + tests ForceSSLControllerLevel + + def test_force_ssl_redirects_to_same_path + with_routing do |set| + set.draw do + get "/foo", to: "force_ssl_controller_level#banana" + get "/bar", to: "force_ssl_controller_level#banana" + end + + @request.env["PATH_INFO"] = "/bar" + + get :banana + assert_response 301 + assert_equal "https://test.host/bar", redirect_to_url + end + end +end + +class ForceSSLFormatTest < ActionController::TestCase + tests ForceSSLControllerLevel + + def test_force_ssl_redirects_to_same_format + with_routing do |set| + set.draw do + get "/foo", to: "force_ssl_controller_level#banana" + end + + get :banana, format: :json + assert_response 301 + assert_equal "https://test.host/foo.json", redirect_to_url + end + end +end + +class ForceSSLOptionalSegmentsTest < ActionController::TestCase + tests ForceSSLControllerLevel + + def test_force_ssl_redirects_to_same_format + with_routing do |set| + set.draw do + scope "(:locale)" do + defaults locale: "en" do + get "/foo", to: "force_ssl_controller_level#banana" + end + end + end + + @request.env["PATH_INFO"] = "/en/foo" + get :banana, params: { locale: "en" } + assert_equal "en", @controller.params[:locale] + assert_response 301 + assert_equal "https://test.host/en/foo", redirect_to_url + end + end +end + +class RedirectToSSLTest < ActionController::TestCase + def test_banana_redirects_to_https_if_not_https + get :banana + assert_response 301 + assert_equal "https://test.host/redirect_to_ssl/banana", redirect_to_url + end + + def test_cheeseburgers_redirects_to_https_with_new_host_if_not_https + get :cheeseburger + assert_response 301 + assert_equal "https://secure.cheeseburger.host/redirect_to_ssl/cheeseburger", redirect_to_url + end + + def test_cheeseburgers_does_not_redirect_if_already_https + request.env["HTTPS"] = "on" + get :cheeseburger + assert_response 200 + assert_equal "ihaz", response.body + end +end + +class ForceSSLControllerLevelTest < ActionController::TestCase + def test_no_redirect_websocket_ssl_request + request.env["rack.url_scheme"] = "wss" + request.env["Upgrade"] = "websocket" + get :cheeseburger + assert_response 200 + end +end diff --git a/actionpack/test/controller/form_builder_test.rb b/actionpack/test/controller/form_builder_test.rb new file mode 100644 index 0000000000..2db0834c5e --- /dev/null +++ b/actionpack/test/controller/form_builder_test.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class FormBuilderController < ActionController::Base + class SpecializedFormBuilder < ActionView::Helpers::FormBuilder ; end + + default_form_builder SpecializedFormBuilder +end + +class ControllerFormBuilderTest < ActiveSupport::TestCase + setup do + @controller = FormBuilderController.new + end + + def test_default_form_builder_assigned + assert_equal FormBuilderController::SpecializedFormBuilder, @controller.default_form_builder + end +end diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb new file mode 100644 index 0000000000..de8072a994 --- /dev/null +++ b/actionpack/test/controller/helper_test.rb @@ -0,0 +1,295 @@ +# frozen_string_literal: true + +require "abstract_unit" + +ActionController::Base.helpers_path = File.expand_path("../fixtures/helpers", __dir__) + +module Fun + class GamesController < ActionController::Base + def render_hello_world + render inline: "hello: <%= stratego %>" + end + end + + class PdfController < ActionController::Base + def test + render inline: "test: <%= foobar %>" + end + end +end + +class AllHelpersController < ActionController::Base + helper :all +end + +module ImpressiveLibrary + extend ActiveSupport::Concern + included do + helper_method :useful_function + end + + def useful_function() end +end + +ActionController::Base.include(ImpressiveLibrary) + +class JustMeController < ActionController::Base + clear_helpers + + def flash + render inline: "<h1><%= notice %></h1>" + end + + def lib + render inline: "<%= useful_function %>" + end +end + +class MeTooController < JustMeController +end + +class HelpersPathsController < ActionController::Base + paths = ["helpers2_pack", "helpers1_pack"].map do |path| + File.join(File.expand_path("../fixtures", __dir__), path) + end + $:.unshift(*paths) + + self.helpers_path = paths + helper :all + + def index + render inline: "<%= conflicting_helper %>" + end +end + +class HelpersTypoController < ActionController::Base + path = File.expand_path("../fixtures/helpers_typo", __dir__) + $:.unshift(path) + self.helpers_path = path +end + +module LocalAbcHelper + def a() end + def b() end + def c() end +end + +class HelperPathsTest < ActiveSupport::TestCase + def test_helpers_paths_priority + responses = HelpersPathsController.action(:index).call(ActionController::TestRequest::DEFAULT_ENV.dup) + + # helpers1_pack was given as a second path, so pack1_helper should be + # included as the second one + assert_equal "pack1", responses.last.body + end +end + +class HelpersTypoControllerTest < ActiveSupport::TestCase + def setup + @autoload_paths = ActiveSupport::Dependencies.autoload_paths + ActiveSupport::Dependencies.autoload_paths = Array(HelpersTypoController.helpers_path) + end + + def test_helper_typo_error_message + e = assert_raise(NameError) { HelpersTypoController.helper "admin/users" } + assert_equal "Couldn't find Admin::UsersHelper, expected it to be defined in helpers/admin/users_helper.rb", e.message + end + + def teardown + ActiveSupport::Dependencies.autoload_paths = @autoload_paths + end +end + +class HelperTest < ActiveSupport::TestCase + class TestController < ActionController::Base + attr_accessor :delegate_attr + def delegate_method() end + end + + def setup + # Increment symbol counter. + @symbol = (@@counter ||= "A0").succ.dup + + # Generate new controller class. + controller_class_name = "Helper#{@symbol}Controller" + eval("class #{controller_class_name} < TestController; end") + @controller_class = self.class.const_get(controller_class_name) + + # Set default test helper. + self.test_helper = LocalAbcHelper + end + + def test_helper + assert_equal expected_helper_methods, missing_methods + assert_nothing_raised { @controller_class.helper TestHelper } + assert_equal [], missing_methods + end + + def test_helper_method + assert_nothing_raised { @controller_class.helper_method :delegate_method } + assert_includes master_helper_methods, :delegate_method + end + + def test_helper_attr + assert_nothing_raised { @controller_class.helper_attr :delegate_attr } + assert_includes master_helper_methods, :delegate_attr + assert_includes master_helper_methods, :delegate_attr= + end + + def call_controller(klass, action) + klass.action(action).call(ActionController::TestRequest::DEFAULT_ENV.dup) + end + + def test_helper_for_nested_controller + assert_equal "hello: Iz guuut!", + call_controller(Fun::GamesController, "render_hello_world").last.body + end + + def test_helper_for_acronym_controller + assert_equal "test: baz", call_controller(Fun::PdfController, "test").last.body + end + + def test_default_helpers_only + assert_equal [JustMeHelper], JustMeController._helpers.ancestors.reject(&:anonymous?) + assert_equal [MeTooHelper, JustMeHelper], MeTooController._helpers.ancestors.reject(&:anonymous?) + end + + def test_base_helper_methods_after_clear_helpers + assert_nothing_raised do + call_controller(JustMeController, "flash") + end + end + + def test_lib_helper_methods_after_clear_helpers + assert_nothing_raised do + call_controller(JustMeController, "lib") + end + end + + def test_all_helpers + methods = AllHelpersController._helpers.instance_methods + + # abc_helper.rb + assert_includes methods, :bare_a + + # fun/games_helper.rb + assert_includes methods, :stratego + + # fun/pdf_helper.rb + assert_includes methods, :foobar + end + + def test_all_helpers_with_alternate_helper_dir + @controller_class.helpers_path = File.expand_path("../fixtures/alternate_helpers", __dir__) + + # Reload helpers + @controller_class._helpers = Module.new + @controller_class.helper :all + + # helpers/abc_helper.rb should not be included + assert_not_includes master_helper_methods, :bare_a + + # alternate_helpers/foo_helper.rb + assert_includes master_helper_methods, :baz + end + + def test_helper_proxy + methods = AllHelpersController.helpers.methods + + # Action View + assert_includes methods, :pluralize + + # abc_helper.rb + assert_includes methods, :bare_a + + # fun/games_helper.rb + assert_includes methods, :stratego + + # fun/pdf_helper.rb + assert_includes methods, :foobar + end + + def test_helper_proxy_in_instance + methods = AllHelpersController.new.helpers.methods + + # Action View + assert_includes methods, :pluralize + + # abc_helper.rb + assert_includes methods, :bare_a + + # fun/games_helper.rb + assert_includes methods, :stratego + + # fun/pdf_helper.rb + assert_includes methods, :foobar + end + + def test_helper_proxy_config + AllHelpersController.config.my_var = "smth" + + assert_equal "smth", AllHelpersController.helpers.config.my_var + end + + private + def expected_helper_methods + TestHelper.instance_methods + end + + def master_helper_methods + @controller_class._helpers.instance_methods + end + + def missing_methods + expected_helper_methods - master_helper_methods + end + + def test_helper=(helper_module) + silence_warnings { self.class.const_set("TestHelper", helper_module) } + end +end + +class IsolatedHelpersTest < ActionController::TestCase + class A < ActionController::Base + def index + render inline: "<%= shout %>" + end + end + + class B < A + helper { def shout; "B" end } + + def index + render inline: "<%= shout %>" + end + end + + class C < A + helper { def shout; "C" end } + + def index + render inline: "<%= shout %>" + end + end + + def call_controller(klass, action) + klass.action(action).call(@request.env) + end + + def setup + super + @request.action = "index" + end + + def test_helper_in_a + assert_raise(ActionView::Template::Error) { call_controller(A, "index") } + end + + def test_helper_in_b + assert_equal "B", call_controller(B, "index").last.body + end + + def test_helper_in_c + assert_equal "C", call_controller(C, "index").last.body + end +end diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb new file mode 100644 index 0000000000..1544a627ee --- /dev/null +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class HttpBasicAuthenticationTest < ActionController::TestCase + class DummyController < ActionController::Base + before_action :authenticate, only: :index + before_action :authenticate_with_request, only: :display + before_action :authenticate_long_credentials, only: :show + before_action :auth_with_special_chars, only: :special_creds + + http_basic_authenticate_with name: "David", password: "Goliath", only: :search + + def index + render plain: "Hello Secret" + end + + def display + render plain: "Definitely Maybe" if @logged_in + end + + def show + render plain: "Only for loooooong credentials" + end + + def special_creds + render plain: "Only for special credentials" + end + + def search + render plain: "All inline" + end + + private + + def authenticate + authenticate_or_request_with_http_basic do |username, password| + username == "lifo" && password == "world" + end + end + + def authenticate_with_request + if authenticate_with_http_basic { |username, password| username == "pretty" && password == "please" } + @logged_in = true + else + request_http_basic_authentication("SuperSecret", "Authentication Failed\n") + end + end + + def auth_with_special_chars + authenticate_or_request_with_http_basic do |username, password| + username == 'login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' && password == 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' + end + end + + def authenticate_long_credentials + authenticate_or_request_with_http_basic do |username, password| + username == "1234567890123456789012345678901234567890" && password == "1234567890123456789012345678901234567890" + end + end + end + + AUTH_HEADERS = ["HTTP_AUTHORIZATION", "X-HTTP_AUTHORIZATION", "X_HTTP_AUTHORIZATION", "REDIRECT_X_HTTP_AUTHORIZATION"] + + tests DummyController + + AUTH_HEADERS.each do |header| + test "successful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials("lifo", "world") + get :index + + assert_response :success + assert_equal "Hello Secret", @response.body, "Authentication failed for request header #{header}" + end + test "successful authentication with #{header.downcase} and long credentials" do + @request.env[header] = encode_credentials("1234567890123456789012345678901234567890", "1234567890123456789012345678901234567890") + get :show + + assert_response :success + assert_equal "Only for loooooong credentials", @response.body, "Authentication failed for request header #{header} and long credentials" + end + end + + AUTH_HEADERS.each do |header| + test "unsuccessful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials("h4x0r", "world") + get :index + + assert_response :unauthorized + assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}" + end + test "unsuccessful authentication with #{header.downcase} and long credentials" do + @request.env[header] = encode_credentials("h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r", "worldworldworldworldworldworldworldworld") + get :show + + assert_response :unauthorized + assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and long credentials" + end + + test "unsuccessful authentication with #{header.downcase} and no credentials" do + get :show + + assert_response :unauthorized + assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and no credentials" + end + end + + def test_encode_credentials_has_no_newline + username = "laskjdfhalksdjfhalkjdsfhalksdjfhklsdjhalksdjfhalksdjfhlakdsjfh" + password = "kjfhueyt9485osdfasdkljfh4lkjhakldjfhalkdsjf" + result = ActionController::HttpAuthentication::Basic.encode_credentials( + username, password) + assert_no_match(/\n/, result) + end + + test "successful authentication with uppercase authorization scheme" do + @request.env["HTTP_AUTHORIZATION"] = "BASIC #{::Base64.encode64("lifo:world")}" + get :index + + assert_response :success + assert_equal "Hello Secret", @response.body, "Authentication failed when authorization scheme BASIC" + end + + test "authentication request without credential" do + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed\n", @response.body + assert_equal 'Basic realm="SuperSecret"', @response.headers["WWW-Authenticate"] + end + + test "authentication request with invalid credential" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials("pretty", "foo") + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed\n", @response.body + assert_equal 'Basic realm="SuperSecret"', @response.headers["WWW-Authenticate"] + end + + test "authentication request with valid credential" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials("pretty", "please") + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "authentication request with valid credential special chars" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials('login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t', 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t') + get :special_creds + + assert_response :success + assert_equal "Only for special credentials", @response.body + end + + test "authenticate with class method" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials("David", "Goliath") + get :search + assert_response :success + + @request.env["HTTP_AUTHORIZATION"] = encode_credentials("David", "WRONG!") + get :search + assert_response :unauthorized + end + + test "authentication request with wrong scheme" do + header = "Bearer " + encode_credentials("David", "Goliath").split(" ", 2)[1] + @request.env["HTTP_AUTHORIZATION"] = header + get :search + assert_response :unauthorized + end + + private + + def encode_credentials(username, password) + "Basic #{::Base64.encode64("#{username}:#{password}")}" + end +end diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb new file mode 100644 index 0000000000..b133afb343 --- /dev/null +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -0,0 +1,283 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/key_generator" + +class HttpDigestAuthenticationTest < ActionController::TestCase + class DummyDigestController < ActionController::Base + before_action :authenticate, only: :index + before_action :authenticate_with_request, only: :display + + USERS = { "lifo" => "world", "pretty" => "please", + "dhh" => ::Digest::MD5.hexdigest(["dhh", "SuperSecret", "secret"].join(":")) } + + def index + render plain: "Hello Secret" + end + + def display + render plain: "Definitely Maybe" if @logged_in + end + + private + + def authenticate + authenticate_or_request_with_http_digest("SuperSecret") do |username| + # Returns the password + USERS[username] + end + end + + def authenticate_with_request + if authenticate_with_http_digest("SuperSecret") { |username| USERS[username] } + @logged_in = true + else + request_http_digest_authentication("SuperSecret", "Authentication Failed") + end + end + end + + AUTH_HEADERS = ["HTTP_AUTHORIZATION", "X-HTTP_AUTHORIZATION", "X_HTTP_AUTHORIZATION", "REDIRECT_X_HTTP_AUTHORIZATION"] + + tests DummyDigestController + + setup do + # Used as secret in generating nonce to prevent tampering of timestamp + @secret = "4fb45da9e4ab4ddeb7580d6a35503d99" + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(@secret) + end + + teardown do + # ActionController::Base.session_options[:secret] = @old_secret + end + + AUTH_HEADERS.each do |header| + test "successful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials(username: "lifo", password: "world") + get :index + + assert_response :success + assert_equal "Hello Secret", @response.body, "Authentication failed for request header #{header}" + end + end + + AUTH_HEADERS.each do |header| + test "unsuccessful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials(username: "h4x0r", password: "world") + get :index + + assert_response :unauthorized + assert_equal "HTTP Digest: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}" + end + end + + test "authentication request without credential" do + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed", @response.body + credentials = decode_credentials(@response.headers["WWW-Authenticate"]) + assert_equal "SuperSecret", credentials[:realm] + end + + test "authentication request with nil credentials" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: nil, password: nil) + get :index + + assert_response :unauthorized + assert_equal "HTTP Digest: Access denied.\n", @response.body, "Authentication didn't fail for request" + assert_not_equal "Hello Secret", @response.body, "Authentication didn't fail for request" + end + + test "authentication request with invalid password" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "foo") + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed", @response.body + end + + test "authentication request with invalid nonce" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please", nonce: "xxyyzz") + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed", @response.body + end + + test "authentication request with invalid opaque" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "foo", opaque: "xxyyzz") + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed", @response.body + end + + test "authentication request with invalid realm" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "foo", realm: "NotSecret") + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed", @response.body + end + + test "authentication request with valid credential" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please") + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "authentication request with valid credential and nil session" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please") + + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "authentication request with request-uri that doesn't match credentials digest-uri" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please") + @request.env["PATH_INFO"] = "/proxied/uri" + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "authentication request with absolute request uri (as in webrick)" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please") + @request.env["SERVER_NAME"] = "test.host" + @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest" + + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "authentication request with absolute uri in credentials (as in IE)" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(url: "http://test.host/http_digest_authentication_test/dummy_digest", + username: "pretty", password: "please") + + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "authentication request with absolute uri in both request and credentials (as in Webrick with IE)" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(url: "http://test.host/http_digest_authentication_test/dummy_digest", + username: "pretty", password: "please") + @request.env["SERVER_NAME"] = "test.host" + @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest" + + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "authentication request with password stored as ha1 digest hash" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials( + username: "dhh", + password: ::Digest::MD5.hexdigest(["dhh", "SuperSecret", "secret"].join(":")), + password_is_ha1: true) + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "authentication request with _method" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please", method: :post) + @request.env["rack.methodoverride.original_method"] = "POST" + put :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "validate_digest_response should fail with nil returning password_procedure" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: nil, password: nil) + assert_not ActionController::HttpAuthentication::Digest.validate_digest_response(@request, "SuperSecret") { nil } + end + + test "authentication request with request-uri ending in '/'" do + @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest/" + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please") + + # simulate normalizing PATH_INFO + @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest" + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "authentication request with request-uri ending in '?'" do + @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest/?" + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "pretty", password: "please") + + # simulate normalizing PATH_INFO + @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest" + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "authentication request with absolute uri in credentials (as in IE) ending with /" do + @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest/" + @request.env["HTTP_AUTHORIZATION"] = encode_credentials(uri: "http://test.host/http_digest_authentication_test/dummy_digest/", + username: "pretty", password: "please") + + # simulate normalizing PATH_INFO + @request.env["PATH_INFO"] = "/http_digest_authentication_test/dummy_digest" + get :display + + assert_response :success + assert_equal "Definitely Maybe", @response.body + end + + test "when sent a basic auth header, returns Unauthorized" do + @request.env["HTTP_AUTHORIZATION"] = "Basic Gwf2aXq8ZLF3Hxq=" + + get :display + + assert_response :unauthorized + end + + private + + def encode_credentials(options) + options.reverse_merge!(nc: "00000001", cnonce: "0a4f113b", password_is_ha1: false) + password = options.delete(:password) + + # Perform unauthenticated request to retrieve digest parameters to use on subsequent request + method = options.delete(:method) || "GET" + + case method.to_s.upcase + when "GET" + get :index + when "POST" + post :index + end + + assert_response :unauthorized + + credentials = decode_credentials(@response.headers["WWW-Authenticate"]) + credentials.merge!(options) + path_info = @request.env["PATH_INFO"].to_s + uri = options[:uri] || path_info + credentials[:uri] = uri + @request.env["ORIGINAL_FULLPATH"] = path_info + ActionController::HttpAuthentication::Digest.encode_credentials(method, credentials, password, options[:password_is_ha1]) + end + + def decode_credentials(header) + ActionController::HttpAuthentication::Digest.decode_credentials(header) + end +end diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb new file mode 100644 index 0000000000..103123f98c --- /dev/null +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class HttpTokenAuthenticationTest < ActionController::TestCase + class DummyController < ActionController::Base + before_action :authenticate, only: :index + before_action :authenticate_with_request, only: :display + before_action :authenticate_long_credentials, only: :show + + def index + render plain: "Hello Secret" + end + + def display + render plain: "Definitely Maybe" + end + + def show + render plain: "Only for loooooong credentials" + end + + private + + def authenticate + authenticate_or_request_with_http_token do |token, _| + token == "lifo" + end + end + + def authenticate_with_request + if authenticate_with_http_token { |token, options| token == '"quote" pretty' && options[:algorithm] == "test" } + @logged_in = true + else + request_http_token_authentication("SuperSecret", "Authentication Failed\n") + end + end + + def authenticate_long_credentials + authenticate_or_request_with_http_token do |token, options| + token == "1234567890123456789012345678901234567890" && options[:algorithm] == "test" + end + end + end + + AUTH_HEADERS = ["HTTP_AUTHORIZATION", "X-HTTP_AUTHORIZATION", "X_HTTP_AUTHORIZATION", "REDIRECT_X_HTTP_AUTHORIZATION"] + + tests DummyController + + AUTH_HEADERS.each do |header| + test "successful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials("lifo") + get :index + + assert_response :success + assert_equal "Hello Secret", @response.body, "Authentication failed for request header #{header}" + end + test "successful authentication with #{header.downcase} and long credentials" do + @request.env[header] = encode_credentials("1234567890123456789012345678901234567890", algorithm: "test") + get :show + + assert_response :success + assert_equal "Only for loooooong credentials", @response.body, "Authentication failed for request header #{header} and long credentials" + end + end + + AUTH_HEADERS.each do |header| + test "unsuccessful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials("h4x0r") + get :index + + assert_response :unauthorized + assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}" + end + test "unsuccessful authentication with #{header.downcase} and long credentials" do + @request.env[header] = encode_credentials("h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r") + get :show + + assert_response :unauthorized + assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and long credentials" + end + end + + test "authentication request with badly formatted header" do + @request.env["HTTP_AUTHORIZATION"] = 'Token token$"lifo"' + get :index + + assert_response :unauthorized + assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication header was not properly parsed" + end + + test "successful authentication request with Bearer instead of Token" do + @request.env["HTTP_AUTHORIZATION"] = "Bearer lifo" + get :index + + assert_response :success + end + + test "authentication request with tab in header" do + @request.env["HTTP_AUTHORIZATION"] = "Token\ttoken=\"lifo\"" + get :index + + assert_response :success + assert_equal "Hello Secret", @response.body + end + + test "authentication request without credential" do + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed\n", @response.body + assert_equal 'Token realm="SuperSecret"', @response.headers["WWW-Authenticate"] + end + + test "authentication request with invalid credential" do + @request.env["HTTP_AUTHORIZATION"] = encode_credentials('"quote" pretty') + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed\n", @response.body + assert_equal 'Token realm="SuperSecret"', @response.headers["WWW-Authenticate"] + end + + test "token_and_options returns correct token" do + token = "rcHu+HzSFw89Ypyhn/896A==" + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first + expected = token + assert_equal(expected, actual) + end + + test "token_and_options returns correct token with value after the equal sign" do + token = "rcHu+=HzSFw89Ypyhn/896A==f34" + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first + expected = token + assert_equal(expected, actual) + end + + test "token_and_options returns correct token with slashes" do + token = 'rcHu+\\\\"/896A' + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first + expected = token + assert_equal(expected, actual) + end + + test "token_and_options returns correct token with quotes" do + token = '\"quote\" pretty' + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first + expected = token + assert_equal(expected, actual) + end + + test "token_and_options returns empty string with empty token" do + token = +"" + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first + expected = token + assert_equal(expected, actual) + end + + test "token_and_options returns correct token with nounce option" do + token = "rcHu+HzSFw89Ypyhn/896A=" + nonce_hash = { nonce: "123abc" } + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token, nonce_hash)) + expected_token = token + expected_nonce = { "nonce" => nonce_hash[:nonce] } + assert_equal(expected_token, actual.first) + assert_equal(expected_nonce, actual.last) + end + + test "token_and_options returns nil with no value after the equal sign" do + actual = ActionController::HttpAuthentication::Token.token_and_options(malformed_request).first + assert_nil actual + end + + test "raw_params returns a tuple of two key value pair strings" do + auth = sample_request("rcHu+HzSFw89Ypyhn/896A=").authorization.to_s + actual = ActionController::HttpAuthentication::Token.raw_params(auth) + expected = ["token=\"rcHu+HzSFw89Ypyhn/896A=\"", "nonce=\"def\""] + assert_equal(expected, actual) + end + + test "token_and_options returns right token when token key is not specified in header" do + token = "rcHu+HzSFw89Ypyhn/896A=" + + actual = ActionController::HttpAuthentication::Token.token_and_options( + sample_request_without_token_key(token) + ).first + + expected = token + assert_equal(expected, actual) + end + + private + + def sample_request(token, options = { nonce: "def" }) + authorization = options.inject([%{Token token="#{token}"}]) do |arr, (k, v)| + arr << "#{k}=\"#{v}\"" + end.join(", ") + mock_authorization_request(authorization) + end + + def malformed_request + mock_authorization_request(%{Token token=}) + end + + def sample_request_without_token_key(token) + mock_authorization_request(%{Token #{token}}) + end + + def mock_authorization_request(authorization) + OpenStruct.new(authorization: authorization) + end + + def encode_credentials(token, options = {}) + ActionController::HttpAuthentication::Token.encode_credentials(token, options) + end +end diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb new file mode 100644 index 0000000000..b5503a9c64 --- /dev/null +++ b/actionpack/test/controller/integration_test.rb @@ -0,0 +1,1146 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_controllers" +require "rails/engine" + +class SessionTest < ActiveSupport::TestCase + StubApp = lambda { |env| + [200, { "Content-Type" => "text/html", "Content-Length" => "13" }, ["Hello, World!"]] + } + + def setup + @session = ActionDispatch::Integration::Session.new(StubApp) + end + + def test_https_bang_works_and_sets_truth_by_default + assert_not_predicate @session, :https? + @session.https! + assert_predicate @session, :https? + @session.https! false + assert_not_predicate @session, :https? + end + + def test_host! + assert_not_equal "glu.ttono.us", @session.host + @session.host! "rubyonrails.com" + assert_equal "rubyonrails.com", @session.host + end + + def test_follow_redirect_raises_when_no_redirect + @session.stub :redirect?, false do + assert_raise(RuntimeError) { @session.follow_redirect! } + end + end + + def test_get + path = "/index"; params = "blah"; headers = { location: "blah" } + + assert_called_with @session, :process, [:get, path, params: params, headers: headers] do + @session.get(path, params: params, headers: headers) + end + end + + def test_get_with_env_and_headers + path = "/index"; params = "blah"; headers = { location: "blah" }; env = { "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" } + assert_called_with @session, :process, [:get, path, params: params, headers: headers, env: env] do + @session.get(path, params: params, headers: headers, env: env) + end + end + + def test_post + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:post, path, params: params, headers: headers] do + @session.post(path, params: params, headers: headers) + end + end + + def test_patch + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:patch, path, params: params, headers: headers] do + @session.patch(path, params: params, headers: headers) + end + end + + def test_put + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:put, path, params: params, headers: headers] do + @session.put(path, params: params, headers: headers) + end + end + + def test_delete + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:delete, path, params: params, headers: headers] do + @session.delete(path, params: params, headers: headers) + end + end + + def test_head + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:head, path, params: params, headers: headers] do + @session.head(path, params: params, headers: headers) + end + end + + def test_xml_http_request_get + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:get, path, params: params, headers: headers, xhr: true] do + @session.get(path, params: params, headers: headers, xhr: true) + end + end + + def test_xml_http_request_post + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:post, path, params: params, headers: headers, xhr: true] do + @session.post(path, params: params, headers: headers, xhr: true) + end + end + + def test_xml_http_request_patch + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:patch, path, params: params, headers: headers, xhr: true] do + @session.patch(path, params: params, headers: headers, xhr: true) + end + end + + def test_xml_http_request_put + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:put, path, params: params, headers: headers, xhr: true] do + @session.put(path, params: params, headers: headers, xhr: true) + end + end + + def test_xml_http_request_delete + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do + @session.delete(path, params: params, headers: headers, xhr: true) + end + end + + def test_xml_http_request_head + path = "/index"; params = "blah"; headers = { location: "blah" } + assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do + @session.head(path, params: params, headers: headers, xhr: true) + end + end +end + +class IntegrationTestTest < ActiveSupport::TestCase + def setup + @test = ::ActionDispatch::IntegrationTest.new(:app) + end + + def test_opens_new_session + session1 = @test.open_session { |sess| } + session2 = @test.open_session # implicit session + + assert_not session1.equal?(session2) + end + + # RSpec mixes Matchers (which has a #method_missing) into + # IntegrationTest's superclass. Make sure IntegrationTest does not + # try to delegate these methods to the session object. + def test_does_not_prevent_method_missing_passing_up_to_ancestors + mixin = Module.new do + def method_missing(name, *args) + name.to_s == "foo" ? "pass" : super + end + end + @test.class.superclass.include(mixin) + begin + assert_equal "pass", @test.foo + ensure + # leave other tests as unaffected as possible + mixin.remove_method :method_missing + end + end +end + +# Tests that integration tests don't call Controller test methods for processing. +# Integration tests have their own setup and teardown. +class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest + def test_integration_methods_called + reset! + + %w( get post head patch put delete ).each do |verb| + assert_nothing_raised { __send__(verb, "/") } + end + end +end + +class IntegrationProcessTest < ActionDispatch::IntegrationTest + class IntegrationController < ActionController::Base + def get + respond_to do |format| + format.html { render plain: "OK", status: 200 } + format.js { render plain: "JS OK", status: 200 } + format.json { render json: "JSON OK", status: 200 } + format.xml { render xml: "<root></root>", status: 200 } + format.rss { render xml: "<root></root>", status: 200 } + format.atom { render xml: "<root></root>", status: 200 } + end + end + + def get_with_params + render plain: "foo: #{params[:foo]}", status: 200 + end + + def post + render plain: "Created", status: 201 + end + + def method + render plain: "method: #{request.method.downcase}" + end + + def cookie_monster + cookies["cookie_1"] = nil + cookies["cookie_3"] = "chocolate" + render plain: "Gone", status: 410 + end + + def set_cookie + cookies["foo"] = "bar" + head :ok + end + + def get_cookie + render plain: cookies["foo"] + end + + def redirect + redirect_to action_url("get") + end + + def remove_header + response.headers.delete params[:header] + head :ok, "c" => "3" + end + end + + def test_get + with_test_route_set do + get "/get" + assert_equal 200, status + assert_equal "OK", status_message + assert_response 200 + assert_response :success + assert_response :ok + assert_equal({}, cookies.to_hash) + assert_equal "OK", body + assert_equal "OK", response.body + assert_kind_of Nokogiri::HTML::Document, html_document + assert_equal 1, request_count + end + end + + def test_get_xml_rss_atom + %w[ application/xml application/rss+xml application/atom+xml ].each do |mime_string| + with_test_route_set do + get "/get", headers: { "HTTP_ACCEPT" => mime_string } + assert_equal 200, status + assert_equal "OK", status_message + assert_response 200 + assert_response :success + assert_response :ok + assert_equal({}, cookies.to_hash) + assert_equal "<root></root>", body + assert_equal "<root></root>", response.body + assert_instance_of Nokogiri::XML::Document, html_document + assert_equal 1, request_count + end + end + end + + def test_post + with_test_route_set do + post "/post" + assert_equal 201, status + assert_equal "Created", status_message + assert_response 201 + assert_response :success + assert_response :created + assert_equal({}, cookies.to_hash) + assert_equal "Created", body + assert_equal "Created", response.body + assert_kind_of Nokogiri::HTML::Document, html_document + assert_equal 1, request_count + end + end + + test "response cookies are added to the cookie jar for the next request" do + with_test_route_set do + cookies["cookie_1"] = "sugar" + cookies["cookie_2"] = "oatmeal" + get "/cookie_monster" + assert_equal "cookie_1=; path=/\ncookie_3=chocolate; path=/", headers["Set-Cookie"] + assert_equal({ "cookie_1" => "", "cookie_2" => "oatmeal", "cookie_3" => "chocolate" }, cookies.to_hash) + end + end + + test "cookie persist to next request" do + with_test_route_set do + get "/set_cookie" + assert_response :success + + assert_equal "foo=bar; path=/", headers["Set-Cookie"] + assert_equal({ "foo" => "bar" }, cookies.to_hash) + + get "/get_cookie" + assert_response :success + assert_equal "bar", body + + assert_nil headers["Set-Cookie"] + assert_equal({ "foo" => "bar" }, cookies.to_hash) + end + end + + test "cookie persist to next request on another domain" do + with_test_route_set do + host! "37s.backpack.test" + + get "/set_cookie" + assert_response :success + + assert_equal "foo=bar; path=/", headers["Set-Cookie"] + assert_equal({ "foo" => "bar" }, cookies.to_hash) + + get "/get_cookie" + assert_response :success + assert_equal "bar", body + + assert_nil headers["Set-Cookie"] + assert_equal({ "foo" => "bar" }, cookies.to_hash) + end + end + + def test_redirect + with_test_route_set do + get "/redirect" + assert_equal 302, status + assert_equal "Found", status_message + assert_response 302 + assert_response :redirect + assert_response :found + assert_equal "<html><body>You are being <a href=\"http://www.example.com/get\">redirected</a>.</body></html>", response.body + assert_kind_of Nokogiri::HTML::Document, html_document + assert_equal 1, request_count + + follow_redirect! + assert_response :success + assert_equal "/get", path + + get "/moved" + assert_response :redirect + assert_redirected_to "/method" + end + end + + def test_redirect_reset_html_document + with_test_route_set do + get "/redirect" + previous_html_document = html_document + + follow_redirect! + + assert_response :ok + assert_not_same previous_html_document, html_document + end + end + + def test_redirect_with_arguments + with_test_route_set do + get "/redirect" + follow_redirect! params: { foo: :bar } + + assert_response :ok + assert_equal "bar", request.parameters["foo"] + end + end + + def test_xml_http_request_get + with_test_route_set do + get "/get", xhr: true + assert_equal 200, status + assert_equal "OK", status_message + assert_response 200 + assert_response :success + assert_response :ok + assert_equal "JS OK", response.body + end + end + + def test_request_with_bad_format + with_test_route_set do + get "/get.php", xhr: true + assert_equal 406, status + assert_response 406 + assert_response :not_acceptable + end + end + + test "creation of multiple integration sessions" do + integration_session # initialize first session + a = open_session + b = open_session + + assert_not_same(a.integration_session, b.integration_session) + end + + def test_get_with_query_string + with_test_route_set do + get "/get_with_params?foo=bar" + assert_equal "/get_with_params?foo=bar", request.env["REQUEST_URI"] + assert_equal "/get_with_params?foo=bar", request.fullpath + assert_equal "foo=bar", request.env["QUERY_STRING"] + assert_equal "foo=bar", request.query_string + assert_equal "bar", request.parameters["foo"] + + assert_equal 200, status + assert_equal "foo: bar", response.body + end + end + + def test_get_with_parameters + with_test_route_set do + get "/get_with_params", params: { foo: "bar" } + assert_equal "/get_with_params", request.env["PATH_INFO"] + assert_equal "/get_with_params", request.path_info + assert_equal "foo=bar", request.env["QUERY_STRING"] + assert_equal "foo=bar", request.query_string + assert_equal "bar", request.parameters["foo"] + + assert_equal 200, status + assert_equal "foo: bar", response.body + end + end + + def test_post_then_get_with_parameters_do_not_leak_across_requests + with_test_route_set do + post "/post", params: { leaks: "does-leak?" } + + get "/get_with_params", params: { foo: "bar" } + + assert_empty request.env["rack.input"].string + assert_equal "foo=bar", request.env["QUERY_STRING"] + assert_equal "foo=bar", request.query_string + assert_equal "bar", request.parameters["foo"] + assert_predicate request.parameters["leaks"], :nil? + end + end + + def test_head + with_test_route_set do + head "/get" + assert_equal 200, status + assert_equal "", body + + head "/post" + assert_equal 201, status + assert_equal "", body + + get "/get/method" + assert_equal 200, status + assert_equal "method: get", body + + head "/get/method" + assert_equal 200, status + assert_equal "", body + end + end + + def test_generate_url_with_controller + assert_equal "http://www.example.com/foo", url_for(controller: "foo") + end + + def test_port_via_host! + with_test_route_set do + host! "www.example.com:8080" + get "/get" + assert_equal 8080, request.port + end + end + + def test_port_via_process + with_test_route_set do + get "http://www.example.com:8080/get" + assert_equal 8080, request.port + end + end + + def test_https_and_port_via_host_and_https! + with_test_route_set do + host! "www.example.com" + https! true + + get "/get" + assert_equal 443, request.port + assert_equal true, request.ssl? + + host! "www.example.com:443" + https! true + + get "/get" + assert_equal 443, request.port + assert_equal true, request.ssl? + + host! "www.example.com:8443" + https! true + + get "/get" + assert_equal 8443, request.port + assert_equal true, request.ssl? + end + end + + def test_https_and_port_via_process + with_test_route_set do + get "https://www.example.com/get" + assert_equal 443, request.port + assert_equal true, request.ssl? + + get "https://www.example.com:8443/get" + assert_equal 8443, request.port + assert_equal true, request.ssl? + end + end + + def test_respect_removal_of_default_headers_by_a_controller_action + with_test_route_set do + with_default_headers "a" => "1", "b" => "2" do + get "/remove_header", params: { header: "a" } + end + end + + assert_not_includes @response.headers, "a", "Response should not include default header removed by the controller action" + assert_includes @response.headers, "b" + assert_includes @response.headers, "c" + end + + def test_accept_not_overridden_when_xhr_true + with_test_route_set do + get "/get", headers: { "Accept" => "application/json" }, xhr: true + assert_equal "application/json", request.accept + assert_equal "application/json", response.content_type + + get "/get", headers: { "HTTP_ACCEPT" => "application/json" }, xhr: true + assert_equal "application/json", request.accept + assert_equal "application/json", response.content_type + end + end + + private + def with_default_headers(headers) + original = ActionDispatch::Response.default_headers + ActionDispatch::Response.default_headers = headers + yield + ensure + ActionDispatch::Response.default_headers = original + end + + def with_test_route_set + with_routing do |set| + controller = ::IntegrationProcessTest::IntegrationController.clone + controller.class_eval do + include set.url_helpers + end + + set.draw do + get "moved" => redirect("/method") + + ActiveSupport::Deprecation.silence do + match ":action", to: controller, via: [:get, :post], as: :action + get "get/:action", to: controller, as: :get_action + end + end + + singleton_class.include(set.url_helpers) + + yield + end + end +end + +class MetalIntegrationTest < ActionDispatch::IntegrationTest + include SharedTestRoutes.url_helpers + + class Poller + def self.call(env) + if env["PATH_INFO"] =~ /^\/success/ + [200, { "Content-Type" => "text/plain", "Content-Length" => "12" }, ["Hello World!"]] + else + [404, { "Content-Type" => "text/plain", "Content-Length" => "0" }, []] + end + end + end + + def setup + @app = Poller + end + + def test_successful_get + get "/success" + assert_response 200 + assert_response :success + assert_response :ok + assert_equal "Hello World!", response.body + end + + def test_failed_get + get "/failure" + assert_response 404 + assert_response :not_found + assert_equal "", response.body + end + + def test_generate_url_without_controller + assert_equal "http://www.example.com/foo", url_for(controller: "foo") + end + + def test_pass_headers + get "/success", headers: { "Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com" } + + assert_equal "http://nohost.com", @request.env["HTTP_HOST"] + assert_equal "http://www.example.com/foo", @request.env["HTTP_REFERER"] + end + + def test_pass_headers_and_env + get "/success", headers: { "X-Test-Header" => "value" }, env: { "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" } + + assert_equal "http://test.com", @request.env["HTTP_HOST"] + assert_equal "http://test.com/", @request.env["HTTP_REFERER"] + assert_equal "value", @request.env["HTTP_X_TEST_HEADER"] + end + + def test_pass_env + get "/success", env: { "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com" } + + assert_equal "http://test.com", @request.env["HTTP_HOST"] + assert_equal "http://test.com/", @request.env["HTTP_REFERER"] + end + + def test_ignores_common_ports_in_host + get "http://test.com" + assert_equal "test.com", @request.env["HTTP_HOST"] + + get "https://test.com" + assert_equal "test.com", @request.env["HTTP_HOST"] + end + + def test_keeps_uncommon_ports_in_host + get "http://test.com:123" + assert_equal "test.com:123", @request.env["HTTP_HOST"] + + get "http://test.com:443" + assert_equal "test.com:443", @request.env["HTTP_HOST"] + + get "https://test.com:80" + assert_equal "test.com:80", @request.env["HTTP_HOST"] + end +end + +class ApplicationIntegrationTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + def index + render plain: "index" + end + end + + def self.call(env) + routes.call(env) + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + class MountedApp < Rails::Engine + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + routes.draw do + get "baz", to: "application_integration_test/test#index", as: :baz + end + + def self.call(*) + end + end + + routes.draw do + get "", to: "application_integration_test/test#index", as: :empty_string + + get "foo", to: "application_integration_test/test#index", as: :foo + get "bar", to: "application_integration_test/test#index", as: :bar + + mount MountedApp => "/mounted", :as => "mounted" + get "fooz" => proc { |env| [ 200, { "X-Cascade" => "pass" }, [ "omg" ] ] }, :anchor => false + get "fooz", to: "application_integration_test/test#index" + end + + def app + self.class + end + + test "includes route helpers" do + assert_equal "/", empty_string_path + assert_equal "/foo", foo_path + assert_equal "/bar", bar_path + end + + test "includes mounted helpers" do + assert_equal "/mounted/baz", mounted.baz_path + end + + test "path after cascade pass" do + get "/fooz" + assert_equal "index", response.body + assert_equal "/fooz", path + end + + test "route helpers after controller access" do + get "/" + assert_equal "/", empty_string_path + + get "/foo" + assert_equal "/foo", foo_path + + get "/bar" + assert_equal "/bar", bar_path + end + + test "missing route helper before controller access" do + assert_raise(NameError) { missing_path } + end + + test "missing route helper after controller access" do + get "/foo" + assert_raise(NameError) { missing_path } + end + + test "process do not modify the env passed as argument" do + env = { :SERVER_NAME => "server", "action_dispatch.custom" => "custom" } + old_env = env.dup + get "/foo", env: env + assert_equal old_env, env + end +end + +class EnvironmentFilterIntegrationTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + def post + render plain: "Created", status: 201 + end + end + + def self.call(env) + env["action_dispatch.parameter_filter"] = [:password] + routes.call(env) + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + routes.draw do + match "/post", to: "environment_filter_integration_test/test#post", via: :post + end + + def app + self.class + end + + test "filters rack request form vars" do + post "/post", params: { username: "cjolly", password: "secret" } + + assert_equal "cjolly", request.filtered_parameters["username"] + assert_equal "[FILTERED]", request.filtered_parameters["password"] + assert_equal "[FILTERED]", request.filtered_env["rack.request.form_vars"] + end +end + +class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def index + render plain: "foo#index" + end + + def show + render plain: "foo#show" + end + + def edit + render plain: "foo#show" + end + end + + class BarController < ActionController::Base + def default_url_options + { host: "bar.com" } + end + + def index + render plain: "foo#index" + end + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + def self.call(env) + routes.call(env) + end + + def app + self.class + end + + routes.draw do + default_url_options host: "foo.com" + + scope module: "url_options_integration_test" do + get "/foo" => "foo#index", :as => :foos + get "/foo/:id" => "foo#show", :as => :foo + get "/foo/:id/edit" => "foo#edit", :as => :edit_foo + get "/bar" => "bar#index", :as => :bars + end + end + + test "session uses default url options from routes" do + assert_equal "http://foo.com/foo", foos_url + end + + test "current host overrides default url options from routes" do + get "/foo" + assert_response :success + assert_equal "http://www.example.com/foo", foos_url + end + + test "controller can override default url options from request" do + get "/bar" + assert_response :success + assert_equal "http://bar.com/foo", foos_url + end + + def test_can_override_default_url_options + original_host = default_url_options.dup + + default_url_options[:host] = "foobar.com" + assert_equal "http://foobar.com/foo", foos_url + + get "/bar" + assert_response :success + assert_equal "http://foobar.com/foo", foos_url + ensure + ActionDispatch::Integration::Session.default_url_options = self.default_url_options = original_host + end + + test "current request path parameters are recalled" do + get "/foo/1" + assert_response :success + assert_equal "/foo/1/edit", url_for(action: "edit", only_path: true) + end +end + +class HeadWithStatusActionIntegrationTest < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def status + head :ok + end + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + def self.call(env) + routes.call(env) + end + + def app + self.class + end + + routes.draw do + get "/foo/status" => "head_with_status_action_integration_test/foo#status" + end + + test "get /foo/status with head result does not cause stack overflow error" do + assert_nothing_raised do + get "/foo/status" + end + assert_response :ok + end +end + +class IntegrationWithRoutingTest < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def index + render plain: "ok" + end + end + + def test_with_routing_resets_session + klass_namespace = self.class.name.underscore + + with_routing do |routes| + routes.draw do + namespace klass_namespace do + resources :foo, path: "/with" + end + end + + get "/integration_with_routing_test/with" + assert_response 200 + assert_equal "ok", response.body + end + + with_routing do |routes| + routes.draw do + namespace klass_namespace do + resources :foo, path: "/routing" + end + end + + get "/integration_with_routing_test/routing" + assert_response 200 + assert_equal "ok", response.body + end + end +end + +# to work in contexts like rspec before(:all) +class IntegrationRequestsWithoutSetup < ActionDispatch::IntegrationTest + self._setup_callbacks = [] + self._teardown_callbacks = [] + + class FooController < ActionController::Base + def ok + cookies[:key] = "ok" + render plain: "ok" + end + end + + def test_request + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + get ":action" => FooController + end + end + + get "/ok" + + assert_response 200 + assert_equal "ok", response.body + assert_equal "ok", cookies["key"] + end + end +end + +# to ensure that session requirements in setup are persisted in the tests +class IntegrationRequestsWithSessionSetup < ActionDispatch::IntegrationTest + setup do + cookies["user_name"] = "david" + end + + def test_cookies_set_in_setup_are_persisted_through_the_session + get "/foo" + assert_equal({ "user_name" => "david" }, cookies.to_hash) + end +end + +class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def foos + render plain: "ok" + end + + def foos_json + render json: params.permit(:foo) + end + + def foos_wibble + render plain: "ok" + end + end + + def test_standard_json_encoding_works + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + post ":action" => FooController + end + end + + post "/foos_json.json", params: { foo: "fighters" }.to_json, + headers: { "Content-Type" => "application/json" } + + assert_response :success + assert_equal({ "foo" => "fighters" }, response.parsed_body) + end + end + + def test_encoding_as_json + post_to_foos as: :json do + assert_response :success + assert_equal "application/json", request.content_type + assert_equal "application/json", request.accepts.first.to_s + assert_equal :json, request.format.ref + assert_equal({ "foo" => "fighters" }, request.request_parameters) + assert_equal({ "foo" => "fighters" }, response.parsed_body) + end + end + + def test_doesnt_mangle_request_path + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + post ":action" => FooController + end + end + + post "/foos" + assert_equal "/foos", request.path + + post "/foos_json", as: :json + assert_equal "/foos_json", request.path + end + end + + def test_encoding_as_without_mime_registration + assert_raise ArgumentError do + ActionDispatch::IntegrationTest.register_encoder :wibble + end + end + + def test_registering_custom_encoder + Mime::Type.register "text/wibble", :wibble + + ActionDispatch::IntegrationTest.register_encoder(:wibble, + param_encoder: -> params { params }) + + post_to_foos as: :wibble do + assert_response :success + assert_equal "/foos_wibble", request.path + assert_equal "text/wibble", request.content_type + assert_equal "text/wibble", request.accepts.first.to_s + assert_equal :wibble, request.format.ref + assert_equal Hash.new, request.request_parameters # Unregistered MIME Type can't be parsed. + assert_equal "ok", response.parsed_body + end + ensure + Mime::Type.unregister :wibble + end + + def test_parsed_body_without_as_option + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + get ":action" => FooController + end + end + + get "/foos_json.json", params: { foo: "heyo" } + + assert_equal({ "foo" => "heyo" }, response.parsed_body) + end + end + + def test_get_parameters_with_as_option + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + get ":action" => FooController + end + end + + get "/foos_json?foo=heyo", as: :json + + assert_equal({ "foo" => "heyo" }, response.parsed_body) + end + end + + def test_get_request_with_json_uses_method_override_and_sends_a_post_request + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + get ":action" => FooController + end + end + + get "/foos_json", params: { foo: "heyo" }, as: :json + + assert_equal "POST", request.method + assert_equal "GET", request.headers["X-Http-Method-Override"] + assert_equal({ "foo" => "heyo" }, response.parsed_body) + end + end + + def test_get_request_with_json_excludes_null_query_string + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + get ":action" => FooController + end + end + + get "/foos_json", as: :json + + assert_equal "http://www.example.com/foos_json", request.url + end + end + + private + def post_to_foos(as:) + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + post ":action" => FooController + end + end + + post "/foos_#{as}", params: { foo: "fighters" }, as: as + + yield + end + end +end + +class IntegrationFileUploadTest < ActionDispatch::IntegrationTest + class IntegrationController < ActionController::Base + def test_file_upload + render plain: params[:file].size + end + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + def self.call(env) + routes.call(env) + end + + def app + self.class + end + + def self.fixture_path + File.expand_path("../fixtures/multipart", __dir__) + end + + routes.draw do + post "test_file_upload", to: "integration_file_upload_test/integration#test_file_upload" + end + + def test_fixture_file_upload + post "/test_file_upload", + params: { + file: fixture_file_upload("/ruby_on_rails.jpg", "image/jpg") + } + assert_equal "45142", @response.body + end +end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb new file mode 100644 index 0000000000..d81c43b87d --- /dev/null +++ b/actionpack/test/controller/live_stream_test.rb @@ -0,0 +1,518 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "timeout" +require "concurrent/atomic/count_down_latch" +Thread.abort_on_exception = true + +module ActionController + class SSETest < ActionController::TestCase + class SSETestController < ActionController::Base + include ActionController::Live + + def basic_sse + response.headers["Content-Type"] = "text/event-stream" + sse = SSE.new(response.stream) + sse.write("{\"name\":\"John\"}") + sse.write(name: "Ryan") + ensure + sse.close + end + + def sse_with_event + sse = SSE.new(response.stream, event: "send-name") + sse.write("{\"name\":\"John\"}") + sse.write(name: "Ryan") + ensure + sse.close + end + + def sse_with_retry + sse = SSE.new(response.stream, retry: 1000) + sse.write("{\"name\":\"John\"}") + sse.write({ name: "Ryan" }, { retry: 1500 }) + ensure + sse.close + end + + def sse_with_id + sse = SSE.new(response.stream) + sse.write("{\"name\":\"John\"}", id: 1) + sse.write({ name: "Ryan" }, { id: 2 }) + ensure + sse.close + end + + def sse_with_multiple_line_message + sse = SSE.new(response.stream) + sse.write("first line.\nsecond line.") + ensure + sse.close + end + end + + tests SSETestController + + def wait_for_response_stream_close + response.body + end + + def test_basic_sse + get :basic_sse + + wait_for_response_stream_close + assert_match(/data: {\"name\":\"John\"}/, response.body) + assert_match(/data: {\"name\":\"Ryan\"}/, response.body) + end + + def test_sse_with_event_name + get :sse_with_event + + wait_for_response_stream_close + assert_match(/data: {\"name\":\"John\"}/, response.body) + assert_match(/data: {\"name\":\"Ryan\"}/, response.body) + assert_match(/event: send-name/, response.body) + end + + def test_sse_with_retry + get :sse_with_retry + + wait_for_response_stream_close + first_response, second_response = response.body.split("\n\n") + assert_match(/data: {\"name\":\"John\"}/, first_response) + assert_match(/retry: 1000/, first_response) + + assert_match(/data: {\"name\":\"Ryan\"}/, second_response) + assert_match(/retry: 1500/, second_response) + end + + def test_sse_with_id + get :sse_with_id + + wait_for_response_stream_close + first_response, second_response = response.body.split("\n\n") + assert_match(/data: {\"name\":\"John\"}/, first_response) + assert_match(/id: 1/, first_response) + + assert_match(/data: {\"name\":\"Ryan\"}/, second_response) + assert_match(/id: 2/, second_response) + end + + def test_sse_with_multiple_line_message + get :sse_with_multiple_line_message + + wait_for_response_stream_close + first_response, second_response = response.body.split("\n") + assert_match(/data: first line/, first_response) + assert_match(/data: second line/, second_response) + end + end + + class LiveStreamTest < ActionController::TestCase + class Exception < StandardError + end + + class TestController < ActionController::Base + include ActionController::Live + + attr_accessor :latch, :tc, :error_latch + + def self.controller_path + "test" + end + + def set_cookie + cookies[:hello] = "world" + response.stream.write "hello world" + response.close + end + + def render_text + render plain: "zomg" + end + + def default_header + response.stream.write "<html><body>hi</body></html>" + response.stream.close + end + + def basic_stream + response.headers["Content-Type"] = "text/event-stream" + %w{ hello world }.each do |word| + response.stream.write word + end + response.stream.close + end + + def blocking_stream + response.headers["Content-Type"] = "text/event-stream" + %w{ hello world }.each do |word| + response.stream.write word + latch.wait + end + response.stream.close + end + + def write_sleep_autoload + path = File.expand_path("../fixtures", __dir__) + ActiveSupport::Dependencies.autoload_paths << path + + response.headers["Content-Type"] = "text/event-stream" + response.stream.write "before load" + sleep 0.01 + silence_warning do + ::LoadMe + end + response.stream.close + latch.count_down + + ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path } + end + + def thread_locals + tc.assert_equal "aaron", Thread.current[:setting] + + response.headers["Content-Type"] = "text/event-stream" + %w{ hello world }.each do |word| + response.stream.write word + end + response.stream.close + end + + def with_stale + render plain: "stale" if stale?(etag: "123", template: false) + end + + def exception_in_view + render "doesntexist" + end + + def exception_in_view_after_commit + response.stream.write "" + render "doesntexist" + end + + def exception_with_callback + response.headers["Content-Type"] = "text/event-stream" + + response.stream.on_error do + response.stream.write %(data: "500 Internal Server Error"\n\n) + response.stream.close + end + + response.stream.write "" # make sure the response is committed + raise "An exception occurred..." + end + + def exception_in_controller + raise Exception, "Exception in controller" + end + + def bad_request_error + raise ActionController::BadRequest + end + + def exception_in_exception_callback + response.headers["Content-Type"] = "text/event-stream" + response.stream.on_error do + raise "We need to go deeper." + end + response.stream.write "" + response.stream.write params[:widget][:didnt_check_for_nil] + end + + def overfill_buffer_and_die + logger = ActionController::Base.logger || Logger.new($stdout) + response.stream.on_error do + logger.warn "Error while streaming." + error_latch.count_down + end + + # Write until the buffer is full. It doesn't expose that + # information directly, so we must hard-code its size: + 10.times do + response.stream.write "." + end + # .. plus one more, because the #each frees up a slot: + response.stream.write "." + + latch.count_down + + # This write will block, and eventually raise + response.stream.write "x" + + 20.times do + response.stream.write "." + end + end + + def ignore_client_disconnect + response.stream.ignore_disconnect = true + + response.stream.write "" # commit + + # These writes will be ignored + 15.times do + response.stream.write "x" + end + + logger.info "Work complete" + latch.count_down + end + end + + tests TestController + + def assert_stream_closed + assert response.stream.closed?, "stream should be closed" + assert response.committed?, "response should be committed" + assert response.sent?, "response should be sent" + end + + def capture_log_output + output = StringIO.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, ActiveSupport::Logger.new(output) + + begin + yield output + ensure + ActionController::Base.logger = old_logger + end + end + + def setup + super + + def @controller.new_controller_thread + Thread.new { yield } + end + end + + def test_set_cookie + get :set_cookie + assert_equal({ "hello" => "world" }, @response.cookies) + assert_equal "hello world", @response.body + end + + def test_write_to_stream + get :basic_stream + assert_equal "helloworld", @response.body + assert_equal "text/event-stream", @response.headers["Content-Type"] + end + + def test_delayed_autoload_after_write_within_interlock_hook + # Simulate InterlockHook + ActiveSupport::Dependencies.interlock.start_running + res = get :write_sleep_autoload + res.each { } + ActiveSupport::Dependencies.interlock.done_running + end + + def test_async_stream + rubinius_skip "https://github.com/rubinius/rubinius/issues/2934" + + @controller.latch = Concurrent::CountDownLatch.new + parts = ["hello", "world"] + + get :blocking_stream + + t = Thread.new(response) { |resp| + resp.await_commit + resp.stream.each do |part| + assert_equal parts.shift, part + ol = @controller.latch + @controller.latch = Concurrent::CountDownLatch.new + ol.count_down + end + } + + assert t.join(3), "timeout expired before the thread terminated" + end + + def test_abort_with_full_buffer + @controller.latch = Concurrent::CountDownLatch.new + @controller.error_latch = Concurrent::CountDownLatch.new + + capture_log_output do |output| + get :overfill_buffer_and_die, format: "plain" + + t = Thread.new(response) { |resp| + resp.await_commit + _, _, body = resp.to_a + body.each do + @controller.latch.wait + body.close + break + end + } + + t.join + @controller.error_latch.wait + assert_match "Error while streaming", output.rewind && output.read + end + end + + def test_ignore_client_disconnect + @controller.latch = Concurrent::CountDownLatch.new + + capture_log_output do |output| + get :ignore_client_disconnect + + t = Thread.new(response) { |resp| + resp.await_commit + _, _, body = resp.to_a + body.each do + body.close + break + end + } + + t.join + Timeout.timeout(3) do + @controller.latch.wait + end + assert_match "Work complete", output.rewind && output.read + end + end + + def test_thread_locals_get_copied + @controller.tc = self + Thread.current[:originating_thread] = Thread.current.object_id + Thread.current[:setting] = "aaron" + + get :thread_locals + end + + def test_live_stream_default_header + get :default_header + assert response.headers["Content-Type"] + end + + def test_render_text + get :render_text + assert_equal "zomg", response.body + assert_stream_closed + end + + def test_exception_handling_html + assert_raises(ActionView::MissingTemplate) do + get :exception_in_view + end + + capture_log_output do |output| + get :exception_in_view_after_commit + assert_match %r((window\.location = "/500\.html"</script></html>)$), response.body + assert_match "Missing template test/doesntexist", output.rewind && output.read + assert_stream_closed + end + assert response.body + assert_stream_closed + end + + def test_exception_handling_plain_text + assert_raises(ActionView::MissingTemplate) do + get :exception_in_view, format: :json + end + + capture_log_output do |output| + get :exception_in_view_after_commit, format: :json + assert_equal "", response.body + assert_match "Missing template test/doesntexist", output.rewind && output.read + assert_stream_closed + end + end + + def test_exception_callback_when_committed + current_threads = Thread.list + + capture_log_output do |output| + get :exception_with_callback, format: "text/event-stream" + + # Wait on the execution of all threads + (Thread.list - current_threads).each(&:join) + + assert_equal %(data: "500 Internal Server Error"\n\n), response.body + assert_match "An exception occurred...", output.rewind && output.read + assert_stream_closed + end + end + + def test_exception_in_controller_before_streaming + assert_raises(ActionController::LiveStreamTest::Exception) do + get :exception_in_controller, format: "text/event-stream" + end + end + + def test_bad_request_in_controller_before_streaming + assert_raises(ActionController::BadRequest) do + get :bad_request_error, format: "text/event-stream" + end + end + + def test_exceptions_raised_handling_exceptions_and_committed + capture_log_output do |output| + get :exception_in_exception_callback, format: "text/event-stream" + assert_equal "", response.body + assert_match "We need to go deeper", output.rewind && output.read + assert_stream_closed + end + end + + def test_stale_without_etag + get :with_stale + assert_equal 200, response.status.to_i + end + + def test_stale_with_etag + @request.if_none_match = %(W/"#{ActiveSupport::Digest.hexdigest('123')}") + get :with_stale + assert_equal 304, response.status.to_i + end + end + + class BufferTest < ActionController::TestCase + def test_nil_callback + buf = ActionController::Live::Buffer.new nil + assert buf.call_on_error + end + end +end + +class LiveStreamRouterTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + include ActionController::Live + + def index + response.headers["Content-Type"] = "text/event-stream" + sse = SSE.new(response.stream) + sse.write("{\"name\":\"John\"}") + sse.write(name: "Ryan") + ensure + sse.close + end + end + + def self.call(env) + routes.call(env) + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + routes.draw do + get "/test" => "live_stream_router_test/test#index" + end + + def app + self.class + end + + test "streaming served through the router" do + get "/test" + + assert_response :ok + assert_match(/data: {\"name\":\"John\"}/, response.body) + assert_match(/data: {\"name\":\"Ryan\"}/, response.body) + end +end diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb new file mode 100644 index 0000000000..d84a76fb46 --- /dev/null +++ b/actionpack/test/controller/localized_templates_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class LocalizedController < ActionController::Base + def hello_world + end +end + +class LocalizedTemplatesTest < ActionController::TestCase + tests LocalizedController + + setup do + @old_locale = I18n.locale + end + + teardown do + I18n.locale = @old_locale + end + + def test_localized_template_is_used + I18n.locale = :de + get :hello_world + assert_equal "Guten Tag", @response.body + end + + def test_default_locale_template_is_used_when_locale_is_missing + I18n.locale = :dk + get :hello_world + assert_equal "Hello World", @response.body + end + + def test_use_fallback_locales + I18n.locale = :"de-AT" + I18n.backend.class.include(I18n::Backend::Fallbacks) + I18n.fallbacks[:"de-AT"] = [:de] + + get :hello_world + assert_equal "Guten Tag", @response.body + end + + def test_localized_template_has_correct_header_with_no_format_in_template_name + I18n.locale = :it + get :hello_world + assert_equal "Ciao Mondo", @response.body + assert_equal "text/html", @response.content_type + end +end diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb new file mode 100644 index 0000000000..0562c16284 --- /dev/null +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -0,0 +1,369 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/log_subscriber/test_helper" +require "action_controller/log_subscriber" + +module Another + class LogSubscribersController < ActionController::Base + wrap_parameters :person, include: :name, format: :json + + class SpecialException < Exception + end + + rescue_from SpecialException do + head 406 + end + + before_action :redirector, only: :never_executed + + def never_executed + end + + def show + head :ok + end + + def redirector + redirect_to "http://foo.bar/" + end + + def filterable_redirector + redirect_to "http://secret.foo.bar/" + end + + def data_sender + send_data "cool data", filename: "file.txt" + end + + def file_sender + send_file File.expand_path("company.rb", FIXTURE_LOAD_PATH) + end + + def with_fragment_cache + render inline: "<%= cache('foo'){ 'bar' } %>" + end + + def with_fragment_cache_and_percent_in_key + render inline: "<%= cache('foo%bar'){ 'Contains % sign in key' } %>" + end + + def with_fragment_cache_if_with_true_condition + render inline: "<%= cache_if(true, 'foo') { 'bar' } %>" + end + + def with_fragment_cache_if_with_false_condition + render inline: "<%= cache_if(false, 'foo') { 'bar' } %>" + end + + def with_fragment_cache_unless_with_false_condition + render inline: "<%= cache_unless(false, 'foo') { 'bar' } %>" + end + + def with_fragment_cache_unless_with_true_condition + render inline: "<%= cache_unless(true, 'foo') { 'bar' } %>" + end + + def with_exception + raise Exception + end + + def with_rescued_exception + raise SpecialException + end + + def with_action_not_found + raise AbstractController::ActionNotFound + end + + def append_info_to_payload(payload) + super + payload[:test_key] = "test_value" + @last_payload = payload + end + + attr_reader :last_payload + end +end + +class ACLogSubscriberTest < ActionController::TestCase + tests Another::LogSubscribersController + include ActiveSupport::LogSubscriber::TestHelper + + def setup + super + ActionController::Base.enable_fragment_cache_logging = true + + @old_logger = ActionController::Base.logger + + @cache_path = Dir.mktmpdir(%w[tmp cache]) + @controller.cache_store = :file_store, @cache_path + ActionController::LogSubscriber.attach_to :action_controller + end + + def teardown + super + ActiveSupport::LogSubscriber.log_subscribers.clear + FileUtils.rm_rf(@cache_path) + ActionController::Base.logger = @old_logger + ActionController::Base.enable_fragment_cache_logging = true + end + + def set_logger(logger) + ActionController::Base.logger = logger + end + + def test_start_processing + get :show + wait + assert_equal 2, logs.size + assert_equal "Processing by Another::LogSubscribersController#show as HTML", logs.first + end + + def test_halted_callback + get :never_executed + wait + assert_equal 4, logs.size + assert_equal "Filter chain halted as :redirector rendered or redirected", logs.third + end + + def test_process_action + get :show + wait + assert_equal 2, logs.size + assert_match(/Completed/, logs.last) + assert_match(/200 OK/, logs.last) + end + + def test_process_action_without_parameters + get :show + wait + assert_nil logs.detect { |l| l =~ /Parameters/ } + end + + def test_process_action_with_parameters + get :show, params: { id: "10" } + wait + + assert_equal 3, logs.size + assert_equal 'Parameters: {"id"=>"10"}', logs[1] + end + + def test_multiple_process_with_parameters + get :show, params: { id: "10" } + get :show, params: { id: "20" } + + wait + + assert_equal 6, logs.size + assert_equal 'Parameters: {"id"=>"10"}', logs[1] + assert_equal 'Parameters: {"id"=>"20"}', logs[4] + end + + def test_process_action_with_wrapped_parameters + @request.env["CONTENT_TYPE"] = "application/json" + post :show, params: { id: "10", name: "jose" } + wait + + assert_equal 3, logs.size + assert_match '"person"=>{"name"=>"jose"}', logs[1] + end + + def test_process_action_with_view_runtime + get :show + wait + assert_match(/Completed 200 OK in \d+ms/, logs[1]) + end + + def test_append_info_to_payload_is_called_even_with_exception + begin + get :with_exception + wait + rescue Exception + end + + assert_equal "test_value", @controller.last_payload[:test_key] + end + + def test_process_action_headers + get :show + wait + assert_equal "Rails Testing", @controller.last_payload[:headers]["User-Agent"] + end + + def test_process_action_with_filter_parameters + @request.env["action_dispatch.parameter_filter"] = [:lifo, :amount] + + get :show, params: { + lifo: "Pratik", amount: "420", step: "1" + } + wait + + params = logs[1] + assert_match(/"amount"=>"\[FILTERED\]"/, params) + assert_match(/"lifo"=>"\[FILTERED\]"/, params) + assert_match(/"step"=>"1"/, params) + end + + def test_redirect_to + get :redirector + wait + + assert_equal 3, logs.size + assert_equal "Redirected to http://foo.bar/", logs[1] + end + + def test_filter_redirect_url_by_string + @request.env["action_dispatch.redirect_filter"] = ["secret"] + get :filterable_redirector + wait + + assert_equal 3, logs.size + assert_equal "Redirected to [FILTERED]", logs[1] + end + + def test_filter_redirect_url_by_regexp + @request.env["action_dispatch.redirect_filter"] = [/secret\.foo.+/] + get :filterable_redirector + wait + + assert_equal 3, logs.size + assert_equal "Redirected to [FILTERED]", logs[1] + end + + def test_send_data + get :data_sender + wait + + assert_equal 3, logs.size + assert_match(/Sent data file\.txt/, logs[1]) + end + + def test_send_file + get :file_sender + wait + + assert_equal 3, logs.size + assert_match(/Sent file/, logs[1]) + assert_match(/test\/fixtures\/company\.rb/, logs[1]) + end + + def test_with_fragment_cache + @controller.config.perform_caching = true + get :with_fragment_cache + wait + + assert_equal 4, logs.size + assert_match(/Read fragment views\/foo/, logs[1]) + assert_match(/Write fragment views\/foo/, logs[2]) + ensure + @controller.config.perform_caching = true + end + + def test_with_fragment_cache_when_log_disabled + @controller.config.perform_caching = true + ActionController::Base.enable_fragment_cache_logging = false + get :with_fragment_cache + wait + + assert_equal 2, logs.size + assert_equal "Processing by Another::LogSubscribersController#with_fragment_cache as HTML", logs[0] + assert_match(/Completed 200 OK in \d+ms/, logs[1]) + ensure + @controller.config.perform_caching = true + ActionController::Base.enable_fragment_cache_logging = true + end + + def test_with_fragment_cache_if_with_true + @controller.config.perform_caching = true + get :with_fragment_cache_if_with_true_condition + wait + + assert_equal 4, logs.size + assert_match(/Read fragment views\/foo/, logs[1]) + assert_match(/Write fragment views\/foo/, logs[2]) + ensure + @controller.config.perform_caching = true + end + + def test_with_fragment_cache_if_with_false + @controller.config.perform_caching = true + get :with_fragment_cache_if_with_false_condition + wait + + assert_equal 2, logs.size + assert_no_match(/Read fragment views\/foo/, logs[1]) + assert_no_match(/Write fragment views\/foo/, logs[2]) + ensure + @controller.config.perform_caching = true + end + + def test_with_fragment_cache_unless_with_true + @controller.config.perform_caching = true + get :with_fragment_cache_unless_with_true_condition + wait + + assert_equal 2, logs.size + assert_no_match(/Read fragment views\/foo/, logs[1]) + assert_no_match(/Write fragment views\/foo/, logs[2]) + ensure + @controller.config.perform_caching = true + end + + def test_with_fragment_cache_unless_with_false + @controller.config.perform_caching = true + get :with_fragment_cache_unless_with_false_condition + wait + + assert_equal 4, logs.size + assert_match(/Read fragment views\/foo/, logs[1]) + assert_match(/Write fragment views\/foo/, logs[2]) + ensure + @controller.config.perform_caching = true + end + + def test_with_fragment_cache_and_percent_in_key + @controller.config.perform_caching = true + get :with_fragment_cache_and_percent_in_key + wait + + assert_equal 4, logs.size + assert_match(/Read fragment views\/foo/, logs[1]) + assert_match(/Write fragment views\/foo/, logs[2]) + ensure + @controller.config.perform_caching = true + end + + def test_process_action_with_exception_includes_http_status_code + begin + get :with_exception + wait + rescue Exception + end + assert_equal 2, logs.size + assert_match(/Completed 500/, logs.last) + end + + def test_process_action_with_rescued_exception_includes_http_status_code + get :with_rescued_exception + wait + + assert_equal 2, logs.size + assert_match(/Completed 406/, logs.last) + end + + def test_process_action_with_with_action_not_found_logs_404 + begin + get :with_action_not_found + wait + rescue AbstractController::ActionNotFound + end + + assert_equal 2, logs.size + assert_match(/Completed 404/, logs.last) + end + + def logs + @logs ||= @logger.logged(:info) + end +end diff --git a/actionpack/test/controller/metal/renderers_test.rb b/actionpack/test/controller/metal/renderers_test.rb new file mode 100644 index 0000000000..5f0d125128 --- /dev/null +++ b/actionpack/test/controller/metal/renderers_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/hash/conversions" + +class MetalRenderingController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + include ActionController::Renderers +end + +class MetalRenderingJsonController < MetalRenderingController + class Model + def to_json(options = {}) + { a: "b" }.to_json(options) + end + + def to_xml(options = {}) + { a: "b" }.to_xml(options) + end + end + + use_renderers :json + + def one + render json: Model.new + end + + def two + render xml: Model.new + end +end + +class RenderersMetalTest < ActionController::TestCase + tests MetalRenderingJsonController + + def test_render_json + get :one + assert_response :success + assert_equal({ a: "b" }.to_json, @response.body) + assert_equal "application/json", @response.content_type + end + + def test_render_xml + get :two + assert_response :success + assert_equal(" ", @response.body) + assert_equal "text/plain", @response.content_type + end +end diff --git a/actionpack/test/controller/metal_test.rb b/actionpack/test/controller/metal_test.rb new file mode 100644 index 0000000000..7b53092266 --- /dev/null +++ b/actionpack/test/controller/metal_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class MetalControllerInstanceTests < ActiveSupport::TestCase + class SimpleController < ActionController::Metal + def hello + self.response_body = "hello" + end + end + + def test_response_does_not_have_default_headers + original_default_headers = ActionDispatch::Response.default_headers + + ActionDispatch::Response.default_headers = { + "X-Frame-Options" => "DENY", + "X-Content-Type-Options" => "nosniff", + "X-XSS-Protection" => "1;" + } + + response_headers = SimpleController.action("hello").call( + "REQUEST_METHOD" => "GET", + "rack.input" => -> { } + )[1] + + assert_not response_headers.key?("X-Frame-Options") + assert_not response_headers.key?("X-Content-Type-Options") + assert_not response_headers.key?("X-XSS-Protection") + ensure + ActionDispatch::Response.default_headers = original_default_headers + end +end diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb new file mode 100644 index 0000000000..eed671d593 --- /dev/null +++ b/actionpack/test/controller/mime/accept_format_test.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class StarStarMimeController < ActionController::Base + layout nil + + def index + render + end +end + +class StarStarMimeControllerTest < ActionController::TestCase + def test_javascript_with_format + @request.accept = "text/javascript" + get :index, format: "js" + assert_match "function addition(a,b){ return a+b; }", @response.body + end + + def test_javascript_with_no_format + @request.accept = "text/javascript" + get :index + assert_match "function addition(a,b){ return a+b; }", @response.body + end + + def test_javascript_with_no_format_only_star_star + @request.accept = "*/*" + get :index + assert_match "function addition(a,b){ return a+b; }", @response.body + end +end + +class AbstractPostController < ActionController::Base + self.view_paths = File.expand_path("../../fixtures/post_test", __dir__) +end + +# For testing layouts which are set automatically +class PostController < AbstractPostController + around_action :with_iphone + + def index + respond_to(:html, :iphone, :js) + end + +private + + def with_iphone + request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone" + yield + end +end + +class SuperPostController < PostController +end + +class MimeControllerLayoutsTest < ActionController::TestCase + tests PostController + + def setup + super + @request.host = "www.example.com" + Mime::Type.register_alias("text/html", :iphone) + end + + def teardown + super + Mime::Type.unregister(:iphone) + end + + def test_missing_layout_renders_properly + get :index + assert_equal '<html><div id="html">Hello Firefox</div></html>', @response.body + + @request.accept = "text/iphone" + get :index + assert_equal "Hello iPhone", @response.body + end + + def test_format_with_inherited_layouts + @controller = SuperPostController.new + + get :index + assert_equal '<html><div id="html">Super Firefox</div></html>', @response.body + + @request.accept = "text/iphone" + get :index + assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body + end + + def test_non_navigational_format_with_no_template_fallbacks_to_html_template_with_no_layout + get :index, format: :js + assert_equal "Hello Firefox", @response.body + end +end diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb new file mode 100644 index 0000000000..00e1d5f3b3 --- /dev/null +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -0,0 +1,877 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/log_subscriber/test_helper" + +class RespondToController < ActionController::Base + layout :set_layout + + before_action { + case params[:v] + when String then request.variant = params[:v].to_sym + when Array then request.variant = params[:v].map(&:to_sym) + end + } + + def html_xml_or_rss + respond_to do |type| + type.html { render body: "HTML" } + type.xml { render body: "XML" } + type.rss { render body: "RSS" } + type.all { render body: "Nothing" } + end + end + + def js_or_html + respond_to do |type| + type.html { render body: "HTML" } + type.js { render body: "JS" } + type.all { render body: "Nothing" } + end + end + + def json_or_yaml + respond_to do |type| + type.json { render body: "JSON" } + type.yaml { render body: "YAML" } + end + end + + def html_or_xml + respond_to do |type| + type.html { render body: "HTML" } + type.xml { render body: "XML" } + type.all { render body: "Nothing" } + end + end + + def json_xml_or_html + respond_to do |type| + type.json { render body: "JSON" } + type.xml { render xml: "XML" } + type.html { render body: "HTML" } + end + end + + def forced_xml + request.format = :xml + + respond_to do |type| + type.html { render body: "HTML" } + type.xml { render body: "XML" } + end + end + + def just_xml + respond_to do |type| + type.xml { render body: "XML" } + end + end + + def using_defaults + respond_to do |type| + type.html + type.xml + end + end + + def missing_templates + respond_to do |type| + # This test requires a block that is empty + type.json { } + type.xml + end + end + + def using_defaults_with_type_list + respond_to(:html, :xml) + end + + def using_defaults_with_all + respond_to do |type| + type.html + type.all { render body: "ALL" } + end + end + + def made_for_content_type + respond_to do |type| + type.rss { render body: "RSS" } + type.atom { render body: "ATOM" } + type.all { render body: "Nothing" } + end + end + + def using_conflicting_nested_js_then_html + respond_to do |outer_type| + outer_type.js do + respond_to do |inner_type| + inner_type.html { render body: "HTML" } + end + end + end + end + + def using_non_conflicting_nested_js_then_js + respond_to do |outer_type| + outer_type.js do + respond_to do |inner_type| + inner_type.js { render body: "JS" } + end + end + end + end + + def custom_type_handling + respond_to do |type| + type.html { render body: "HTML" } + type.custom("application/crazy-xml") { render body: "Crazy XML" } + type.all { render body: "Nothing" } + end + end + + def custom_constant_handling + respond_to do |type| + type.html { render body: "HTML" } + type.mobile { render body: "Mobile" } + end + end + + def custom_constant_handling_without_block + respond_to do |type| + type.html { render body: "HTML" } + type.mobile + end + end + + def handle_any + respond_to do |type| + type.html { render body: "HTML" } + type.any(:js, :xml) { render body: "Either JS or XML" } + end + end + + def handle_any_any + respond_to do |type| + type.html { render body: "HTML" } + type.any { render body: "Whatever you ask for, I got it" } + end + end + + def all_types_with_layout + respond_to do |type| + type.html + end + end + + def json_with_callback + respond_to do |type| + type.json { render json: "JS", callback: "alert" } + end + end + + def iphone_with_html_response_type + request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone" + + respond_to do |type| + type.html { @type = "Firefox" } + type.iphone { @type = "iPhone" } + end + end + + def iphone_with_html_response_type_without_layout + request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone" + + respond_to do |type| + type.html { @type = "Firefox"; render action: "iphone_with_html_response_type" } + type.iphone { @type = "iPhone" ; render action: "iphone_with_html_response_type" } + end + end + + def variant_with_implicit_template_rendering + # This has exactly one variant template defined in the file system (+mobile.html.erb), + # which raises the regular MissingTemplate error for other variants. + end + + def variant_without_implicit_template_rendering + # This differs from the above in that it does not have any templates defined in the file + # system, which triggers the ImplicitRender (204 No Content) behavior. + end + + def variant_with_format_and_custom_render + request.variant = :mobile + + respond_to do |type| + type.html { render body: "mobile" } + end + end + + def multiple_variants_for_format + respond_to do |type| + type.html do |html| + html.tablet { render body: "tablet" } + html.phone { render body: "phone" } + end + end + end + + def variant_plus_none_for_format + respond_to do |format| + format.html do |variant| + variant.phone { render body: "phone" } + variant.none + end + end + end + + def variant_inline_syntax + respond_to do |format| + format.js { render body: "js" } + format.html.none { render body: "none" } + format.html.phone { render body: "phone" } + end + end + + def variant_inline_syntax_without_block + respond_to do |format| + format.js + format.html.none + format.html.phone + end + end + + def variant_any + respond_to do |format| + format.html do |variant| + variant.any(:tablet, :phablet) { render body: "any" } + variant.phone { render body: "phone" } + end + end + end + + def variant_any_any + respond_to do |format| + format.html do |variant| + variant.any { render body: "any" } + variant.phone { render body: "phone" } + end + end + end + + def variant_inline_any + respond_to do |format| + format.html.any(:tablet, :phablet) { render body: "any" } + format.html.phone { render body: "phone" } + end + end + + def variant_inline_any_any + respond_to do |format| + format.html.phone { render body: "phone" } + format.html.any { render body: "any" } + end + end + + def variant_any_implicit_render + respond_to do |format| + format.html.phone + format.html.any(:tablet, :phablet) + end + end + + def variant_any_with_none + respond_to do |format| + format.html.any(:none, :phone) { render body: "none or phone" } + end + end + + def format_any_variant_any + respond_to do |format| + format.html { render body: "HTML" } + format.any(:js, :xml) do |variant| + variant.phone { render body: "phone" } + variant.any(:tablet, :phablet) { render body: "tablet" } + end + end + end + + private + def set_layout + case action_name + when "all_types_with_layout", "iphone_with_html_response_type" + "respond_to/layouts/standard" + when "iphone_with_html_response_type_without_layout" + "respond_to/layouts/missing" + end + end +end + +class RespondToControllerTest < ActionController::TestCase + NO_CONTENT_WARNING = "No template found for RespondToController#variant_without_implicit_template_rendering, rendering head :no_content" + + def setup + super + @request.host = "www.example.com" + Mime::Type.register_alias("text/html", :iphone) + Mime::Type.register("text/x-mobile", :mobile) + end + + def teardown + super + Mime::Type.unregister(:iphone) + Mime::Type.unregister(:mobile) + end + + def test_html + @request.accept = "text/html" + get :js_or_html + assert_equal "HTML", @response.body + + get :html_or_xml + assert_equal "HTML", @response.body + + assert_raises(ActionController::UnknownFormat) do + get :just_xml + end + end + + def test_all + @request.accept = "*/*" + get :js_or_html + assert_equal "HTML", @response.body # js is not part of all + + get :html_or_xml + assert_equal "HTML", @response.body + + get :just_xml + assert_equal "XML", @response.body + end + + def test_xml + @request.accept = "application/xml" + get :html_xml_or_rss + assert_equal "XML", @response.body + end + + def test_js_or_html + @request.accept = "text/javascript, text/html" + get :js_or_html, xhr: true + assert_equal "JS", @response.body + + @request.accept = "text/javascript, text/html" + get :html_or_xml, xhr: true + assert_equal "HTML", @response.body + + @request.accept = "text/javascript, text/html" + + assert_raises(ActionController::UnknownFormat) do + get :just_xml, xhr: true + end + end + + def test_json_or_yaml_with_leading_star_star + @request.accept = "*/*, application/json" + get :json_xml_or_html + assert_equal "HTML", @response.body + + @request.accept = "*/* , application/json" + get :json_xml_or_html + assert_equal "HTML", @response.body + end + + def test_json_or_yaml + get :json_or_yaml, xhr: true + assert_equal "JSON", @response.body + + get :json_or_yaml, format: "json" + assert_equal "JSON", @response.body + + get :json_or_yaml, format: "yaml" + assert_equal "YAML", @response.body + + { "YAML" => %w(text/yaml), + "JSON" => %w(application/json text/x-json) + }.each do |body, content_types| + content_types.each do |content_type| + @request.accept = content_type + get :json_or_yaml + assert_equal body, @response.body + end + end + end + + def test_js_or_anything + @request.accept = "text/javascript, */*" + get :js_or_html, xhr: true + assert_equal "JS", @response.body + + get :html_or_xml, xhr: true + assert_equal "HTML", @response.body + + get :just_xml, xhr: true + assert_equal "XML", @response.body + end + + def test_using_defaults + @request.accept = "*/*" + get :using_defaults + assert_equal "text/html", @response.content_type + assert_equal "Hello world!", @response.body + + @request.accept = "application/xml" + get :using_defaults + assert_equal "application/xml", @response.content_type + assert_equal "<p>Hello world!</p>\n", @response.body + end + + def test_using_defaults_with_all + @request.accept = "*/*" + get :using_defaults_with_all + assert_equal "HTML!", @response.body.strip + + @request.accept = "text/html" + get :using_defaults_with_all + assert_equal "HTML!", @response.body.strip + + @request.accept = "application/json" + get :using_defaults_with_all + assert_equal "ALL", @response.body + end + + def test_using_defaults_with_type_list + @request.accept = "*/*" + get :using_defaults_with_type_list + assert_equal "text/html", @response.content_type + assert_equal "Hello world!", @response.body + + @request.accept = "application/xml" + get :using_defaults_with_type_list + assert_equal "application/xml", @response.content_type + assert_equal "<p>Hello world!</p>\n", @response.body + end + + def test_using_conflicting_nested_js_then_html + @request.accept = "*/*" + assert_raises(ActionController::RespondToMismatchError) do + get :using_conflicting_nested_js_then_html + end + end + + def test_using_non_conflicting_nested_js_then_js + @request.accept = "*/*" + get :using_non_conflicting_nested_js_then_js + assert_equal "text/javascript", @response.content_type + assert_equal "JS", @response.body + end + + def test_with_atom_content_type + @request.accept = "" + @request.env["CONTENT_TYPE"] = "application/atom+xml" + get :made_for_content_type, xhr: true + assert_equal "ATOM", @response.body + end + + def test_with_rss_content_type + @request.accept = "" + @request.env["CONTENT_TYPE"] = "application/rss+xml" + get :made_for_content_type, xhr: true + assert_equal "RSS", @response.body + end + + def test_synonyms + @request.accept = "application/javascript" + get :js_or_html + assert_equal "JS", @response.body + + @request.accept = "application/x-xml" + get :html_xml_or_rss + assert_equal "XML", @response.body + end + + def test_custom_types + @request.accept = "application/crazy-xml" + get :custom_type_handling + assert_equal "application/crazy-xml", @response.content_type + assert_equal "Crazy XML", @response.body + + @request.accept = "text/html" + get :custom_type_handling + assert_equal "text/html", @response.content_type + assert_equal "HTML", @response.body + end + + def test_xhtml_alias + @request.accept = "application/xhtml+xml,application/xml" + get :html_or_xml + assert_equal "HTML", @response.body + end + + def test_firefox_simulation + @request.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" + get :html_or_xml + assert_equal "HTML", @response.body + end + + def test_handle_any + @request.accept = "*/*" + get :handle_any + assert_equal "HTML", @response.body + + @request.accept = "text/javascript" + get :handle_any + assert_equal "Either JS or XML", @response.body + + @request.accept = "text/xml" + get :handle_any + assert_equal "Either JS or XML", @response.body + end + + def test_handle_any_any + @request.accept = "*/*" + get :handle_any_any + assert_equal "HTML", @response.body + end + + def test_handle_any_any_parameter_format + get :handle_any_any, format: "html" + assert_equal "HTML", @response.body + end + + def test_handle_any_any_explicit_html + @request.accept = "text/html" + get :handle_any_any + assert_equal "HTML", @response.body + end + + def test_handle_any_any_javascript + @request.accept = "text/javascript" + get :handle_any_any + assert_equal "Whatever you ask for, I got it", @response.body + end + + def test_handle_any_any_xml + @request.accept = "text/xml" + get :handle_any_any + assert_equal "Whatever you ask for, I got it", @response.body + end + + def test_handle_any_any_unknown_format + get :handle_any_any, format: "php" + assert_equal "Whatever you ask for, I got it", @response.body + end + + def test_browser_check_with_any_any + @request.accept = "application/json, application/xml" + get :json_xml_or_html + assert_equal "JSON", @response.body + + @request.accept = "application/json, application/xml, */*" + get :json_xml_or_html + assert_equal "HTML", @response.body + end + + def test_html_type_with_layout + @request.accept = "text/html" + get :all_types_with_layout + assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body + end + + def test_json_with_callback_sets_javascript_content_type + @request.accept = "application/json" + get :json_with_callback + assert_equal "/**/alert(JS)", @response.body + assert_equal "text/javascript", @response.content_type + end + + def test_xhr + get :js_or_html, xhr: true + assert_equal "JS", @response.body + end + + def test_custom_constant + get :custom_constant_handling, format: "mobile" + assert_equal "text/x-mobile", @response.content_type + assert_equal "Mobile", @response.body + end + + def test_custom_constant_handling_without_block + get :custom_constant_handling_without_block, format: "mobile" + assert_equal "text/x-mobile", @response.content_type + assert_equal "Mobile", @response.body + end + + def test_forced_format + get :html_xml_or_rss + assert_equal "HTML", @response.body + + get :html_xml_or_rss, format: "html" + assert_equal "HTML", @response.body + + get :html_xml_or_rss, format: "xml" + assert_equal "XML", @response.body + + get :html_xml_or_rss, format: "rss" + assert_equal "RSS", @response.body + end + + def test_internally_forced_format + get :forced_xml + assert_equal "XML", @response.body + + get :forced_xml, format: "html" + assert_equal "XML", @response.body + end + + def test_extension_synonyms + get :html_xml_or_rss, format: "xhtml" + assert_equal "HTML", @response.body + end + + def test_render_action_for_html + @controller.instance_eval do + def render(*args) + @action = args.first[:action] unless args.empty? + @action ||= action_name + + response.body = "#{@action} - #{formats}" + end + end + + get :using_defaults + assert_equal "using_defaults - #{[:html]}", @response.body + + get :using_defaults, format: "xml" + assert_equal "using_defaults - #{[:xml]}", @response.body + end + + def test_format_with_custom_response_type + get :iphone_with_html_response_type + assert_equal '<html><div id="html">Hello future from Firefox!</div></html>', @response.body + + get :iphone_with_html_response_type, format: "iphone" + assert_equal "text/html", @response.content_type + assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body + end + + def test_format_with_custom_response_type_and_request_headers + @request.accept = "text/iphone" + get :iphone_with_html_response_type + assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body + assert_equal "text/html", @response.content_type + end + + def test_invalid_format + assert_raises(ActionController::UnknownFormat) do + get :using_defaults, format: "invalidformat" + end + end + + def test_missing_templates + get :missing_templates, format: :json + assert_response :no_content + get :missing_templates, format: :xml + assert_response :no_content + end + + def test_invalid_variant + assert_raises(ActionController::UnknownFormat) do + get :variant_with_implicit_template_rendering, params: { v: :invalid } + end + end + + def test_variant_not_set_regular_unknown_format + assert_raises(ActionController::UnknownFormat) do + get :variant_with_implicit_template_rendering + end + end + + def test_variant_with_implicit_template_rendering + get :variant_with_implicit_template_rendering, params: { v: :mobile } + assert_equal "text/html", @response.content_type + assert_equal "mobile", @response.body + end + + def test_variant_without_implicit_rendering_from_browser + assert_raises(ActionController::MissingExactTemplate) do + get :variant_without_implicit_template_rendering, params: { v: :does_not_matter } + end + end + + def test_variant_variant_not_set_and_without_implicit_rendering_from_browser + assert_raises(ActionController::MissingExactTemplate) do + get :variant_without_implicit_template_rendering + end + end + + def test_variant_without_implicit_rendering_from_xhr + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + + get :variant_without_implicit_template_rendering, xhr: true, params: { v: :does_not_matter } + assert_response :no_content + + assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger + end + + def test_variant_without_implicit_rendering_from_api + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + + get :variant_without_implicit_template_rendering, format: "json", params: { v: :does_not_matter } + assert_response :no_content + + assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger + end + + def test_variant_variant_not_set_and_without_implicit_rendering_from_xhr + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + + get :variant_without_implicit_template_rendering, xhr: true + assert_response :no_content + + assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger + end + + def test_variant_with_format_and_custom_render + get :variant_with_format_and_custom_render, params: { v: :phone } + assert_equal "text/html", @response.content_type + assert_equal "mobile", @response.body + end + + def test_multiple_variants_for_format + get :multiple_variants_for_format, params: { v: :tablet } + assert_equal "text/html", @response.content_type + assert_equal "tablet", @response.body + end + + def test_no_variant_in_variant_setup + get :variant_plus_none_for_format + assert_equal "text/html", @response.content_type + assert_equal "none", @response.body + end + + def test_variant_inline_syntax + get :variant_inline_syntax + assert_equal "text/html", @response.content_type + assert_equal "none", @response.body + + get :variant_inline_syntax, params: { v: :phone } + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_inline_syntax_with_format + get :variant_inline_syntax, format: :js + assert_equal "text/javascript", @response.content_type + assert_equal "js", @response.body + end + + def test_variant_inline_syntax_without_block + get :variant_inline_syntax_without_block, params: { v: :phone } + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_any + get :variant_any, params: { v: :phone } + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + get :variant_any, params: { v: :tablet } + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + + get :variant_any, params: { v: :phablet } + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_any_any + get :variant_any_any + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + + get :variant_any_any, params: { v: :phone } + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + get :variant_any_any, params: { v: :yolo } + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_inline_any + get :variant_any, params: { v: :phone } + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + get :variant_inline_any, params: { v: :tablet } + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + + get :variant_inline_any, params: { v: :phablet } + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_inline_any_any + get :variant_inline_any_any, params: { v: :phone } + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + + get :variant_inline_any_any, params: { v: :yolo } + assert_equal "text/html", @response.content_type + assert_equal "any", @response.body + end + + def test_variant_any_implicit_render + get :variant_any_implicit_render, params: { v: :tablet } + assert_equal "text/html", @response.content_type + assert_equal "tablet", @response.body + + get :variant_any_implicit_render, params: { v: :phablet } + assert_equal "text/html", @response.content_type + assert_equal "phablet", @response.body + end + + def test_variant_any_with_none + get :variant_any_with_none + assert_equal "text/html", @response.content_type + assert_equal "none or phone", @response.body + + get :variant_any_with_none, params: { v: :phone } + assert_equal "text/html", @response.content_type + assert_equal "none or phone", @response.body + end + + def test_format_any_variant_any + get :format_any_variant_any, format: :js, params: { v: :tablet } + assert_equal "text/javascript", @response.content_type + assert_equal "tablet", @response.body + end + + def test_variant_negotiation_inline_syntax + get :variant_inline_syntax_without_block, params: { v: [:tablet, :phone] } + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_negotiation_block_syntax + get :variant_plus_none_for_format, params: { v: [:tablet, :phone] } + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end + + def test_variant_negotiation_without_block + get :variant_inline_syntax_without_block, params: { v: [:tablet, :phone] } + assert_equal "text/html", @response.content_type + assert_equal "phone", @response.body + end +end diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb new file mode 100644 index 0000000000..7572d514fb --- /dev/null +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module BareMetalTest + class BareController < ActionController::Metal + def index + self.response_body = "Hello world" + end + end + + class BareTest < ActiveSupport::TestCase + test "response body is a Rack-compatible response" do + status, headers, body = BareController.action(:index).call(Rack::MockRequest.env_for("/")) + assert_equal 200, status + string = +"" + + body.each do |part| + assert part.is_a?(String), "Each part of the body must be a String" + string << part + end + + assert_kind_of Hash, headers, "Headers must be a Hash" + assert headers["Content-Type"], "Content-Type must exist" + + assert_equal "Hello world", string + end + + test "response_body value is wrapped in an array when the value is a String" do + controller = BareController.new + controller.set_request!(ActionDispatch::Request.empty) + controller.set_response!(BareController.make_response!(controller.request)) + controller.index + assert_equal ["Hello world"], controller.response_body + end + + test "connect a request to controller instance without dispatch" do + env = {} + controller = BareController.new + controller.set_request! ActionDispatch::Request.new(env) + assert controller.request + end + end + + class BareEmptyController < ActionController::Metal + def index + self.response_body = nil + end + end + + class BareEmptyTest < ActiveSupport::TestCase + test "response body is nil" do + controller = BareEmptyController.new + controller.set_request!(ActionDispatch::Request.empty) + controller.set_response!(BareController.make_response!(controller.request)) + controller.index + assert_nil controller.response_body + end + end + + class HeadController < ActionController::Metal + include ActionController::Head + + def index + head :not_found + end + + def continue + self.content_type = "text/html" + head 100 + end + + def switching_protocols + self.content_type = "text/html" + head 101 + end + + def processing + self.content_type = "text/html" + head 102 + end + + def no_content + self.content_type = "text/html" + head 204 + end + + def reset_content + self.content_type = "text/html" + head 205 + end + + def not_modified + self.content_type = "text/html" + head 304 + end + end + + class HeadTest < ActiveSupport::TestCase + test "head works on its own" do + status = HeadController.action(:index).call(Rack::MockRequest.env_for("/")).first + assert_equal 404, status + end + + test "head :continue (100) does not return a content-type header" do + headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second + assert_nil headers["Content-Type"] + assert_nil headers["Content-Length"] + end + + test "head :switching_protocols (101) does not return a content-type header" do + headers = HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")).second + assert_nil headers["Content-Type"] + assert_nil headers["Content-Length"] + end + + test "head :processing (102) does not return a content-type header" do + headers = HeadController.action(:processing).call(Rack::MockRequest.env_for("/")).second + assert_nil headers["Content-Type"] + assert_nil headers["Content-Length"] + end + + test "head :no_content (204) does not return a content-type header" do + headers = HeadController.action(:no_content).call(Rack::MockRequest.env_for("/")).second + assert_nil headers["Content-Type"] + assert_nil headers["Content-Length"] + end + + test "head :reset_content (205) does not return a content-type header" do + headers = HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/")).second + assert_nil headers["Content-Type"] + assert_nil headers["Content-Length"] + end + + test "head :not_modified (304) does not return a content-type header" do + headers = HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/")).second + assert_nil headers["Content-Type"] + assert_nil headers["Content-Length"] + end + + test "head :no_content (204) does not return any content" do + content = body(HeadController.action(:no_content).call(Rack::MockRequest.env_for("/"))) + assert_empty content + end + + test "head :reset_content (205) does not return any content" do + content = body(HeadController.action(:reset_content).call(Rack::MockRequest.env_for("/"))) + assert_empty content + end + + test "head :not_modified (304) does not return any content" do + content = body(HeadController.action(:not_modified).call(Rack::MockRequest.env_for("/"))) + assert_empty content + end + + test "head :continue (100) does not return any content" do + content = body(HeadController.action(:continue).call(Rack::MockRequest.env_for("/"))) + assert_empty content + end + + test "head :switching_protocols (101) does not return any content" do + content = body(HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/"))) + assert_empty content + end + + test "head :processing (102) does not return any content" do + content = body(HeadController.action(:processing).call(Rack::MockRequest.env_for("/"))) + assert_empty content + end + + def body(rack_response) + buf = [] + rack_response[2].each { |x| buf << x } + buf.join + end + end + + class BareControllerTest < ActionController::TestCase + test "GET index" do + get :index + assert_equal "Hello world", @response.body + end + end +end diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb new file mode 100644 index 0000000000..280134f8d2 --- /dev/null +++ b/actionpack/test/controller/new_base/base_test.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require "abstract_unit" + +# Tests the controller dispatching happy path +module Dispatching + class SimpleController < ActionController::Base + before_action :authenticate + + def index + render body: "success" + end + + def modify_response_body + self.response_body = "success" + end + + def modify_response_body_twice + ret = (self.response_body = "success") + self.response_body = "#{ret}!" + end + + def modify_response_headers + end + + def show_actions + render body: "actions: #{action_methods.to_a.sort.join(', ')}" + end + + private + def authenticate + end + end + + class EmptyController < ActionController::Base ; end + class SubEmptyController < EmptyController ; end + class NonDefaultPathController < ActionController::Base + def self.controller_path; "i_am_not_default"; end + end + + module Submodule + class ContainedEmptyController < ActionController::Base ; end + class ContainedSubEmptyController < ContainedEmptyController ; end + class ContainedNonDefaultPathController < ActionController::Base + def self.controller_path; "i_am_extremely_not_default"; end + end + end + + class BaseTest < Rack::TestCase + test "simple dispatching" do + get "/dispatching/simple/index" + + assert_body "success" + assert_status 200 + assert_content_type "text/plain; charset=utf-8" + end + + test "directly modifying response body" do + get "/dispatching/simple/modify_response_body" + + assert_body "success" + end + + test "directly modifying response body twice" do + get "/dispatching/simple/modify_response_body_twice" + + assert_body "success!" + end + + test "controller path" do + assert_equal "dispatching/empty", EmptyController.controller_path + assert_equal EmptyController.controller_path, EmptyController.new.controller_path + end + + test "non-default controller path" do + assert_equal "i_am_not_default", NonDefaultPathController.controller_path + assert_equal NonDefaultPathController.controller_path, NonDefaultPathController.new.controller_path + end + + test "sub controller path" do + assert_equal "dispatching/sub_empty", SubEmptyController.controller_path + assert_equal SubEmptyController.controller_path, SubEmptyController.new.controller_path + end + + test "namespaced controller path" do + assert_equal "dispatching/submodule/contained_empty", Submodule::ContainedEmptyController.controller_path + assert_equal Submodule::ContainedEmptyController.controller_path, Submodule::ContainedEmptyController.new.controller_path + end + + test "namespaced non-default controller path" do + assert_equal "i_am_extremely_not_default", Submodule::ContainedNonDefaultPathController.controller_path + assert_equal Submodule::ContainedNonDefaultPathController.controller_path, Submodule::ContainedNonDefaultPathController.new.controller_path + end + + test "namespaced sub controller path" do + assert_equal "dispatching/submodule/contained_sub_empty", Submodule::ContainedSubEmptyController.controller_path + assert_equal Submodule::ContainedSubEmptyController.controller_path, Submodule::ContainedSubEmptyController.new.controller_path + end + + test "controller name" do + assert_equal "empty", EmptyController.controller_name + assert_equal "contained_empty", Submodule::ContainedEmptyController.controller_name + end + + test "non-default path controller name" do + assert_equal "non_default_path", NonDefaultPathController.controller_name + assert_equal "contained_non_default_path", Submodule::ContainedNonDefaultPathController.controller_name + end + + test "sub controller name" do + assert_equal "sub_empty", SubEmptyController.controller_name + assert_equal "contained_sub_empty", Submodule::ContainedSubEmptyController.controller_name + end + + test "action methods" do + assert_equal Set.new(%w( + index + modify_response_headers + modify_response_body_twice + modify_response_body + show_actions + )), SimpleController.action_methods + + assert_equal Set.new, EmptyController.action_methods + assert_equal Set.new, Submodule::ContainedEmptyController.action_methods + + get "/dispatching/simple/show_actions" + assert_body "actions: index, modify_response_body, modify_response_body_twice, modify_response_headers, show_actions" + end + end +end diff --git a/actionpack/test/controller/new_base/content_negotiation_test.rb b/actionpack/test/controller/new_base/content_negotiation_test.rb new file mode 100644 index 0000000000..7205e90176 --- /dev/null +++ b/actionpack/test/controller/new_base/content_negotiation_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ContentNegotiation + # This has no layout and it works + class BasicController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new( + "content_negotiation/basic/hello.html.erb" => "Hello world <%= request.formats.first.to_s %>!" + )] + + def all + render plain: formats.inspect + end + end + + class TestContentNegotiation < Rack::TestCase + test "A */* Accept header will return HTML" do + get "/content_negotiation/basic/hello", headers: { "HTTP_ACCEPT" => "*/*" } + assert_body "Hello world */*!" + end + + test "Not all mimes are converted to symbol" do + get "/content_negotiation/basic/all", headers: { "HTTP_ACCEPT" => "text/plain, mime/another" } + assert_body '[:text, "mime/another"]' + end + end +end diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb new file mode 100644 index 0000000000..d3ee4a8a6f --- /dev/null +++ b/actionpack/test/controller/new_base/content_type_test.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ContentType + class BaseController < ActionController::Base + def index + render body: "Hello world!" + end + + def set_on_response_obj + response.content_type = Mime[:rss] + render body: "Hello world!" + end + + def set_on_render + render body: "Hello world!", content_type: Mime[:rss] + end + end + + class ImpliedController < ActionController::Base + # Template's mime type is used if no content_type is specified + + self.view_paths = [ActionView::FixtureResolver.new( + "content_type/implied/i_am_html_erb.html.erb" => "Hello world!", + "content_type/implied/i_am_xml_erb.xml.erb" => "<xml>Hello world!</xml>", + "content_type/implied/i_am_html_builder.html.builder" => "xml.p 'Hello'", + "content_type/implied/i_am_xml_builder.xml.builder" => "xml.awesome 'Hello'" + )] + end + + class CharsetController < ActionController::Base + def set_on_response_obj + response.charset = "utf-16" + render body: "Hello world!" + end + + def set_as_nil_on_response_obj + response.charset = nil + render body: "Hello world!" + end + end + + class ExplicitContentTypeTest < Rack::TestCase + test "default response is text/plain and UTF8" do + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + get ":controller", action: "index" + end + end + + get "/content_type/base" + + assert_body "Hello world!" + assert_header "Content-Type", "text/plain; charset=utf-8" + end + end + + test "setting the content type of the response directly on the response object" do + get "/content_type/base/set_on_response_obj" + + assert_body "Hello world!" + assert_header "Content-Type", "application/rss+xml; charset=utf-8" + end + + test "setting the content type of the response as an option to render" do + get "/content_type/base/set_on_render" + + assert_body "Hello world!" + assert_header "Content-Type", "application/rss+xml; charset=utf-8" + end + end + + class ImpliedContentTypeTest < Rack::TestCase + test "sets Content-Type as text/html when rendering *.html.erb" do + get "/content_type/implied/i_am_html_erb" + + assert_header "Content-Type", "text/html; charset=utf-8" + end + + test "sets Content-Type as application/xml when rendering *.xml.erb" do + get "/content_type/implied/i_am_xml_erb", params: { "format" => "xml" } + + assert_header "Content-Type", "application/xml; charset=utf-8" + end + + test "sets Content-Type as text/html when rendering *.html.builder" do + get "/content_type/implied/i_am_html_builder" + + assert_header "Content-Type", "text/html; charset=utf-8" + end + + test "sets Content-Type as application/xml when rendering *.xml.builder" do + get "/content_type/implied/i_am_xml_builder", params: { "format" => "xml" } + + assert_header "Content-Type", "application/xml; charset=utf-8" + end + end + + class ExplicitCharsetTest < Rack::TestCase + test "setting the charset of the response directly on the response object" do + get "/content_type/charset/set_on_response_obj" + + assert_body "Hello world!" + assert_header "Content-Type", "text/plain; charset=utf-16" + end + + test "setting the charset of the response as nil directly on the response object" do + get "/content_type/charset/set_as_nil_on_response_obj" + + assert_body "Hello world!" + assert_header "Content-Type", "text/plain; charset=utf-8" + end + end +end diff --git a/actionpack/test/controller/new_base/middleware_test.rb b/actionpack/test/controller/new_base/middleware_test.rb new file mode 100644 index 0000000000..df69650a7b --- /dev/null +++ b/actionpack/test/controller/new_base/middleware_test.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module MiddlewareTest + class MyMiddleware + def initialize(app) + @app = app + end + + def call(env) + result = @app.call(env) + result[1]["Middleware-Test"] = "Success" + result[1]["Middleware-Order"] = "First" + result + end + end + + class ExclaimerMiddleware + def initialize(app) + @app = app + end + + def call(env) + result = @app.call(env) + result[1]["Middleware-Order"] += "!" + result + end + end + + class BlockMiddleware + attr_accessor :configurable_message + def initialize(app, &block) + @app = app + yield(self) if block_given? + end + + def call(env) + result = @app.call(env) + result[1]["Configurable-Message"] = configurable_message + result + end + end + + class MyController < ActionController::Metal + use BlockMiddleware do |config| + config.configurable_message = "Configured by block." + end + use MyMiddleware + middleware.insert_before MyMiddleware, ExclaimerMiddleware + + def index + self.response_body = "Hello World" + end + end + + class InheritedController < MyController + end + + class ActionsController < ActionController::Metal + use MyMiddleware, only: :show + middleware.insert_before MyMiddleware, ExclaimerMiddleware, except: :index + + def index + self.response_body = "index" + end + + def show + self.response_body = "show" + end + end + + class TestMiddleware < ActiveSupport::TestCase + def setup + @app = MyController.action(:index) + end + + test "middleware that is 'use'd is called as part of the Rack application" do + result = @app.call(env_for("/")) + assert_equal ["Hello World"], [].tap { |a| result[2].each { |x| a << x } } + assert_equal "Success", result[1]["Middleware-Test"] + end + + test "the middleware stack is exposed as 'middleware' in the controller" do + result = @app.call(env_for("/")) + assert_equal "First!", result[1]["Middleware-Order"] + end + + test "middleware stack accepts block arguments" do + result = @app.call(env_for("/")) + assert_equal "Configured by block.", result[1]["Configurable-Message"] + end + + test "middleware stack accepts only and except as options" do + result = ActionsController.action(:show).call(env_for("/")) + assert_equal "First!", result[1]["Middleware-Order"] + + result = ActionsController.action(:index).call(env_for("/")) + assert_nil result[1]["Middleware-Order"] + end + + def env_for(url) + Rack::MockRequest.env_for(url) + end + end + + class TestInheritedMiddleware < TestMiddleware + def setup + @app = InheritedController.action(:index) + end + end +end diff --git a/actionpack/test/controller/new_base/render_action_test.rb b/actionpack/test/controller/new_base/render_action_test.rb new file mode 100644 index 0000000000..33b55dc5a8 --- /dev/null +++ b/actionpack/test/controller/new_base/render_action_test.rb @@ -0,0 +1,314 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module RenderAction + # This has no layout and it works + class BasicController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new( + "render_action/basic/hello_world.html.erb" => "Hello world!" + )] + + def hello_world + render action: "hello_world" + end + + def hello_world_as_string + render "hello_world" + end + + def hello_world_as_string_with_options + render "hello_world", status: 404 + end + + def hello_world_as_symbol + render :hello_world + end + + def hello_world_with_symbol + render action: :hello_world + end + + def hello_world_with_layout + render action: "hello_world", layout: true + end + + def hello_world_with_layout_false + render action: "hello_world", layout: false + end + + def hello_world_with_layout_nil + render action: "hello_world", layout: nil + end + + def hello_world_with_custom_layout + render action: "hello_world", layout: "greetings" + end + end + + class RenderActionTest < Rack::TestCase + test "rendering an action using :action => <String>" do + get "/render_action/basic/hello_world" + + assert_body "Hello world!" + assert_status 200 + end + + test "rendering an action using '<action>'" do + get "/render_action/basic/hello_world_as_string" + + assert_body "Hello world!" + assert_status 200 + end + + test "rendering an action using '<action>' and options" do + get "/render_action/basic/hello_world_as_string_with_options" + + assert_body "Hello world!" + assert_status 404 + end + + test "rendering an action using :action" do + get "/render_action/basic/hello_world_as_symbol" + + assert_body "Hello world!" + assert_status 200 + end + + test "rendering an action using :action => :hello_world" do + get "/render_action/basic/hello_world_with_symbol" + + assert_body "Hello world!" + assert_status 200 + end + end + + class RenderLayoutTest < Rack::TestCase + def setup + end + + test "rendering with layout => true" do + assert_raise(ArgumentError) do + get "/render_action/basic/hello_world_with_layout", headers: { "action_dispatch.show_exceptions" => false } + end + end + + test "rendering with layout => false" do + get "/render_action/basic/hello_world_with_layout_false" + + assert_body "Hello world!" + assert_status 200 + end + + test "rendering with layout => :nil" do + get "/render_action/basic/hello_world_with_layout_nil" + + assert_body "Hello world!" + assert_status 200 + end + + test "rendering with layout => 'greetings'" do + assert_raise(ActionView::MissingTemplate) do + get "/render_action/basic/hello_world_with_custom_layout", headers: { "action_dispatch.show_exceptions" => false } + end + end + end +end + +module RenderActionWithApplicationLayout + # # ==== Render actions with layouts ==== + class BasicController < ::ApplicationController + # Set the view path to an application view structure with layouts + self.view_paths = [ActionView::FixtureResolver.new( + "render_action_with_application_layout/basic/hello_world.html.erb" => "Hello World!", + "render_action_with_application_layout/basic/hello.html.builder" => "xml.p 'Hello'", + "layouts/application.html.erb" => "Hi <%= yield %> OK, Bye", + "layouts/greetings.html.erb" => "Greetings <%= yield %> Bye", + "layouts/builder.html.builder" => "xml.html do\n xml << yield\nend" + )] + + def hello_world + render action: "hello_world" + end + + def hello_world_with_layout + render action: "hello_world", layout: true + end + + def hello_world_with_layout_false + render action: "hello_world", layout: false + end + + def hello_world_with_layout_nil + render action: "hello_world", layout: nil + end + + def hello_world_with_custom_layout + render action: "hello_world", layout: "greetings" + end + + def with_builder_and_layout + render action: "hello", layout: "builder" + end + end + + class LayoutTest < Rack::TestCase + test "rendering implicit application.html.erb as layout" do + get "/render_action_with_application_layout/basic/hello_world" + + assert_body "Hi Hello World! OK, Bye" + assert_status 200 + end + + test "rendering with layout => true" do + get "/render_action_with_application_layout/basic/hello_world_with_layout" + + assert_body "Hi Hello World! OK, Bye" + assert_status 200 + end + + test "rendering with layout => false" do + get "/render_action_with_application_layout/basic/hello_world_with_layout_false" + + assert_body "Hello World!" + assert_status 200 + end + + test "rendering with layout => :nil" do + get "/render_action_with_application_layout/basic/hello_world_with_layout_nil" + + assert_body "Hello World!" + assert_status 200 + end + + test "rendering with layout => 'greetings'" do + get "/render_action_with_application_layout/basic/hello_world_with_custom_layout" + + assert_body "Greetings Hello World! Bye" + assert_status 200 + end + end + + class TestLayout < Rack::TestCase + testing BasicController + + test "builder works with layouts" do + get :with_builder_and_layout + assert_response "<html>\n<p>Hello</p>\n</html>\n" + end + end +end + +module RenderActionWithControllerLayout + class BasicController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new( + "render_action_with_controller_layout/basic/hello_world.html.erb" => "Hello World!", + "layouts/render_action_with_controller_layout/basic.html.erb" => "With Controller Layout! <%= yield %> Bye" + )] + + def hello_world + render action: "hello_world" + end + + def hello_world_with_layout + render action: "hello_world", layout: true + end + + def hello_world_with_layout_false + render action: "hello_world", layout: false + end + + def hello_world_with_layout_nil + render action: "hello_world", layout: nil + end + + def hello_world_with_custom_layout + render action: "hello_world", layout: "greetings" + end + end + + class ControllerLayoutTest < Rack::TestCase + test "render hello_world and implicitly use <controller_path>.html.erb as a layout." do + get "/render_action_with_controller_layout/basic/hello_world" + + assert_body "With Controller Layout! Hello World! Bye" + assert_status 200 + end + + test "rendering with layout => true" do + get "/render_action_with_controller_layout/basic/hello_world_with_layout" + + assert_body "With Controller Layout! Hello World! Bye" + assert_status 200 + end + + test "rendering with layout => false" do + get "/render_action_with_controller_layout/basic/hello_world_with_layout_false" + + assert_body "Hello World!" + assert_status 200 + end + + test "rendering with layout => :nil" do + get "/render_action_with_controller_layout/basic/hello_world_with_layout_nil" + + assert_body "Hello World!" + assert_status 200 + end + end +end + +module RenderActionWithBothLayouts + class BasicController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new( + "render_action_with_both_layouts/basic/hello_world.html.erb" => "Hello World!", + "layouts/application.html.erb" => "Oh Hi <%= yield %> Bye", + "layouts/render_action_with_both_layouts/basic.html.erb" => "With Controller Layout! <%= yield %> Bye")] + + def hello_world + render action: "hello_world" + end + + def hello_world_with_layout + render action: "hello_world", layout: true + end + + def hello_world_with_layout_false + render action: "hello_world", layout: false + end + + def hello_world_with_layout_nil + render action: "hello_world", layout: nil + end + end + + class ControllerLayoutTest < Rack::TestCase + test "rendering implicitly use <controller_path>.html.erb over application.html.erb as a layout" do + get "/render_action_with_both_layouts/basic/hello_world" + + assert_body "With Controller Layout! Hello World! Bye" + assert_status 200 + end + + test "rendering with layout => true" do + get "/render_action_with_both_layouts/basic/hello_world_with_layout" + + assert_body "With Controller Layout! Hello World! Bye" + assert_status 200 + end + + test "rendering with layout => false" do + get "/render_action_with_both_layouts/basic/hello_world_with_layout_false" + + assert_body "Hello World!" + assert_status 200 + end + + test "rendering with layout => :nil" do + get "/render_action_with_both_layouts/basic/hello_world_with_layout_nil" + + assert_body "Hello World!" + assert_status 200 + end + end +end diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb new file mode 100644 index 0000000000..d0b61f0665 --- /dev/null +++ b/actionpack/test/controller/new_base/render_body_test.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module RenderBody + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + + def index + render body: "Hello World!" + end + end + + class SimpleController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new] + + def index + render body: "hello david" + end + end + + class WithLayoutController < ::ApplicationController + self.view_paths = [ActionView::FixtureResolver.new( + "layouts/application.erb" => "<%= yield %>, I'm here!", + "layouts/greetings.erb" => "<%= yield %>, I wish thee well.", + "layouts/ivar.erb" => "<%= yield %>, <%= @ivar %>" + )] + + def index + render body: "hello david" + end + + def custom_code + render body: "hello world", status: 404 + end + + def with_custom_code_as_string + render body: "hello world", status: "404 Not Found" + end + + def with_nil + render body: nil + end + + def with_nil_and_status + render body: nil, status: 403 + end + + def with_false + render body: false + end + + def with_layout_true + render body: "hello world", layout: true + end + + def with_layout_false + render body: "hello world", layout: false + end + + def with_layout_nil + render body: "hello world", layout: nil + end + + def with_custom_layout + render body: "hello world", layout: "greetings" + end + + def with_custom_content_type + response.headers["Content-Type"] = "application/json" + render body: '["troll","face"]' + end + + def with_ivar_in_layout + @ivar = "hello world" + render body: "hello world", layout: "ivar" + end + end + + class RenderBodyTest < Rack::TestCase + test "rendering body from a minimal controller" do + get "/render_body/minimal/index" + assert_body "Hello World!" + assert_status 200 + end + + test "rendering body from an action with default options renders the body with the layout" do + with_routing do |set| + set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } } + + get "/render_body/simple" + assert_body "hello david" + assert_status 200 + end + end + + test "rendering body from an action with default options renders the body without the layout" do + with_routing do |set| + set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } } + + get "/render_body/with_layout" + + assert_body "hello david" + assert_status 200 + end + end + + test "rendering body, while also providing a custom status code" do + get "/render_body/with_layout/custom_code" + + assert_body "hello world" + assert_status 404 + end + + test "rendering body with nil returns an empty body" do + get "/render_body/with_layout/with_nil" + + assert_body "" + assert_status 200 + end + + test "Rendering body with nil and custom status code returns an empty body and the status" do + get "/render_body/with_layout/with_nil_and_status" + + assert_body "" + assert_status 403 + end + + test "rendering body with false returns the string 'false'" do + get "/render_body/with_layout/with_false" + + assert_body "false" + assert_status 200 + end + + test "rendering body with layout: true" do + get "/render_body/with_layout/with_layout_true" + + assert_body "hello world, I'm here!" + assert_status 200 + end + + test "rendering body with layout: 'greetings'" do + get "/render_body/with_layout/with_custom_layout" + + assert_body "hello world, I wish thee well." + assert_status 200 + end + + test "specified content type should not be removed" do + get "/render_body/with_layout/with_custom_content_type" + + assert_equal %w{ troll face }, JSON.parse(response.body) + assert_equal "application/json", response.headers["Content-Type"] + end + + test "rendering body with layout: false" do + get "/render_body/with_layout/with_layout_false" + + assert_body "hello world" + assert_status 200 + end + + test "rendering body with layout: nil" do + get "/render_body/with_layout/with_layout_nil" + + assert_body "hello world" + assert_status 200 + end + end +end diff --git a/actionpack/test/controller/new_base/render_context_test.rb b/actionpack/test/controller/new_base/render_context_test.rb new file mode 100644 index 0000000000..5e570a1d79 --- /dev/null +++ b/actionpack/test/controller/new_base/render_context_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "abstract_unit" + +# This is testing the decoupling of view renderer and view context +# by allowing the controller to be used as view context. This is +# similar to the way sinatra renders templates. +module RenderContext + class BasicController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new( + "render_context/basic/hello_world.html.erb" => "<%= @value %> from <%= self.__controller_method__ %>", + "layouts/basic.html.erb" => "?<%= yield %>?" + )] + + # 1) Include ActionView::Context to bring the required dependencies + include ActionView::Context + + # 2) Call _prepare_context that will do the required initialization + before_action :_prepare_context + + def hello_world + @value = "Hello" + render action: "hello_world", layout: false + end + + def with_layout + @value = "Hello" + render action: "hello_world", layout: "basic" + end + + protected def __controller_method__ + "controller context!" + end + + private + # 3) Set view_context to self + def view_context + self + end + end + + class RenderContextTest < Rack::TestCase + test "rendering using the controller as context" do + get "/render_context/basic/hello_world" + assert_body "Hello from controller context!" + assert_status 200 + end + + test "rendering using the controller as context with layout" do + get "/render_context/basic/with_layout" + assert_body "?Hello from controller context!?" + assert_status 200 + end + end +end diff --git a/actionpack/test/controller/new_base/render_file_test.rb b/actionpack/test/controller/new_base/render_file_test.rb new file mode 100644 index 0000000000..de8af029e0 --- /dev/null +++ b/actionpack/test/controller/new_base/render_file_test.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module RenderFile + class BasicController < ActionController::Base + self.view_paths = __dir__ + + def index + render file: File.expand_path("../../fixtures/test/hello_world", __dir__) + end + + def with_instance_variables + @secret = "in the sauce" + render file: File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__) + end + + def relative_path + @secret = "in the sauce" + render file: "../../fixtures/test/render_file_with_ivar" + end + + def relative_path_with_dot + @secret = "in the sauce" + render file: "../../fixtures/test/dot.directory/render_file_with_ivar" + end + + def pathname + @secret = "in the sauce" + render file: Pathname.new(__dir__).join(*%w[.. .. fixtures test dot.directory render_file_with_ivar]) + end + + def with_locals + path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__) + render file: path, locals: { secret: "in the sauce" } + end + end + + class TestBasic < Rack::TestCase + testing RenderFile::BasicController + + test "rendering simple template" do + get :index + assert_response "Hello world!" + end + + test "rendering template with ivar" do + get :with_instance_variables + assert_response "The secret is in the sauce\n" + end + + test "rendering a relative path" do + get :relative_path + assert_response "The secret is in the sauce\n" + end + + test "rendering a relative path with dot" do + get :relative_path_with_dot + assert_response "The secret is in the sauce\n" + end + + test "rendering a Pathname" do + get :pathname + assert_response "The secret is in the sauce\n" + end + + test "rendering file with locals" do + get :with_locals + assert_response "The secret is in the sauce\n" + end + end +end diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb new file mode 100644 index 0000000000..4bea2ba2e9 --- /dev/null +++ b/actionpack/test/controller/new_base/render_html_test.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module RenderHtml + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + + def index + render html: "Hello World!" + end + end + + class SimpleController < ActionController::Base + self.view_paths = [ActionView::FixtureResolver.new] + + def index + render html: "hello david" + end + end + + class WithLayoutController < ::ApplicationController + self.view_paths = [ActionView::FixtureResolver.new( + "layouts/application.html.erb" => "<%= yield %>, I'm here!", + "layouts/greetings.html.erb" => "<%= yield %>, I wish thee well.", + "layouts/ivar.html.erb" => "<%= yield %>, <%= @ivar %>" + )] + + def index + render html: "hello david" + end + + def custom_code + render html: "hello world", status: 404 + end + + def with_custom_code_as_string + render html: "hello world", status: "404 Not Found" + end + + def with_nil + render html: nil + end + + def with_nil_and_status + render html: nil, status: 403 + end + + def with_false + render html: false + end + + def with_layout_true + render html: "hello world", layout: true + end + + def with_layout_false + render html: "hello world", layout: false + end + + def with_layout_nil + render html: "hello world", layout: nil + end + + def with_custom_layout + render html: "hello world", layout: "greetings" + end + + def with_ivar_in_layout + @ivar = "hello world" + render html: "hello world", layout: "ivar" + end + + def with_unsafe_html_tag + render html: "<p>hello world</p>", layout: nil + end + + def with_safe_html_tag + render html: "<p>hello world</p>".html_safe, layout: nil + end + end + + class RenderHtmlTest < Rack::TestCase + test "rendering text from a minimal controller" do + get "/render_html/minimal/index" + assert_body "Hello World!" + assert_status 200 + end + + test "rendering text from an action with default options renders the text with the layout" do + with_routing do |set| + set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } } + + get "/render_html/simple" + assert_body "hello david" + assert_status 200 + end + end + + test "rendering text from an action with default options renders the text without the layout" do + with_routing do |set| + set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } } + + get "/render_html/with_layout" + + assert_body "hello david" + assert_status 200 + end + end + + test "rendering text, while also providing a custom status code" do + get "/render_html/with_layout/custom_code" + + assert_body "hello world" + assert_status 404 + end + + test "rendering text with nil returns an empty body" do + get "/render_html/with_layout/with_nil" + + assert_body "" + assert_status 200 + end + + test "Rendering text with nil and custom status code returns an empty body and the status" do + get "/render_html/with_layout/with_nil_and_status" + + assert_body "" + assert_status 403 + end + + test "rendering text with false returns the string 'false'" do + get "/render_html/with_layout/with_false" + + assert_body "false" + assert_status 200 + end + + test "rendering text with layout: true" do + get "/render_html/with_layout/with_layout_true" + + assert_body "hello world, I'm here!" + assert_status 200 + end + + test "rendering text with layout: 'greetings'" do + get "/render_html/with_layout/with_custom_layout" + + assert_body "hello world, I wish thee well." + assert_status 200 + end + + test "rendering text with layout: false" do + get "/render_html/with_layout/with_layout_false" + + assert_body "hello world" + assert_status 200 + end + + test "rendering text with layout: nil" do + get "/render_html/with_layout/with_layout_nil" + + assert_body "hello world" + assert_status 200 + end + + test "rendering html should escape the string if it is not html safe" do + get "/render_html/with_layout/with_unsafe_html_tag" + + assert_body "<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..d683bc73e6 --- /dev/null +++ b/actionpack/test/controller/output_escaping_test.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class OutputEscapingTest < ActiveSupport::TestCase + test "escape_html shouldn't die when passed nil" do + assert_predicate ERB::Util.h(nil), :blank? + end + + test "escapeHTML should escape strings" do + assert_equal "<>"", ERB::Util.h("<>\"") + end + + test "escapeHTML shouldn't touch explicitly safe strings" do + assert_equal "<", ERB::Util.h("<".html_safe) + end +end diff --git a/actionpack/test/controller/parameter_encoding_test.rb b/actionpack/test/controller/parameter_encoding_test.rb new file mode 100644 index 0000000000..e2194e8974 --- /dev/null +++ b/actionpack/test/controller/parameter_encoding_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ParameterEncodingController < ActionController::Base + skip_parameter_encoding :test_bar + skip_parameter_encoding :test_all_values_encoding + + def test_foo + render body: params[:foo].encoding + end + + def test_bar + render body: params[:bar].encoding + end + + def test_all_values_encoding + render body: ::JSON.dump(params.values.map(&:encoding).map(&:name)) + end +end + +class ParameterEncodingTest < ActionController::TestCase + tests ParameterEncodingController + + test "properly transcodes UTF8 parameters into declared encodings" do + post :test_foo, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" } + + assert_response :success + assert_equal "UTF-8", @response.body + end + + test "properly encodes ASCII_8BIT parameters into binary" do + post :test_bar, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" } + + assert_response :success + assert_equal "ASCII-8BIT", @response.body + end + + test "properly encodes all ASCII_8BIT parameters into binary" do + post :test_all_values_encoding, params: { "foo" => "foo", "bar" => "bar", "baz" => "baz" } + + assert_response :success + assert_equal ["ASCII-8BIT"], JSON.parse(@response.body).uniq + end + + test "does not raise an error when passed a param declared as ASCII-8BIT that contains invalid bytes" do + get :test_bar, params: { "bar" => URI.parser.escape("bar\xE2baz".b) } + + assert_response :success + assert_equal "ASCII-8BIT", @response.body + end +end diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb new file mode 100644 index 0000000000..7789e654d5 --- /dev/null +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -0,0 +1,338 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" + +class ParametersAccessorsTest < ActiveSupport::TestCase + setup do + ActionController::Parameters.permit_all_parameters = false + + @params = ActionController::Parameters.new( + person: { + age: "32", + name: { + first: "David", + last: "Heinemeier Hansson" + }, + addresses: [{ city: "Chicago", state: "Illinois" }] + } + ) + end + + test "[] retains permitted status" do + @params.permit! + assert_predicate @params[:person], :permitted? + assert_predicate @params[:person][:name], :permitted? + end + + test "[] retains unpermitted status" do + assert_not_predicate @params[:person], :permitted? + assert_not_predicate @params[:person][:name], :permitted? + end + + test "as_json returns the JSON representation of the parameters hash" do + assert_not @params.as_json.key? "parameters" + assert_not @params.as_json.key? "permitted" + assert @params.as_json.key? "person" + end + + test "to_s returns the string representation of the parameters hash" do + assert_equal '{"person"=>{"age"=>"32", "name"=>{"first"=>"David", "last"=>"Heinemeier Hansson"}, ' \ + '"addresses"=>[{"city"=>"Chicago", "state"=>"Illinois"}]}}', @params.to_s + end + + test "each carries permitted status" do + @params.permit! + @params.each { |key, value| assert(value.permitted?) if key == "person" } + end + + test "each carries unpermitted status" do + @params.each { |key, value| assert_not(value.permitted?) if key == "person" } + end + + test "each returns key,value array for block with arity 1" do + @params.each do |arg| + assert_kind_of Array, arg + assert_equal "person", arg[0] + assert_kind_of ActionController::Parameters, arg[1] + end + end + + test "each_pair carries permitted status" do + @params.permit! + @params.each_pair { |key, value| assert(value.permitted?) if key == "person" } + end + + test "each_pair carries unpermitted status" do + @params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" } + end + + test "each_pair returns key,value array for block with arity 1" do + @params.each_pair do |arg| + assert_kind_of Array, arg + assert_equal "person", arg[0] + assert_kind_of ActionController::Parameters, arg[1] + end + end + + test "each_value carries permitted status" do + @params.permit! + @params.each_value do |value| + assert_predicate(value, :permitted?) + end + end + + test "each_value carries unpermitted status" do + @params.each_value do |value| + assert_not_predicate(value, :permitted?) + end + end + + test "each_key converts to hash for permitted" do + @params.permit! + @params.each_key { |key| assert_kind_of(String, key) if key == "person" } + end + + test "each_key converts to hash for unpermitted" do + @params.each_key { |key| assert_kind_of(String, key) if key == "person" } + end + + test "empty? returns true when params contains no key/value pairs" do + params = ActionController::Parameters.new + assert_empty params + end + + test "empty? returns false when any params are present" do + assert_not_empty @params + end + + test "except retains permitted status" do + @params.permit! + assert_predicate @params.except(:person), :permitted? + assert_predicate @params[:person].except(:name), :permitted? + end + + test "except retains unpermitted status" do + assert_not_predicate @params.except(:person), :permitted? + assert_not_predicate @params[:person].except(:name), :permitted? + end + + test "fetch retains permitted status" do + @params.permit! + assert_predicate @params.fetch(:person), :permitted? + assert_predicate @params[:person].fetch(:name), :permitted? + end + + test "fetch retains unpermitted status" do + assert_not_predicate @params.fetch(:person), :permitted? + assert_not_predicate @params[:person].fetch(:name), :permitted? + end + + test "has_key? returns true if the given key is present in the params" do + assert @params.has_key?(:person) + end + + test "has_key? returns false if the given key is not present in the params" do + assert_not @params.has_key?(:address) + end + + test "has_value? returns true if the given value is present in the params" do + params = ActionController::Parameters.new(city: "Chicago", state: "Illinois") + assert params.has_value?("Chicago") + end + + test "has_value? returns false if the given value is not present in the params" do + params = ActionController::Parameters.new(city: "Chicago", state: "Illinois") + assert_not params.has_value?("New York") + end + + test "include? returns true if the given key is present in the params" do + assert @params.include?(:person) + end + + test "include? returns false if the given key is not present in the params" do + assert_not @params.include?(:address) + end + + test "key? returns true if the given key is present in the params" do + assert @params.key?(:person) + end + + test "key? returns false if the given key is not present in the params" do + assert_not @params.key?(:address) + end + + test "keys returns an array of the keys of the params" do + assert_equal ["person"], @params.keys + assert_equal ["age", "name", "addresses"], @params[:person].keys + end + + test "reject retains permitted status" do + assert_not_predicate @params.reject { |k| k == "person" }, :permitted? + end + + test "reject retains unpermitted status" do + @params.permit! + assert_predicate @params.reject { |k| k == "person" }, :permitted? + end + + test "select retains permitted status" do + @params.permit! + assert_predicate @params.select { |k| k == "person" }, :permitted? + end + + test "select retains unpermitted status" do + assert_not_predicate @params.select { |k| k == "person" }, :permitted? + end + + test "slice retains permitted status" do + @params.permit! + assert_predicate @params.slice(:person), :permitted? + end + + test "slice retains unpermitted status" do + assert_not_predicate @params.slice(:person), :permitted? + end + + test "transform_keys retains permitted status" do + @params.permit! + assert_predicate @params.transform_keys { |k| k }, :permitted? + end + + test "transform_keys retains unpermitted status" do + assert_not_predicate @params.transform_keys { |k| k }, :permitted? + end + + test "transform_values retains permitted status" do + @params.permit! + assert_predicate @params.transform_values { |v| v }, :permitted? + end + + test "transform_values retains unpermitted status" do + assert_not_predicate @params.transform_values { |v| v }, :permitted? + end + + test "transform_values converts hashes to parameters" do + @params.transform_values do |value| + assert_kind_of ActionController::Parameters, value + value + end + end + + test "transform_values without block yieds an enumerator" do + assert_kind_of Enumerator, @params.transform_values + end + + test "transform_values! converts hashes to parameters" do + @params.transform_values! do |value| + assert_kind_of ActionController::Parameters, value + end + end + + test "transform_values! without block yields an enumerator" do + assert_kind_of Enumerator, @params.transform_values! + end + + test "value? returns true if the given value is present in the params" do + params = ActionController::Parameters.new(city: "Chicago", state: "Illinois") + assert params.value?("Chicago") + end + + test "value? returns false if the given value is not present in the params" do + params = ActionController::Parameters.new(city: "Chicago", state: "Illinois") + assert_not params.value?("New York") + end + + test "values returns an array of the values of the params" do + params = ActionController::Parameters.new(city: "Chicago", state: "Illinois") + assert_equal ["Chicago", "Illinois"], params.values + end + + test "values_at retains permitted status" do + @params.permit! + assert_predicate @params.values_at(:person).first, :permitted? + assert_predicate @params[:person].values_at(:name).first, :permitted? + end + + test "values_at retains unpermitted status" do + assert_not_predicate @params.values_at(:person).first, :permitted? + assert_not_predicate @params[:person].values_at(:name).first, :permitted? + end + + test "is equal to Parameters instance with same params" do + params1 = ActionController::Parameters.new(a: 1, b: 2) + params2 = ActionController::Parameters.new(a: 1, b: 2) + assert(params1 == params2) + end + + test "is equal to Parameters instance with same permitted params" do + params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + assert(params1 == params2) + end + + test "is equal to Parameters instance with same different source params, but same permitted params" do + params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + params2 = ActionController::Parameters.new(a: 1, c: 3).permit(:a) + assert(params1 == params2) + assert(params2 == params1) + end + + test "is not equal to an unpermitted Parameters instance with same params" do + params1 = ActionController::Parameters.new(a: 1).permit(:a) + params2 = ActionController::Parameters.new(a: 1) + assert(params1 != params2) + assert(params2 != params1) + end + + test "is not equal to Parameters instance with different permitted params" do + params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a, :b) + params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a) + assert(params1 != params2) + assert(params2 != params1) + end + + test "equality with simple types works" do + assert(@params != "Hello") + assert(@params != 42) + assert(@params != false) + end + + test "inspect shows both class name, parameters and permitted flag" do + assert_equal( + '<ActionController::Parameters {"person"=>{"age"=>"32", '\ + '"name"=>{"first"=>"David", "last"=>"Heinemeier Hansson"}, ' \ + '"addresses"=>[{"city"=>"Chicago", "state"=>"Illinois"}]}} permitted: false>', + @params.inspect + ) + end + + test "inspect prints updated permitted flag in the output" do + assert_match(/permitted: false/, @params.inspect) + + @params.permit! + + assert_match(/permitted: true/, @params.inspect) + end + + test "#dig delegates the dig method to its values" do + assert_equal "David", @params.dig(:person, :name, :first) + assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city) + end + + test "#dig converts hashes to parameters" do + assert_kind_of ActionController::Parameters, @params.dig(:person) + assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0) + assert @params.dig(:person, :addresses).all? do |value| + value.is_a?(ActionController::Parameters) + end + end + + test "mutating #dig return value mutates underlying parameters" do + @params.dig(:person, :name)[:first] = "Bill" + assert_equal "Bill", @params.dig(:person, :name, :first) + + @params.dig(:person, :addresses)[0] = { city: "Boston", state: "Massachusetts" } + assert_equal "Boston", @params.dig(:person, :addresses, 0, :city) + end +end diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb new file mode 100644 index 0000000000..974612fb7b --- /dev/null +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" + +class AlwaysPermittedParametersTest < ActiveSupport::TestCase + def setup + ActionController::Parameters.action_on_unpermitted_parameters = :raise + ActionController::Parameters.always_permitted_parameters = %w( controller action format ) + end + + def teardown + ActionController::Parameters.action_on_unpermitted_parameters = false + ActionController::Parameters.always_permitted_parameters = %w( controller action ) + end + + test "returns super on missing constant other than NEVER_UNPERMITTED_PARAMS" do + ActionController::Parameters.superclass.stub :const_missing, "super" do + assert_equal "super", ActionController::Parameters::NON_EXISTING_CONSTANT + end + end + + test "allows both explicitly listed and always-permitted parameters" do + params = ActionController::Parameters.new( + book: { pages: 65 }, + format: "json") + permitted = params.permit book: [:pages] + assert_predicate permitted, :permitted? + end +end diff --git a/actionpack/test/controller/parameters/dup_test.rb b/actionpack/test/controller/parameters/dup_test.rb new file mode 100644 index 0000000000..5403fc6d93 --- /dev/null +++ b/actionpack/test/controller/parameters/dup_test.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" +require "active_support/core_ext/object/deep_dup" + +class ParametersDupTest < ActiveSupport::TestCase + setup do + ActionController::Parameters.permit_all_parameters = false + + @params = ActionController::Parameters.new( + person: { + age: "32", + name: { + first: "David", + last: "Heinemeier Hansson" + }, + addresses: [{ city: "Chicago", state: "Illinois" }] + } + ) + end + + test "a duplicate maintains the original's permitted status" do + @params.permit! + dupped_params = @params.dup + assert_predicate dupped_params, :permitted? + end + + test "a duplicate maintains the original's parameters" do + @params.permit! + dupped_params = @params.dup + assert_equal @params.to_h, dupped_params.to_h + end + + test "changes to a duplicate's parameters do not affect the original" do + dupped_params = @params.dup + dupped_params.delete(:person) + assert_not_equal @params, dupped_params + end + + test "changes to a duplicate's permitted status do not affect the original" do + dupped_params = @params.dup + dupped_params.permit! + assert_not_equal @params, dupped_params + end + + test "deep_dup content" do + dupped_params = @params.deep_dup + dupped_params[:person][:age] = "45" + dupped_params[:person][:addresses].clear + + assert_not_equal @params[:person][:age], dupped_params[:person][:age] + assert_not_equal @params[:person][:addresses], dupped_params[:person][:addresses] + end + + test "deep_dup @permitted" do + dupped_params = @params.deep_dup + dupped_params.permit! + + assert_not_predicate @params, :permitted? + end + + test "deep_dup @permitted is being copied" do + @params.permit! + assert_predicate @params.deep_dup, :permitted? + end +end diff --git a/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb new file mode 100644 index 0000000000..fc9229ca1d --- /dev/null +++ b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" + +class LogOnUnpermittedParamsTest < ActiveSupport::TestCase + def setup + ActionController::Parameters.action_on_unpermitted_parameters = :log + end + + def teardown + ActionController::Parameters.action_on_unpermitted_parameters = false + end + + test "logs on unexpected param" do + params = ActionController::Parameters.new( + book: { pages: 65 }, + fishing: "Turnips") + + assert_logged("Unpermitted parameter: :fishing") do + params.permit(book: [:pages]) + end + end + + test "logs on unexpected params" do + params = ActionController::Parameters.new( + book: { pages: 65 }, + fishing: "Turnips", + car: "Mersedes") + + assert_logged("Unpermitted parameters: :fishing, :car") do + params.permit(book: [:pages]) + end + end + + test "logs on unexpected nested param" do + params = ActionController::Parameters.new( + book: { pages: 65, title: "Green Cats and where to find then." }) + + assert_logged("Unpermitted parameter: :title") do + params.permit(book: [:pages]) + end + end + + test "logs on unexpected nested params" do + params = ActionController::Parameters.new( + book: { pages: 65, title: "Green Cats and where to find then.", author: "G. A. Dog" }) + + assert_logged("Unpermitted parameters: :title, :author") do + params.permit(book: [:pages]) + end + end + + private + + def assert_logged(message) + old_logger = ActionController::Base.logger + log = StringIO.new + ActionController::Base.logger = Logger.new(log) + + begin + yield + + log.rewind + assert_match message, log.read + ensure + ActionController::Base.logger = old_logger + end + end +end diff --git a/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb new file mode 100644 index 0000000000..c890839727 --- /dev/null +++ b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" + +class MultiParameterAttributesTest < ActiveSupport::TestCase + test "permitted multi-parameter attribute keys" do + params = ActionController::Parameters.new( + book: { + "shipped_at(1i)" => "2012", + "shipped_at(2i)" => "3", + "shipped_at(3i)" => "25", + "shipped_at(4i)" => "10", + "shipped_at(5i)" => "15", + "published_at(1i)" => "1999", + "published_at(2i)" => "2", + "published_at(3i)" => "5", + "price(1)" => "R$", + "price(2f)" => "2.02" + }) + + permitted = params.permit book: [ :shipped_at, :price ] + + assert_predicate permitted, :permitted? + + assert_equal "2012", permitted[:book]["shipped_at(1i)"] + assert_equal "3", permitted[:book]["shipped_at(2i)"] + assert_equal "25", permitted[:book]["shipped_at(3i)"] + assert_equal "10", permitted[:book]["shipped_at(4i)"] + assert_equal "15", permitted[:book]["shipped_at(5i)"] + + assert_equal "R$", permitted[:book]["price(1)"] + assert_equal "2.02", permitted[:book]["price(2f)"] + + assert_nil permitted[:book]["published_at(1i)"] + assert_nil permitted[:book]["published_at(2i)"] + assert_nil permitted[:book]["published_at(3i)"] + end +end diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb new file mode 100644 index 0000000000..312b1e5b27 --- /dev/null +++ b/actionpack/test/controller/parameters/mutators_test.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" + +class ParametersMutatorsTest < ActiveSupport::TestCase + setup do + @params = ActionController::Parameters.new( + person: { + age: "32", + name: { + first: "David", + last: "Heinemeier Hansson" + }, + addresses: [{ city: "Chicago", state: "Illinois" }] + } + ) + end + + test "delete retains permitted status" do + @params.permit! + assert_predicate @params.delete(:person), :permitted? + end + + test "delete retains unpermitted status" do + assert_not_predicate @params.delete(:person), :permitted? + end + + test "delete returns the value when the key is present" do + assert_equal "32", @params[:person].delete(:age) + end + + test "delete removes the entry when the key present" do + @params[:person].delete(:age) + assert_not @params[:person].key?(:age) + end + + test "delete returns nil when the key is not present" do + assert_nil @params[:person].delete(:first_name) + end + + test "delete returns the value of the given block when the key is not present" do + assert_equal "David", @params[:person].delete(:first_name) { "David" } + end + + test "delete yields the key to the given block when the key is not present" do + assert_equal "first_name: David", @params[:person].delete(:first_name) { |k| "#{k}: David" } + end + + test "delete_if retains permitted status" do + @params.permit! + assert_predicate @params.delete_if { |k| k == "person" }, :permitted? + end + + test "delete_if retains unpermitted status" do + assert_not_predicate @params.delete_if { |k| k == "person" }, :permitted? + end + + test "extract! retains permitted status" do + @params.permit! + assert_predicate @params.extract!(:person), :permitted? + end + + test "extract! retains unpermitted status" do + assert_not_predicate @params.extract!(:person), :permitted? + end + + test "keep_if retains permitted status" do + @params.permit! + assert_predicate @params.keep_if { |k, v| k == "person" }, :permitted? + end + + test "keep_if retains unpermitted status" do + assert_not_predicate @params.keep_if { |k, v| k == "person" }, :permitted? + end + + test "reject! retains permitted status" do + @params.permit! + assert_predicate @params.reject! { |k| k == "person" }, :permitted? + end + + test "reject! retains unpermitted status" do + assert_not_predicate @params.reject! { |k| k == "person" }, :permitted? + end + + test "select! retains permitted status" do + @params.permit! + assert_predicate @params.select! { |k| k != "person" }, :permitted? + end + + test "select! retains unpermitted status" do + assert_not_predicate @params.select! { |k| k != "person" }, :permitted? + end + + test "slice! retains permitted status" do + @params.permit! + assert_predicate @params.slice!(:person), :permitted? + end + + test "slice! retains unpermitted status" do + assert_not_predicate @params.slice!(:person), :permitted? + end + + test "transform_keys! retains permitted status" do + @params.permit! + assert_predicate @params.transform_keys! { |k| k }, :permitted? + end + + test "transform_keys! retains unpermitted status" do + assert_not_predicate @params.transform_keys! { |k| k }, :permitted? + end + + test "transform_values! retains permitted status" do + @params.permit! + assert_predicate @params.transform_values! { |v| v }, :permitted? + end + + test "transform_values! retains unpermitted status" do + assert_not_predicate @params.transform_values! { |v| v }, :permitted? + end +end diff --git a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb new file mode 100644 index 0000000000..1403e224c0 --- /dev/null +++ b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" + +class NestedParametersPermitTest < ActiveSupport::TestCase + def assert_filtered_out(params, key) + assert_not params.has_key?(key), "key #{key.inspect} has not been filtered out" + end + + test "permitted nested parameters" do + params = ActionController::Parameters.new( + book: { + title: "Romeo and Juliet", + authors: [{ + name: "William Shakespeare", + born: "1564-04-26" + }, { + name: "Christopher Marlowe" + }, { + name: %w(malicious injected names) + }], + details: { + pages: 200, + genre: "Tragedy" + }, + id: { + isbn: "x" + } + }, + magazine: "Mjallo!") + + permitted = params.permit book: [ :title, { authors: [ :name ] }, { details: :pages }, :id ] + + assert_predicate permitted, :permitted? + assert_equal "Romeo and Juliet", permitted[:book][:title] + assert_equal "William Shakespeare", permitted[:book][:authors][0][:name] + assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name] + assert_equal 200, permitted[:book][:details][:pages] + + assert_filtered_out permitted, :magazine + assert_filtered_out permitted[:book], :id + assert_filtered_out permitted[:book][:details], :genre + assert_filtered_out permitted[:book][:authors][0], :born + assert_filtered_out permitted[:book][:authors][2], :name + end + + test "permitted nested parameters with a string or a symbol as a key" do + params = ActionController::Parameters.new( + book: { + "authors" => [ + { name: "William Shakespeare", born: "1564-04-26" }, + { name: "Christopher Marlowe" } + ] + }) + + permitted = params.permit book: [ { "authors" => [ :name ] } ] + + assert_equal "William Shakespeare", permitted[:book]["authors"][0][:name] + assert_equal "William Shakespeare", permitted[:book][:authors][0][:name] + assert_equal "Christopher Marlowe", permitted[:book]["authors"][1][:name] + assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name] + + permitted = params.permit book: [ { authors: [ :name ] } ] + + assert_equal "William Shakespeare", permitted[:book]["authors"][0][:name] + assert_equal "William Shakespeare", permitted[:book][:authors][0][:name] + assert_equal "Christopher Marlowe", permitted[:book]["authors"][1][:name] + assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name] + end + + test "nested arrays with strings" do + params = ActionController::Parameters.new( + book: { + genres: ["Tragedy"] + }) + + permitted = params.permit book: { genres: [] } + assert_equal ["Tragedy"], permitted[:book][:genres] + end + + test "permit may specify symbols or strings" do + params = ActionController::Parameters.new( + book: { + title: "Romeo and Juliet", + author: "William Shakespeare" + }, + magazine: "Shakespeare Today") + + permitted = params.permit({ book: ["title", :author] }, "magazine") + assert_equal "Romeo and Juliet", permitted[:book][:title] + assert_equal "William Shakespeare", permitted[:book][:author] + assert_equal "Shakespeare Today", permitted[:magazine] + end + + test "nested array with strings that should be hashes" do + params = ActionController::Parameters.new( + book: { + genres: ["Tragedy"] + }) + + permitted = params.permit book: { genres: :type } + assert_empty permitted[:book][:genres] + end + + test "nested array with strings that should be hashes and additional values" do + params = ActionController::Parameters.new( + book: { + title: "Romeo and Juliet", + genres: ["Tragedy"] + }) + + permitted = params.permit book: [ :title, { genres: :type } ] + assert_equal "Romeo and Juliet", permitted[:book][:title] + assert_empty permitted[:book][:genres] + end + + test "nested string that should be a hash" do + params = ActionController::Parameters.new( + book: { + genre: "Tragedy" + }) + + permitted = params.permit book: { genre: :type } + assert_nil permitted[:book][:genre] + end + + test "fields_for-style nested params" do + params = ActionController::Parameters.new( + book: { + authors_attributes: { + '0': { name: "William Shakespeare", age_of_death: "52" }, + '1': { name: "Unattributed Assistant" }, + '2': { name: %w(injected names) } + } + }) + permitted = params.permit book: { authors_attributes: [ :name ] } + + assert_not_nil permitted[:book][:authors_attributes]["0"] + assert_not_nil permitted[:book][:authors_attributes]["1"] + assert_empty permitted[:book][:authors_attributes]["2"] + assert_equal "William Shakespeare", permitted[:book][:authors_attributes]["0"][:name] + assert_equal "Unattributed Assistant", permitted[:book][:authors_attributes]["1"][:name] + + assert_equal( + { "book" => { "authors_attributes" => { "0" => { "name" => "William Shakespeare" }, "1" => { "name" => "Unattributed Assistant" }, "2" => {} } } }, + permitted.to_h + ) + + assert_filtered_out permitted[:book][:authors_attributes]["0"], :age_of_death + end + + test "fields_for-style nested params with negative numbers" do + params = ActionController::Parameters.new( + book: { + authors_attributes: { + '-1': { name: "William Shakespeare", age_of_death: "52" }, + '-2': { name: "Unattributed Assistant" } + } + }) + permitted = params.permit book: { authors_attributes: [:name] } + + assert_not_nil permitted[:book][:authors_attributes]["-1"] + assert_not_nil permitted[:book][:authors_attributes]["-2"] + assert_equal "William Shakespeare", permitted[:book][:authors_attributes]["-1"][:name] + assert_equal "Unattributed Assistant", permitted[:book][:authors_attributes]["-2"][:name] + + assert_filtered_out permitted[:book][:authors_attributes]["-1"], :age_of_death + end + + test "nested number as key" do + params = ActionController::Parameters.new( + product: { + properties: { + "0" => "prop0", + "1" => "prop1" + } + }) + params = params.require(:product).permit(properties: ["0"]) + assert_not_nil params[:properties]["0"] + assert_nil params[:properties]["1"] + assert_equal "prop0", params[:properties]["0"] + end +end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb new file mode 100644 index 0000000000..fbfe24059b --- /dev/null +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -0,0 +1,510 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_dispatch/http/upload" +require "action_controller/metal/strong_parameters" + +class ParametersPermitTest < ActiveSupport::TestCase + def assert_filtered_out(params, key) + assert_not params.has_key?(key), "key #{key.inspect} has not been filtered out" + end + + setup do + @params = ActionController::Parameters.new( + person: { + age: "32", + name: { + first: "David", + last: "Heinemeier Hansson" + }, + addresses: [{ city: "Chicago", state: "Illinois" }] + } + ) + + @struct_fields = [] + %w(0 1 12).each do |number| + ["", "i", "f"].each do |suffix| + @struct_fields << "sf(#{number}#{suffix})" + end + end + end + + def walk_permitted(params) + params.each do |k, v| + case v + when ActionController::Parameters + walk_permitted v + when Array + v.each { |x| walk_permitted v } + end + end + end + + test "iteration should not impact permit" do + hash = { "foo" => { "bar" => { "0" => { "baz" => "hello", "zot" => "1" } } } } + params = ActionController::Parameters.new(hash) + + walk_permitted params + + sanitized = params[:foo].permit(bar: [:baz]) + assert_equal({ "0" => { "baz" => "hello" } }, sanitized[:bar].to_unsafe_h) + end + + test "if nothing is permitted, the hash becomes empty" do + params = ActionController::Parameters.new(id: "1234") + permitted = params.permit + assert_predicate permitted, :permitted? + assert_empty permitted + end + + test "key: permitted scalar values" do + values = ["a", :a, nil] + values += [0, 1.0, 2**128, BigDecimal(1)] + values += [true, false] + values += [Date.today, Time.now, DateTime.now] + values += [STDOUT, StringIO.new, ActionDispatch::Http::UploadedFile.new(tempfile: __FILE__), + Rack::Test::UploadedFile.new(__FILE__)] + + values.each do |value| + params = ActionController::Parameters.new(id: value) + permitted = params.permit(:id) + if value.nil? + assert_nil permitted[:id] + else + assert_equal value, permitted[:id] + end + + @struct_fields.each do |sf| + params = ActionController::Parameters.new(sf => value) + permitted = params.permit(:sf) + if value.nil? + assert_nil permitted[sf] + else + assert_equal value, permitted[sf] + end + end + end + end + + test "key: unknown keys are filtered out" do + params = ActionController::Parameters.new(id: "1234", injected: "injected") + permitted = params.permit(:id) + assert_equal "1234", permitted[:id] + assert_filtered_out permitted, :injected + end + + test "key: arrays are filtered out" do + [[], [1], ["1"]].each do |array| + params = ActionController::Parameters.new(id: array) + permitted = params.permit(:id) + assert_filtered_out permitted, :id + + @struct_fields.each do |sf| + params = ActionController::Parameters.new(sf => array) + permitted = params.permit(:sf) + assert_filtered_out permitted, sf + end + end + end + + test "key: hashes are filtered out" do + [{}, { foo: 1 }, { foo: "bar" }].each do |hash| + params = ActionController::Parameters.new(id: hash) + permitted = params.permit(:id) + assert_filtered_out permitted, :id + + @struct_fields.each do |sf| + params = ActionController::Parameters.new(sf => hash) + permitted = params.permit(:sf) + assert_filtered_out permitted, sf + end + end + end + + test "key: non-permitted scalar values are filtered out" do + params = ActionController::Parameters.new(id: Object.new) + permitted = params.permit(:id) + assert_filtered_out permitted, :id + + @struct_fields.each do |sf| + params = ActionController::Parameters.new(sf => Object.new) + permitted = params.permit(:sf) + assert_filtered_out permitted, sf + end + end + + test "key: it is not assigned if not present in params" do + params = ActionController::Parameters.new(name: "Joe") + permitted = params.permit(:id) + assert_not permitted.has_key?(:id) + end + + test "key to empty array: empty arrays pass" do + params = ActionController::Parameters.new(id: []) + permitted = params.permit(id: []) + assert_equal [], permitted[:id] + end + + test "do not break params filtering on nil values" do + params = ActionController::Parameters.new(a: 1, b: [1, 2, 3], c: nil) + + permitted = params.permit(:a, c: [], b: []) + assert_equal 1, permitted[:a] + assert_equal [1, 2, 3], permitted[:b] + assert_nil permitted[:c] + end + + test "key to empty array: arrays of permitted scalars pass" do + [["foo"], [1], ["foo", "bar"], [1, 2, 3]].each do |array| + params = ActionController::Parameters.new(id: array) + permitted = params.permit(id: []) + assert_equal array, permitted[:id] + end + end + + test "key to empty array: permitted scalar values do not pass" do + ["foo", 1].each do |permitted_scalar| + params = ActionController::Parameters.new(id: permitted_scalar) + permitted = params.permit(id: []) + assert_filtered_out permitted, :id + end + end + + test "key to empty array: arrays of non-permitted scalar do not pass" do + [[Object.new], [[]], [[1]], [{}], [{ id: "1" }]].each do |non_permitted_scalar| + params = ActionController::Parameters.new(id: non_permitted_scalar) + permitted = params.permit(id: []) + assert_filtered_out permitted, :id + end + end + + test "key to empty hash: arbitrary hashes are permitted" do + params = ActionController::Parameters.new( + username: "fxn", + preferences: { + scheme: "Marazul", + font: { + name: "Source Code Pro", + size: 12 + }, + tabstops: [4, 8, 12, 16], + suspicious: [true, Object.new, false, /yo!/], + dubious: [{ a: :a, b: /wtf!/ }, { c: :c }], + injected: Object.new + }, + hacked: 1 # not a hash + ) + + permitted = params.permit(:username, preferences: {}, hacked: {}) + + assert_equal "fxn", permitted[:username] + assert_equal "Marazul", permitted[:preferences][:scheme] + assert_equal "Source Code Pro", permitted[:preferences][:font][:name] + assert_equal 12, permitted[:preferences][:font][:size] + assert_equal [4, 8, 12, 16], permitted[:preferences][:tabstops] + assert_equal [true, false], permitted[:preferences][:suspicious] + assert_equal :a, permitted[:preferences][:dubious][0][:a] + assert_equal :c, permitted[:preferences][:dubious][1][:c] + + assert_filtered_out permitted[:preferences][:dubious][0], :b + assert_filtered_out permitted[:preferences], :injected + assert_filtered_out permitted, :hacked + end + + test "fetch raises ParameterMissing exception" do + e = assert_raises(ActionController::ParameterMissing) do + @params.fetch :foo + end + assert_equal :foo, e.param + end + + test "fetch with a default value of a hash does not mutate the object" do + params = ActionController::Parameters.new({}) + params.fetch :foo, {} + assert_nil params[:foo] + end + + test "hashes in array values get wrapped" do + params = ActionController::Parameters.new(foo: [{}, {}]) + params[:foo].each do |hash| + assert_not_predicate hash, :permitted? + end + end + + # Strong params has an optimization to avoid looping every time you read + # a key whose value is an array and building a new object. We check that + # optimization here. + test "arrays are converted at most once" do + params = ActionController::Parameters.new(foo: [{}]) + assert_same params[:foo], params[:foo] + end + + # Strong params has an internal cache to avoid duplicated loops in the most + # common usage pattern. See the docs of the method `converted_arrays`. + # + # This test checks that if we push a hash to an array (in-place modification) + # the cache does not get fooled, the hash is still wrapped as strong params, + # and not permitted. + test "mutated arrays are detected" do + params = ActionController::Parameters.new(users: [{ id: 1 }]) + + permitted = params.permit(users: [:id]) + permitted[:users] << { injected: 1 } + assert_not_predicate permitted[:users].last, :permitted? + end + + test "fetch doesnt raise ParameterMissing exception if there is a default" do + assert_equal "monkey", @params.fetch(:foo, "monkey") + assert_equal "monkey", @params.fetch(:foo) { "monkey" } + end + + test "fetch doesnt raise ParameterMissing exception if there is a default that is nil" do + assert_nil @params.fetch(:foo, nil) + assert_nil @params.fetch(:foo) { nil } + end + + test "KeyError in fetch block should not be covered up" do + params = ActionController::Parameters.new + e = assert_raises(KeyError) do + params.fetch(:missing_key) { {}.fetch(:also_missing) } + end + assert_match(/:also_missing$/, e.message) + end + + test "not permitted is sticky beyond merges" do + assert_not_predicate @params.merge(a: "b"), :permitted? + end + + test "permitted is sticky beyond merges" do + @params.permit! + assert_predicate @params.merge(a: "b"), :permitted? + end + + test "merge with parameters" do + other_params = ActionController::Parameters.new(id: "1234").permit! + merged_params = @params.merge(other_params) + + assert merged_params[:id] + end + + test "not permitted is sticky beyond merge!" do + assert_not_predicate @params.merge!(a: "b"), :permitted? + end + + test "permitted is sticky beyond merge!" do + @params.permit! + assert_predicate @params.merge!(a: "b"), :permitted? + end + + test "merge! with parameters" do + other_params = ActionController::Parameters.new(id: "1234").permit! + @params.merge!(other_params) + + assert_equal "1234", @params[:id] + assert_equal "32", @params[:person][:age] + end + + test "#reverse_merge with parameters" do + default_params = ActionController::Parameters.new(id: "1234", person: {}).permit! + merged_params = @params.reverse_merge(default_params) + + assert_equal "1234", merged_params[:id] + assert_not_predicate merged_params[:person], :empty? + end + + test "#with_defaults is an alias of reverse_merge" do + default_params = ActionController::Parameters.new(id: "1234", person: {}).permit! + merged_params = @params.with_defaults(default_params) + + assert_equal "1234", merged_params[:id] + assert_not_predicate merged_params[:person], :empty? + end + + test "not permitted is sticky beyond reverse_merge" do + assert_not_predicate @params.reverse_merge(a: "b"), :permitted? + end + + test "permitted is sticky beyond reverse_merge" do + @params.permit! + assert_predicate @params.reverse_merge(a: "b"), :permitted? + end + + test "#reverse_merge! with parameters" do + default_params = ActionController::Parameters.new(id: "1234", person: {}).permit! + @params.reverse_merge!(default_params) + + assert_equal "1234", @params[:id] + assert_not_predicate @params[:person], :empty? + end + + test "#with_defaults! is an alias of reverse_merge!" do + default_params = ActionController::Parameters.new(id: "1234", person: {}).permit! + @params.with_defaults!(default_params) + + assert_equal "1234", @params[:id] + assert_not_predicate @params[:person], :empty? + end + + test "modifying the parameters" do + @params[:person][:hometown] = "Chicago" + @params[:person][:family] = { brother: "Jonas" } + + assert_equal "Chicago", @params[:person][:hometown] + assert_equal "Jonas", @params[:person][:family][:brother] + end + + test "permit! is recursive" do + @params[:nested_array] = [[{ x: 2, y: 3 }, { x: 21, y: 42 }]] + @params.permit! + assert_predicate @params, :permitted? + assert_predicate @params[:person], :permitted? + assert_predicate @params[:person][:name], :permitted? + assert_predicate @params[:person][:addresses][0], :permitted? + assert_predicate @params[:nested_array][0][0], :permitted? + assert_predicate @params[:nested_array][0][1], :permitted? + end + + test "permitted takes a default value when Parameters.permit_all_parameters is set" do + ActionController::Parameters.permit_all_parameters = true + params = ActionController::Parameters.new(person: { + age: "32", name: { first: "David", last: "Heinemeier Hansson" } + }) + + assert_predicate params.slice(:person), :permitted? + assert_predicate params[:person][:name], :permitted? + ensure + ActionController::Parameters.permit_all_parameters = false + end + + test "permitting parameters as an array" do + assert_equal "32", @params[:person].permit([ :age ])[:age] + end + + test "to_h raises UnfilteredParameters on unfiltered params" do + assert_raises(ActionController::UnfilteredParameters) do + @params.to_h + end + end + + test "to_h returns converted hash on permitted params" do + @params.permit! + + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @params.to_h + assert_not_kind_of ActionController::Parameters, @params.to_h + end + + test "to_h returns converted hash when .permit_all_parameters is set" do + ActionController::Parameters.permit_all_parameters = true + params = ActionController::Parameters.new(crab: "Senjougahara Hitagi") + + assert_instance_of ActiveSupport::HashWithIndifferentAccess, params.to_h + assert_not_kind_of ActionController::Parameters, params.to_h + assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_h) + ensure + ActionController::Parameters.permit_all_parameters = false + end + + test "to_hash raises UnfilteredParameters on unfiltered params" do + assert_raises(ActionController::UnfilteredParameters) do + @params.to_hash + end + end + + test "to_hash returns converted hash on permitted params" do + @params.permit! + + assert_instance_of Hash, @params.to_hash + assert_not_kind_of ActionController::Parameters, @params.to_hash + end + + test "parameters can be implicit converted to Hash" do + params = ActionController::Parameters.new + params.permit! + + assert_equal({ a: 1 }, { a: 1 }.merge!(params)) + end + + test "to_hash returns converted hash when .permit_all_parameters is set" do + ActionController::Parameters.permit_all_parameters = true + params = ActionController::Parameters.new(crab: "Senjougahara Hitagi") + + assert_instance_of Hash, params.to_hash + assert_not_kind_of ActionController::Parameters, params.to_hash + assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_hash) + assert_equal({ "crab" => "Senjougahara Hitagi" }, params) + ensure + ActionController::Parameters.permit_all_parameters = false + end + + test "to_unsafe_h returns unfiltered params" do + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @params.to_unsafe_h + assert_not_kind_of ActionController::Parameters, @params.to_unsafe_h + end + + test "to_unsafe_h returns unfiltered params even after accessing few keys" do + params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] }) + expected = { "f" => { "language_facet" => ["Tibetan"] } } + + assert_instance_of ActionController::Parameters, params["f"] + assert_equal expected, params.to_unsafe_h + end + + test "to_unsafe_h does not mutate the parameters" do + params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] }) + params[:f] + + params.to_unsafe_h + + assert_not_predicate params, :permitted? + assert_not_predicate params[:f], :permitted? + end + + test "to_h only deep dups Ruby collections" do + company = Class.new do + attr_reader :dupped + def dup; @dupped = true; end + end.new + + params = ActionController::Parameters.new(prem: { likes: %i( dancing ) }) + assert_equal({ "prem" => { "likes" => %i( dancing ) } }, params.permit!.to_h) + + params = ActionController::Parameters.new(companies: [ company, :acme ]) + assert_equal({ "companies" => [ company, :acme ] }, params.permit!.to_h) + assert_not company.dupped + end + + test "to_unsafe_h only deep dups Ruby collections" do + company = Class.new do + attr_reader :dupped + def dup; @dupped = true; end + end.new + + params = ActionController::Parameters.new(prem: { likes: %i( dancing ) }) + assert_equal({ "prem" => { "likes" => %i( dancing ) } }, params.to_unsafe_h) + + params = ActionController::Parameters.new(companies: [ company, :acme ]) + assert_equal({ "companies" => [ company, :acme ] }, params.to_unsafe_h) + assert_not company.dupped + end + + test "include? returns true when the key is present" do + assert @params.include? :person + assert @params.include? "person" + assert_not @params.include? :gorilla + end + + test "scalar values should be filtered when array or hash is specified" do + params = ActionController::Parameters.new(foo: "bar") + + assert params.permit(:foo).has_key?(:foo) + assert_not params.permit(foo: []).has_key?(:foo) + assert_not params.permit(foo: [:bar]).has_key?(:foo) + assert_not params.permit(foo: :bar).has_key?(:foo) + end + + test "#permitted? is false by default" do + params = ActionController::Parameters.new + + assert_equal false, params.permitted? + end +end diff --git a/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb new file mode 100644 index 0000000000..4afd3da593 --- /dev/null +++ b/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" + +class RaiseOnUnpermittedParamsTest < ActiveSupport::TestCase + def setup + ActionController::Parameters.action_on_unpermitted_parameters = :raise + end + + def teardown + ActionController::Parameters.action_on_unpermitted_parameters = false + end + + test "raises on unexpected params" do + params = ActionController::Parameters.new( + book: { pages: 65 }, + fishing: "Turnips") + + assert_raises(ActionController::UnpermittedParameters) do + params.permit(book: [:pages]) + end + end + + test "raises on unexpected nested params" do + params = ActionController::Parameters.new( + book: { pages: 65, title: "Green Cats and where to find then." }) + + assert_raises(ActionController::UnpermittedParameters) do + params.permit(book: [:pages]) + end + end +end diff --git a/actionpack/test/controller/parameters/serialization_test.rb b/actionpack/test/controller/parameters/serialization_test.rb new file mode 100644 index 0000000000..7708c8e4fe --- /dev/null +++ b/actionpack/test/controller/parameters/serialization_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller/metal/strong_parameters" + +class ParametersSerializationTest < ActiveSupport::TestCase + setup do + @old_permitted_parameters = ActionController::Parameters.permit_all_parameters + ActionController::Parameters.permit_all_parameters = false + end + + teardown do + ActionController::Parameters.permit_all_parameters = @old_permitted_parameters + end + + test "yaml serialization" do + params = ActionController::Parameters.new(key: :value) + yaml_dump = YAML.dump(params) + assert_match("--- !ruby/object:ActionController::Parameters", yaml_dump) + assert_match(/parameters: !ruby\/hash:ActiveSupport::HashWithIndifferentAccess\n\s+key: :value/, yaml_dump) + assert_match("permitted: false", yaml_dump) + end + + test "yaml deserialization" do + params = ActionController::Parameters.new(key: :value) + roundtripped = YAML.load(YAML.dump(params)) + + assert_equal params, roundtripped + assert_not_predicate roundtripped, :permitted? + end + + test "yaml backwardscompatible with psych 2.0.8 format" do + params = YAML.load <<~end_of_yaml + --- !ruby/hash:ActionController::Parameters + key: :value + end_of_yaml + + assert_equal :value, params[:key] + assert_not_predicate params, :permitted? + end + + test "yaml backwardscompatible with psych 2.0.9+ format" do + params = YAML.load(<<~end_of_yaml) + --- !ruby/hash-with-ivars:ActionController::Parameters + elements: + key: :value + ivars: + :@permitted: false + end_of_yaml + + assert_equal :value, params[:key] + assert_not_predicate params, :permitted? + end +end diff --git a/actionpack/test/controller/params_parse_test.rb b/actionpack/test/controller/params_parse_test.rb new file mode 100644 index 0000000000..440ab06fd7 --- /dev/null +++ b/actionpack/test/controller/params_parse_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ParamsParseTest < ActionController::TestCase + class UsersController < ActionController::Base + def create + head :ok + end + end + + tests UsersController + + def test_parse_error_logged_once + log_output = capture_log_output do + post :create, body: "{", as: :json + end + assert_equal <<~LOG, log_output + Error occurred while parsing request parameters. + Contents: + + { + LOG + end + + private + + def capture_log_output + output = StringIO.new + request.set_header "action_dispatch.logger", ActiveSupport::Logger.new(output) + yield + output.string + end +end diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb new file mode 100644 index 0000000000..c4c74e8f2b --- /dev/null +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -0,0 +1,422 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module Admin; class User; end; end + +module ParamsWrapperTestHelp + def with_default_wrapper_options(&block) + @controller.class._set_wrapper_options(format: [:json]) + @controller.class.inherited(@controller.class) + yield + end + + def assert_parameters(expected) + assert_equal expected, self.class.controller_class.last_parameters + end +end + +class ParamsWrapperTest < ActionController::TestCase + include ParamsWrapperTestHelp + + class UsersController < ActionController::Base + class << self + attr_accessor :last_parameters + end + + def parse + self.class.last_parameters = request.params.except(:controller, :action) + head :ok + end + end + + class User + def self.attribute_names + [] + end + + def self.stored_attributes + { settings: [:color, :size] } + end + end + + class Person + def self.attribute_names + [] + end + end + + tests UsersController + + def teardown + UsersController.last_parameters = nil + end + + def test_filtered_parameters + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu" } + assert_equal({ "controller" => "params_wrapper_test/users", "action" => "parse", "username" => "sikachu", "user" => { "username" => "sikachu" } }, @request.filtered_parameters) + end + end + + def test_derived_name_from_controller + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu" } + assert_parameters("username" => "sikachu", "user" => { "username" => "sikachu" }) + end + end + + def test_store_accessors_wrapped + assert_called(User, :attribute_names, times: 2, returns: ["username"]) do + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "color" => "blue", "size" => "large" } + assert_parameters("username" => "sikachu", "color" => "blue", "size" => "large", + "user" => { "username" => "sikachu", "color" => "blue", "size" => "large" }) + end + end + end + + def test_specify_wrapper_name + with_default_wrapper_options do + UsersController.wrap_parameters :person + + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu" } + assert_parameters("username" => "sikachu", "person" => { "username" => "sikachu" }) + end + end + + def test_specify_wrapper_model + with_default_wrapper_options do + UsersController.wrap_parameters Person + + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu" } + assert_parameters("username" => "sikachu", "person" => { "username" => "sikachu" }) + end + end + + def test_specify_include_option + with_default_wrapper_options do + UsersController.wrap_parameters include: :username + + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "title" => "Developer" } + assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" }) + end + end + + def test_specify_exclude_option + with_default_wrapper_options do + UsersController.wrap_parameters exclude: :title + + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "title" => "Developer" } + assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" }) + end + end + + def test_specify_both_wrapper_name_and_include_option + with_default_wrapper_options do + UsersController.wrap_parameters :person, include: :username + + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "title" => "Developer" } + assert_parameters("username" => "sikachu", "title" => "Developer", "person" => { "username" => "sikachu" }) + end + end + + def test_not_enabled_format + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/xml" + post :parse, params: { "username" => "sikachu", "title" => "Developer" } + assert_parameters("username" => "sikachu", "title" => "Developer") + end + end + + def test_wrap_parameters_false + with_default_wrapper_options do + UsersController.wrap_parameters false + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "title" => "Developer" } + assert_parameters("username" => "sikachu", "title" => "Developer") + end + end + + def test_specify_format + with_default_wrapper_options do + UsersController.wrap_parameters format: :xml + + @request.env["CONTENT_TYPE"] = "application/xml" + post :parse, params: { "username" => "sikachu", "title" => "Developer" } + assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu", "title" => "Developer" }) + end + end + + def test_not_wrap_reserved_parameters + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "authenticity_token" => "pwned", "_method" => "put", "utf8" => "☃", "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 + + def test_derived_wrapped_keys_from_nested_attributes + def User.nested_attributes_options + { person: {} } + end + + assert_called(User, :attribute_names, times: 2, returns: ["username"]) do + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "person_attributes" => { "title" => "Developer" } } + assert_parameters("username" => "sikachu", "person_attributes" => { "title" => "Developer" }, "user" => { "username" => "sikachu", "person_attributes" => { "title" => "Developer" } }) + end + end + end +end + +class NamespacedParamsWrapperTest < ActionController::TestCase + include ParamsWrapperTestHelp + + module Admin + module Users + class UsersController < ActionController::Base + class << self + attr_accessor :last_parameters + end + + def parse + self.class.last_parameters = request.params.except(:controller, :action) + head :ok + end + end + end + end + + class SampleOne + def self.attribute_names + ["username"] + end + end + + class SampleTwo + def self.attribute_names + ["title"] + end + end + + tests Admin::Users::UsersController + + def teardown + Admin::Users::UsersController.last_parameters = nil + end + + def test_derived_name_from_controller + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu" } + assert_parameters("username" => "sikachu", "user" => { "username" => "sikachu" }) + end + end + + def test_namespace_lookup_from_model + Admin.const_set(:User, Class.new(SampleOne)) + begin + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "title" => "Developer" } + assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "username" => "sikachu" }) + end + ensure + Admin.send :remove_const, :User + end + end + + def test_hierarchy_namespace_lookup_from_model + Object.const_set(:User, Class.new(SampleTwo)) + begin + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "title" => "Developer" } + assert_parameters("username" => "sikachu", "title" => "Developer", "user" => { "title" => "Developer" }) + end + ensure + Object.send :remove_const, :User + end + end +end + +class AnonymousControllerParamsWrapperTest < ActionController::TestCase + include ParamsWrapperTestHelp + + tests(Class.new(ActionController::Base) do + class << self + attr_accessor :last_parameters + end + + def parse + self.class.last_parameters = request.params.except(:controller, :action) + head :ok + end + end) + + def test_does_not_implicitly_wrap_params + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu" } + assert_parameters("username" => "sikachu") + end + end + + def test_does_wrap_params_if_name_provided + with_default_wrapper_options do + @controller.class.wrap_parameters(name: "guest") + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu" } + assert_parameters("username" => "sikachu", "guest" => { "username" => "sikachu" }) + end + end +end + +class IrregularInflectionParamsWrapperTest < ActionController::TestCase + include ParamsWrapperTestHelp + + class ParamswrappernewsItem + def self.attribute_names + ["test_attr"] + end + end + + class ParamswrappernewsController < ActionController::Base + class << self + attr_accessor :last_parameters + end + + def parse + self.class.last_parameters = request.params.except(:controller, :action) + head :ok + end + end + + tests ParamswrappernewsController + + def test_uses_model_attribute_names_with_irregular_inflection + with_dup do + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular "paramswrappernews_item", "paramswrappernews" + end + + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "username" => "sikachu", "test_attr" => "test_value" } + assert_parameters("username" => "sikachu", "test_attr" => "test_value", "paramswrappernews_item" => { "test_attr" => "test_value" }) + end + end + end + + private + + def with_dup + original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en] + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup) + yield + ensure + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original) + end +end diff --git a/actionpack/test/controller/permitted_params_test.rb b/actionpack/test/controller/permitted_params_test.rb new file mode 100644 index 0000000000..caac88ffb2 --- /dev/null +++ b/actionpack/test/controller/permitted_params_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class PeopleController < ActionController::Base + def create + render plain: params[:person].permitted? ? "permitted" : "forbidden" + end + + def create_with_permit + render plain: params[:person].permit(:name).permitted? ? "permitted" : "forbidden" + end +end + +class ActionControllerPermittedParamsTest < ActionController::TestCase + tests PeopleController + + test "parameters are forbidden" do + post :create, params: { person: { name: "Mjallo!" } } + assert_equal "forbidden", response.body + end + + test "parameters can be permitted and are then not forbidden" do + post :create_with_permit, params: { person: { name: "Mjallo!" } } + assert_equal "permitted", response.body + end +end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb new file mode 100644 index 0000000000..998498e1b2 --- /dev/null +++ b/actionpack/test/controller/redirect_test.rb @@ -0,0 +1,402 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class Workshop + extend ActiveModel::Naming + include ActiveModel::Conversion + + OUT_OF_SCOPE_BLOCK = proc do + raise "Not executed in controller's context" unless RedirectController === self + request.original_url + end + + attr_accessor :id + + def initialize(id) + @id = id + end + + def persisted? + id.present? + end + + def to_s + id.to_s + end +end + +class RedirectController < ActionController::Base + # empty method not used anywhere to ensure methods like + # `status` and `location` aren't called on `redirect_to` calls + def status; raise "Should not be called!"; end + def location; raise "Should not be called!"; end + + def simple_redirect + redirect_to action: "hello_world" + end + + def redirect_with_status + redirect_to(action: "hello_world", status: 301) + end + + def redirect_with_status_hash + redirect_to({ action: "hello_world" }, { status: 301 }) + end + + def redirect_with_protocol + redirect_to action: "hello_world", protocol: "https" + end + + def url_redirect_with_status + redirect_to("http://www.example.com", status: :moved_permanently) + end + + def url_redirect_with_status_hash + redirect_to("http://www.example.com", status: 301) + end + + def relative_url_redirect_with_status + redirect_to("/things/stuff", status: :found) + end + + def relative_url_redirect_with_status_hash + redirect_to("/things/stuff", status: 301) + end + + def redirect_back_with_status + redirect_back(fallback_location: "/things/stuff", status: 307) + end + + def safe_redirect_back_with_status + redirect_back(fallback_location: "/things/stuff", status: 307, allow_other_host: false) + end + + def host_redirect + redirect_to action: "other_host", only_path: false, host: "other.test.host" + end + + def module_redirect + redirect_to controller: "module_test/module_redirect", action: "hello_world" + end + + def redirect_to_url + redirect_to "http://www.rubyonrails.org/" + end + + def redirect_to_url_with_unescaped_query_string + redirect_to "http://example.com/query?status=new" + end + + def redirect_to_url_with_complex_scheme + redirect_to "x-test+scheme.complex:redirect" + end + + def redirect_to_url_with_network_path_reference + redirect_to "//www.rubyonrails.org/" + end + + def redirect_to_existing_record + redirect_to Workshop.new(5) + end + + def redirect_to_new_record + redirect_to Workshop.new(nil) + end + + def redirect_to_nil + redirect_to nil + end + + def redirect_to_params + redirect_to ActionController::Parameters.new(status: 200, protocol: "javascript", f: "%0Aeval(name)") + end + + def redirect_to_with_block + redirect_to proc { "http://www.rubyonrails.org/" } + end + + def redirect_to_with_block_and_assigns + @url = "http://www.rubyonrails.org/" + redirect_to proc { @url } + end + + def redirect_to_with_block_and_options + redirect_to proc { { action: "hello_world" } } + end + + def redirect_to_out_of_scope_block + redirect_to Workshop::OUT_OF_SCOPE_BLOCK + end + + def redirect_with_header_break + redirect_to "/lol\r\nwat" + end + + def redirect_with_null_bytes + redirect_to "\000/lol\r\nwat" + end + + def rescue_errors(e) raise e end + + private + def dashbord_url(id, message) + url_for action: "dashboard", params: { "id" => id, "message" => message } + end +end + +class RedirectTest < ActionController::TestCase + tests RedirectController + + def test_simple_redirect + get :simple_redirect + assert_response :redirect + assert_equal "http://test.host/redirect/hello_world", redirect_to_url + end + + def test_redirect_with_header_break + get :redirect_with_header_break + assert_response :redirect + assert_equal "http://test.host/lolwat", redirect_to_url + end + + def test_redirect_with_null_bytes + get :redirect_with_null_bytes + assert_response :redirect + assert_equal "http://test.host/lolwat", redirect_to_url + end + + def test_redirect_with_no_status + get :simple_redirect + assert_response 302 + assert_equal "http://test.host/redirect/hello_world", redirect_to_url + end + + def test_redirect_with_status + get :redirect_with_status + assert_response 301 + assert_equal "http://test.host/redirect/hello_world", redirect_to_url + end + + def test_redirect_with_status_hash + get :redirect_with_status_hash + assert_response 301 + assert_equal "http://test.host/redirect/hello_world", redirect_to_url + end + + def test_redirect_with_protocol + get :redirect_with_protocol + assert_response 302 + assert_equal "https://test.host/redirect/hello_world", redirect_to_url + end + + def test_url_redirect_with_status + get :url_redirect_with_status + assert_response 301 + assert_equal "http://www.example.com", redirect_to_url + end + + def test_url_redirect_with_status_hash + get :url_redirect_with_status_hash + assert_response 301 + assert_equal "http://www.example.com", redirect_to_url + end + + def test_relative_url_redirect_with_status + get :relative_url_redirect_with_status + assert_response 302 + assert_equal "http://test.host/things/stuff", redirect_to_url + end + + def test_relative_url_redirect_with_status_hash + get :relative_url_redirect_with_status_hash + assert_response 301 + assert_equal "http://test.host/things/stuff", redirect_to_url + end + + def test_relative_url_redirect_host_with_port + request.host = "test.host:1234" + get :relative_url_redirect_with_status + assert_response 302 + assert_equal "http://test.host:1234/things/stuff", redirect_to_url + end + + def test_simple_redirect_using_options + get :host_redirect + assert_response :redirect + assert_redirected_to action: "other_host", only_path: false, host: "other.test.host" + end + + def test_module_redirect + get :module_redirect + assert_response :redirect + assert_redirected_to "http://test.host/module_test/module_redirect/hello_world" + end + + def test_module_redirect_using_options + get :module_redirect + assert_response :redirect + assert_redirected_to controller: "module_test/module_redirect", action: "hello_world" + end + + def test_redirect_to_url + get :redirect_to_url + assert_response :redirect + assert_redirected_to "http://www.rubyonrails.org/" + end + + def test_redirect_to_url_with_unescaped_query_string + get :redirect_to_url_with_unescaped_query_string + assert_response :redirect + assert_redirected_to "http://example.com/query?status=new" + end + + def test_redirect_to_url_with_complex_scheme + get :redirect_to_url_with_complex_scheme + assert_response :redirect + assert_equal "x-test+scheme.complex:redirect", redirect_to_url + end + + def test_redirect_to_url_with_network_path_reference + get :redirect_to_url_with_network_path_reference + assert_response :redirect + assert_equal "//www.rubyonrails.org/", redirect_to_url + end + + def test_redirect_back + referer = "http://www.example.com/coming/from" + @request.env["HTTP_REFERER"] = referer + + get :redirect_back_with_status + + assert_response 307 + assert_equal referer, redirect_to_url + end + + def test_redirect_back_with_no_referer + get :redirect_back_with_status + + assert_response 307 + assert_equal "http://test.host/things/stuff", redirect_to_url + end + + def test_safe_redirect_back_from_other_host + @request.env["HTTP_REFERER"] = "http://another.host/coming/from" + get :safe_redirect_back_with_status + + assert_response 307 + assert_equal "http://test.host/things/stuff", redirect_to_url + end + + def test_safe_redirect_back_from_the_same_host + referer = "http://test.host/coming/from" + @request.env["HTTP_REFERER"] = referer + get :safe_redirect_back_with_status + + assert_response 307 + assert_equal referer, redirect_to_url + end + + def test_redirect_to_record + with_routing do |set| + set.draw do + resources :workshops + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + + get :redirect_to_existing_record + assert_equal "http://test.host/workshops/5", redirect_to_url + assert_redirected_to Workshop.new(5) + + get :redirect_to_new_record + assert_equal "http://test.host/workshops", redirect_to_url + assert_redirected_to Workshop.new(nil) + end + end + + def test_redirect_to_nil + error = assert_raise(ActionController::ActionControllerError) do + get :redirect_to_nil + end + assert_equal "Cannot redirect to nil!", error.message + end + + def test_redirect_to_params + error = assert_raise(ActionController::UnfilteredParameters) do + get :redirect_to_params + end + assert_equal "unable to convert unpermitted parameters to hash", error.message + end + + def test_redirect_to_with_block + get :redirect_to_with_block + assert_response :redirect + assert_redirected_to "http://www.rubyonrails.org/" + end + + def test_redirect_to_with_block_and_assigns + get :redirect_to_with_block_and_assigns + assert_response :redirect + assert_redirected_to "http://www.rubyonrails.org/" + end + + def test_redirect_to_out_of_scope_block + get :redirect_to_out_of_scope_block + assert_response :redirect + assert_redirected_to "http://test.host/redirect/redirect_to_out_of_scope_block" + end + + def test_redirect_to_with_block_and_accepted_options + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + + get :redirect_to_with_block_and_options + + assert_response :redirect + assert_redirected_to "http://test.host/redirect/hello_world" + end + end +end + +module ModuleTest + class ModuleRedirectController < ::RedirectController + def module_redirect + redirect_to controller: "/redirect", action: "hello_world" + end + end + + class ModuleRedirectTest < ActionController::TestCase + tests ModuleRedirectController + + def test_simple_redirect + get :simple_redirect + assert_response :redirect + assert_equal "http://test.host/module_test/module_redirect/hello_world", redirect_to_url + end + + def test_simple_redirect_using_options + get :host_redirect + assert_response :redirect + assert_redirected_to action: "other_host", only_path: false, host: "other.test.host" + end + + def test_module_redirect + get :module_redirect + assert_response :redirect + assert_equal "http://test.host/redirect/hello_world", redirect_to_url + end + + def test_module_redirect_using_options + get :module_redirect + assert_response :redirect + assert_redirected_to controller: "/redirect", action: "hello_world" + end + end +end diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb new file mode 100644 index 0000000000..1efc0b9de1 --- /dev/null +++ b/actionpack/test/controller/render_js_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_models" +require "pathname" + +class RenderJSTest < ActionController::TestCase + class TestController < ActionController::Base + protect_from_forgery + + def self.controller_path + "test" + end + + def render_vanilla_js_hello + render js: "alert('hello')" + end + + def show_partial + render partial: "partial" + end + end + + tests TestController + + def test_render_vanilla_js + get :render_vanilla_js_hello, xhr: true + assert_equal "alert('hello')", @response.body + assert_equal "text/javascript", @response.content_type + end + + def test_should_render_js_partial + get :show_partial, format: "js", xhr: true + assert_equal "partial js", @response.body + end +end diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb new file mode 100644 index 0000000000..82c1ba26cb --- /dev/null +++ b/actionpack/test/controller/render_json_test.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_models" +require "active_support/logger" +require "pathname" + +class RenderJsonTest < ActionController::TestCase + class JsonRenderable + def as_json(options = {}) + hash = { a: :b, c: :d, e: :f } + hash.except!(*options[:except]) if options[:except] + hash + end + + def to_json(options = {}) + super except: [:c, :e] + end + end + + class TestController < ActionController::Base + protect_from_forgery + + def self.controller_path + "test" + end + + def render_json_nil + render json: nil + end + + def render_json_render_to_string + render plain: render_to_string(json: "[]") + end + + def render_json_hello_world + render json: ActiveSupport::JSON.encode(hello: "world") + end + + def render_json_hello_world_with_status + render json: ActiveSupport::JSON.encode(hello: "world"), status: 401 + end + + def render_json_hello_world_with_callback + render json: ActiveSupport::JSON.encode(hello: "world"), callback: "alert" + end + + def render_json_with_custom_content_type + render json: ActiveSupport::JSON.encode(hello: "world"), content_type: "text/javascript" + end + + def render_symbol_json + render json: ActiveSupport::JSON.encode(hello: "world") + end + + def render_json_with_render_to_string + render json: { hello: render_to_string(partial: "partial") } + end + + def render_json_with_extra_options + render json: JsonRenderable.new, except: [:c, :e] + end + + def render_json_without_options + render json: JsonRenderable.new + end + end + + tests TestController + + def setup + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + super + @controller.logger = ActiveSupport::Logger.new(nil) + + @request.host = "www.nextangle.com" + end + + def test_render_json_nil + get :render_json_nil + assert_equal "null", @response.body + assert_equal "application/json", @response.content_type + end + + def test_render_json_render_to_string + get :render_json_render_to_string + assert_equal "[]", @response.body + end + + def test_render_json + get :render_json_hello_world + assert_equal '{"hello":"world"}', @response.body + assert_equal "application/json", @response.content_type + end + + def test_render_json_with_status + get :render_json_hello_world_with_status + assert_equal '{"hello":"world"}', @response.body + assert_equal 401, @response.status + end + + def test_render_json_with_callback + get :render_json_hello_world_with_callback, xhr: true + assert_equal '/**/alert({"hello":"world"})', @response.body + assert_equal "text/javascript", @response.content_type + end + + def test_render_json_with_custom_content_type + get :render_json_with_custom_content_type, xhr: true + assert_equal '{"hello":"world"}', @response.body + assert_equal "text/javascript", @response.content_type + end + + def test_render_symbol_json + get :render_symbol_json + assert_equal '{"hello":"world"}', @response.body + assert_equal "application/json", @response.content_type + end + + def test_render_json_with_render_to_string + get :render_json_with_render_to_string + assert_equal '{"hello":"partial html"}', @response.body + assert_equal "application/json", @response.content_type + end + + def test_render_json_forwards_extra_options + get :render_json_with_extra_options + assert_equal '{"a":"b"}', @response.body + assert_equal "application/json", @response.content_type + end + + def test_render_json_calls_to_json_from_object + get :render_json_without_options + assert_equal '{"a":"b"}', @response.body + end +end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb new file mode 100644 index 0000000000..306b245bd1 --- /dev/null +++ b/actionpack/test/controller/render_test.rb @@ -0,0 +1,877 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_models" + +class TestControllerWithExtraEtags < ActionController::Base + def self.controller_name; "test"; end + def self.controller_path; "test"; end + + etag { nil } + etag { "ab" } + etag { :cde } + etag { [:f] } + etag { nil } + + def fresh + render plain: "stale" if stale?(etag: "123", template: false) + end + + def array + render plain: "stale" if stale?(etag: %w(1 2 3), template: false) + end + + def strong + render plain: "stale" if stale?(strong_etag: "strong", template: false) + end + + def with_template + if stale? template: "test/hello_world" + render plain: "stale" + end + end + + def with_implicit_template + fresh_when(etag: "123") + end +end + +class ImplicitRenderTestController < ActionController::Base + def empty_action + end + + def empty_action_with_template + end +end + +module Namespaced + class ImplicitRenderTestController < ActionController::Base + def hello_world + fresh_when(etag: "abc") + end + end +end + +class TestController < ActionController::Base + protect_from_forgery + + before_action :set_variable_for_layout + + class LabellingFormBuilder < ActionView::Helpers::FormBuilder + end + + layout :determine_layout + + def name + nil + end + + private :name + helper_method :name + + def hello_world + end + + def conditional_hello + if stale?(last_modified: Time.now.utc.beginning_of_day, etag: [:foo, 123]) + render action: "hello_world" + end + end + + def conditional_hello_with_record + record = Struct.new(:updated_at, :cache_key).new(Time.now.utc.beginning_of_day, "foo/123") + + if stale?(record) + render action: "hello_world" + end + end + + def dynamic_render + render params[:id] # => String, AC::Params + end + + def dynamic_render_permit + render params[:id].permit(:file) + end + + def dynamic_render_with_file + # This is extremely bad, but should be possible to do. + file = params[:id] # => String, AC::Params + render file: file + end + + class Collection + def initialize(records) + @records = records + end + + def maximum(attribute) + @records.max_by(&attribute).public_send(attribute) + end + end + + def conditional_hello_with_collection_of_records + ts = Time.now.utc.beginning_of_day + + record = Struct.new(:updated_at, :cache_key).new(ts, "foo/123") + old_record = Struct.new(:updated_at, :cache_key).new(ts - 1.day, "bar/123") + + if stale?(Collection.new([record, old_record])) + render action: "hello_world" + end + end + + def conditional_hello_with_expires_in + expires_in 60.1.seconds + render action: "hello_world" + end + + def conditional_hello_with_expires_in_with_public + expires_in 1.minute, public: true + render action: "hello_world" + end + + def conditional_hello_with_expires_in_with_must_revalidate + expires_in 1.minute, must_revalidate: true + render action: "hello_world" + end + + def conditional_hello_with_expires_in_with_public_and_must_revalidate + expires_in 1.minute, public: true, must_revalidate: true + render action: "hello_world" + end + + def conditional_hello_with_expires_in_with_stale_while_revalidate + expires_in 1.minute, public: true, stale_while_revalidate: 5.minutes + render action: "hello_world" + end + + def conditional_hello_with_expires_in_with_stale_if_error + expires_in 1.minute, public: true, stale_if_error: 5.minutes + render action: "hello_world" + end + + def conditional_hello_with_expires_in_with_public_with_more_keys + expires_in 1.minute, :public => true, "s-maxage" => 5.hours + render action: "hello_world" + end + + def conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax + expires_in 1.minute, :public => true, :private => nil, "s-maxage" => 5.hours + render action: "hello_world" + end + + def conditional_hello_with_expires_now + expires_now + render action: "hello_world" + end + + def conditional_hello_with_cache_control_headers + response.headers["Cache-Control"] = "no-transform" + expires_now + render action: "hello_world" + end + + def conditional_hello_with_expires_and_confliciting_cache_control_headers + response.headers["Cache-Control"] = "no-cache, must-revalidate" + expires_now + render action: "hello_world" + end + + def conditional_hello_without_expires_and_confliciting_cache_control_headers + response.headers["Cache-Control"] = "no-cache, must-revalidate" + render action: "hello_world" + end + + def conditional_hello_with_bangs + render action: "hello_world" + end + before_action :handle_last_modified_and_etags, only: :conditional_hello_with_bangs + + def handle_last_modified_and_etags + fresh_when(last_modified: Time.now.utc.beginning_of_day, etag: [ :foo, 123 ]) + end + + def head_created + head :created + end + + def head_created_with_application_json_content_type + head :created, content_type: "application/json" + end + + def head_ok_with_image_png_content_type + head :ok, content_type: "image/png" + end + + def head_with_location_header + head :ok, location: "/foo" + end + + def head_with_location_object + head :ok, location: Customer.new("david", 1) + end + + def head_with_symbolic_status + head params[:status].intern + end + + def head_with_integer_status + head params[:status].to_i + end + + def head_with_string_status + head params[:status] + end + + def head_with_custom_header + head :ok, x_custom_header: "something" + end + + def head_with_www_authenticate_header + head :ok, "WWW-Authenticate" => "something" + end + + def head_with_status_code_first + head :forbidden, x_custom_header: "something" + end + + def head_and_return + head(:ok) && return + raise "should not reach this line" + end + + def head_with_no_content + # Fill in the headers with dummy data to make + # sure they get removed during the testing + response.headers["Content-Type"] = "dummy" + response.headers["Content-Length"] = 42 + + head 204 + end + + def head_default_content_type + # simulating path like "/1.foobar" + request.formats = [] + + respond_to do |format| + format.any { head 200 } + end + end + + private + + def set_variable_for_layout + @variable_for_layout = nil + end + + def determine_layout + case action_name + when "hello_world", "layout_test", "rendering_without_layout", + "rendering_nothing_on_layout", "render_text_hello_world", + "render_text_hello_world_with_layout", + "hello_world_with_layout_false", + "partial_only", "accessing_params_in_template", + "accessing_params_in_template_with_layout", + "render_with_explicit_template", + "render_with_explicit_string_template", + "update_page", "update_page_with_instance_variables" + + "layouts/standard" + when "action_talk_to_layout", "layout_overriding_layout" + "layouts/talk_from_action" + when "render_implicit_html_template_from_xhr_request" + (request.xhr? ? "layouts/xhr" : "layouts/standard") + end + end +end + +module TemplateModificationHelper + private + def modify_template(name) + path = File.expand_path("../fixtures/#{name}.erb", __dir__) + original = File.read(path) + File.write(path, "#{original} Modified!") + ActionView::LookupContext::DetailsKey.clear + yield + ensure + File.write(path, original) + end +end + +class MetalTestController < ActionController::Metal + include AbstractController::Rendering + include ActionView::Rendering + include ActionController::Rendering + + def accessing_logger_in_template + render inline: "<%= logger.class %>" + end +end + +class ExpiresInRenderTest < ActionController::TestCase + tests TestController + + def setup + super + ActionController::Base.view_paths.paths.each(&:clear_cache) + end + + def test_dynamic_render_with_file + # This is extremely bad, but should be possible to do. + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) + response = get :dynamic_render_with_file, params: { id: '../\\../test/abstract_unit.rb' } + assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)), + response.body + end + + def test_dynamic_render_with_absolute_path + file = Tempfile.new("name") + file.write "secrets!" + file.flush + assert_raises ActionView::MissingTemplate do + get :dynamic_render, params: { id: file.path } + end + ensure + file.close + file.unlink + end + + def test_dynamic_render + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) + assert_raises ActionView::MissingTemplate do + get :dynamic_render, params: { id: '../\\../test/abstract_unit.rb' } + end + end + + def test_permitted_dynamic_render_file_hash + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) + response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } } + assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)), + response.body + end + + def test_dynamic_render_file_hash + assert_raises ArgumentError do + get :dynamic_render, params: { id: { file: '../\\../test/abstract_unit.rb' } } + end + end + + def test_expires_in_header + get :conditional_hello_with_expires_in + assert_equal "max-age=60, private", @response.headers["Cache-Control"] + end + + def test_expires_in_header_with_public + get :conditional_hello_with_expires_in_with_public + assert_equal "max-age=60, public", @response.headers["Cache-Control"] + end + + def test_expires_in_header_with_must_revalidate + get :conditional_hello_with_expires_in_with_must_revalidate + assert_equal "max-age=60, private, must-revalidate", @response.headers["Cache-Control"] + end + + def test_expires_in_header_with_public_and_must_revalidate + get :conditional_hello_with_expires_in_with_public_and_must_revalidate + assert_equal "max-age=60, public, must-revalidate", @response.headers["Cache-Control"] + end + + def test_expires_in_header_with_stale_while_revalidate + get :conditional_hello_with_expires_in_with_stale_while_revalidate + assert_equal "max-age=60, public, stale-while-revalidate=300", @response.headers["Cache-Control"] + end + + def test_expires_in_header_with_stale_if_error + get :conditional_hello_with_expires_in_with_stale_if_error + assert_equal "max-age=60, public, stale-if-error=300", @response.headers["Cache-Control"] + end + + def test_expires_in_header_with_additional_headers + get :conditional_hello_with_expires_in_with_public_with_more_keys + assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"] + end + + def test_expires_in_old_syntax + get :conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax + assert_equal "max-age=60, public, s-maxage=18000", @response.headers["Cache-Control"] + end + + def test_expires_now + get :conditional_hello_with_expires_now + assert_equal "no-cache", @response.headers["Cache-Control"] + end + + def test_expires_now_with_cache_control_headers + get :conditional_hello_with_cache_control_headers + assert_match(/no-cache/, @response.headers["Cache-Control"]) + assert_match(/no-transform/, @response.headers["Cache-Control"]) + end + + def test_expires_now_with_conflicting_cache_control_headers + get :conditional_hello_with_expires_and_confliciting_cache_control_headers + assert_equal "no-cache", @response.headers["Cache-Control"] + end + + def test_no_expires_now_with_conflicting_cache_control_headers + get :conditional_hello_without_expires_and_confliciting_cache_control_headers + assert_equal "no-cache", @response.headers["Cache-Control"] + end + + def test_date_header_when_expires_in + time = Time.mktime(2011, 10, 30) + Time.stub :now, time do + get :conditional_hello_with_expires_in + assert_equal Time.now.httpdate, @response.headers["Date"] + end + end +end + +class LastModifiedRenderTest < ActionController::TestCase + tests TestController + + def setup + super + @last_modified = Time.now.utc.beginning_of_day.httpdate + end + + def test_responds_with_last_modified + get :conditional_hello + assert_equal @last_modified, @response.headers["Last-Modified"] + end + + def test_request_not_modified + @request.if_modified_since = @last_modified + get :conditional_hello + assert_equal 304, @response.status.to_i + assert_predicate @response.body, :blank? + assert_equal @last_modified, @response.headers["Last-Modified"] + end + + def test_request_not_modified_but_etag_differs + @request.if_modified_since = @last_modified + @request.if_none_match = '"234"' + get :conditional_hello + assert_response :success + end + + def test_request_modified + @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT" + get :conditional_hello + assert_equal 200, @response.status.to_i + assert_predicate @response.body, :present? + assert_equal @last_modified, @response.headers["Last-Modified"] + end + + def test_responds_with_last_modified_with_record + get :conditional_hello_with_record + assert_equal @last_modified, @response.headers["Last-Modified"] + end + + def test_request_not_modified_with_record + @request.if_modified_since = @last_modified + get :conditional_hello_with_record + assert_equal 304, @response.status.to_i + assert_predicate @response.body, :blank? + assert_not_nil @response.etag + assert_equal @last_modified, @response.headers["Last-Modified"] + end + + def test_request_not_modified_but_etag_differs_with_record + @request.if_modified_since = @last_modified + @request.if_none_match = '"234"' + get :conditional_hello_with_record + assert_response :success + end + + def test_request_modified_with_record + @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT" + get :conditional_hello_with_record + assert_equal 200, @response.status.to_i + assert_predicate @response.body, :present? + assert_equal @last_modified, @response.headers["Last-Modified"] + end + + def test_responds_with_last_modified_with_collection_of_records + get :conditional_hello_with_collection_of_records + assert_equal @last_modified, @response.headers["Last-Modified"] + end + + def test_request_not_modified_with_collection_of_records + @request.if_modified_since = @last_modified + get :conditional_hello_with_collection_of_records + assert_equal 304, @response.status.to_i + assert_predicate @response.body, :blank? + assert_equal @last_modified, @response.headers["Last-Modified"] + end + + def test_request_not_modified_but_etag_differs_with_collection_of_records + @request.if_modified_since = @last_modified + @request.if_none_match = '"234"' + get :conditional_hello_with_collection_of_records + assert_response :success + end + + def test_request_modified_with_collection_of_records + @request.if_modified_since = "Thu, 16 Jul 2008 00:00:00 GMT" + get :conditional_hello_with_collection_of_records + assert_equal 200, @response.status.to_i + assert_predicate @response.body, :present? + assert_equal @last_modified, @response.headers["Last-Modified"] + end + + def test_request_with_bang_gets_last_modified + get :conditional_hello_with_bangs + assert_equal @last_modified, @response.headers["Last-Modified"] + assert_response :success + end + + def test_request_with_bang_obeys_last_modified + @request.if_modified_since = @last_modified + get :conditional_hello_with_bangs + assert_response :not_modified + end + + def test_last_modified_works_with_less_than_too + @request.if_modified_since = 5.years.ago.httpdate + get :conditional_hello_with_bangs + assert_response :success + end +end + +class EtagRenderTest < ActionController::TestCase + tests TestControllerWithExtraEtags + include TemplateModificationHelper + + def test_strong_etag + @request.if_none_match = strong_etag(["strong", "ab", :cde, [:f]]) + get :strong + assert_response :not_modified + + @request.if_none_match = "*" + get :strong + assert_response :not_modified + + @request.if_none_match = '"strong"' + get :strong + assert_response :ok + + @request.if_none_match = weak_etag(["strong", "ab", :cde, [:f]]) + get :strong + assert_response :ok + end + + def test_multiple_etags + @request.if_none_match = weak_etag(["123", "ab", :cde, [:f]]) + get :fresh + assert_response :not_modified + + @request.if_none_match = %("nomatch") + get :fresh + assert_response :success + end + + def test_array + @request.if_none_match = weak_etag([%w(1 2 3), "ab", :cde, [:f]]) + get :array + assert_response :not_modified + + @request.if_none_match = %("nomatch") + get :array + assert_response :success + end + + def test_etag_reflects_template_digest + get :with_template + assert_response :ok + assert_not_nil etag = @response.etag + + request.if_none_match = etag + get :with_template + assert_response :not_modified + + modify_template("test/hello_world") do + request.if_none_match = etag + get :with_template + assert_response :ok + assert_not_equal etag, @response.etag + end + end + + def test_etag_reflects_implicit_template_digest + get :with_implicit_template + assert_response :ok + assert_not_nil etag = @response.etag + + request.if_none_match = etag + get :with_implicit_template + assert_response :not_modified + + modify_template("test/with_implicit_template") do + request.if_none_match = etag + get :with_implicit_template + assert_response :ok + assert_not_equal etag, @response.etag + end + end + + private + def weak_etag(record) + "W/#{strong_etag record}" + end + + def strong_etag(record) + %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}") + end +end + +class NamespacedEtagRenderTest < ActionController::TestCase + tests Namespaced::ImplicitRenderTestController + include TemplateModificationHelper + + def test_etag_reflects_template_digest + get :hello_world + assert_response :ok + assert_not_nil etag = @response.etag + + request.if_none_match = etag + get :hello_world + assert_response :not_modified + + modify_template("namespaced/implicit_render_test/hello_world") do + request.if_none_match = etag + get :hello_world + assert_response :ok + assert_not_equal etag, @response.etag + end + end +end + +class MetalRenderTest < ActionController::TestCase + tests MetalTestController + + def test_access_to_logger_in_view + get :accessing_logger_in_template + assert_equal "NilClass", @response.body + end +end + +class ActionControllerRenderTest < ActionController::TestCase + class MinimalController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + end + + def test_direct_render_to_string_with_body + mc = MinimalController.new + assert_equal "Hello world!", mc.render_to_string(body: ["Hello world!"]) + end +end + +class ActionControllerBaseRenderTest < ActionController::TestCase + def test_direct_render_to_string + ac = ActionController::Base.new() + assert_equal "Hello world!", ac.render_to_string(template: "test/hello_world") + end +end + +class ImplicitRenderTest < ActionController::TestCase + tests ImplicitRenderTestController + + def test_implicit_no_content_response_as_browser + assert_raises(ActionController::MissingExactTemplate) do + get :empty_action + end + end + + def test_implicit_no_content_response_as_xhr + get :empty_action, xhr: true + assert_response :no_content + end + + def test_implicit_success_response_with_right_format + get :empty_action_with_template + assert_equal "<h1>Empty action rendered this implicitly.</h1>\n", @response.body + assert_response :success + end + + def test_implicit_unknown_format_response + assert_raises(ActionController::UnknownFormat) do + get :empty_action_with_template, format: "json" + end + end +end + +class HeadRenderTest < ActionController::TestCase + tests TestController + + def setup + @request.host = "www.nextangle.com" + end + + def test_head_created + post :head_created + assert_predicate @response.body, :blank? + assert_response :created + end + + def test_head_created_with_application_json_content_type + post :head_created_with_application_json_content_type + assert_predicate @response.body, :blank? + assert_equal "application/json", @response.header["Content-Type"] + assert_response :created + end + + def test_head_ok_with_image_png_content_type + post :head_ok_with_image_png_content_type + assert_predicate @response.body, :blank? + assert_equal "image/png", @response.header["Content-Type"] + assert_response :ok + end + + def test_head_with_location_header + get :head_with_location_header + assert_predicate @response.body, :blank? + assert_equal "/foo", @response.headers["Location"] + assert_response :ok + end + + def test_head_with_location_object + with_routing do |set| + set.draw do + resources :customers + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + + get :head_with_location_object + assert_predicate @response.body, :blank? + assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"] + assert_response :ok + end + end + + def test_head_with_custom_header + get :head_with_custom_header + assert_predicate @response.body, :blank? + assert_equal "something", @response.headers["X-Custom-Header"] + assert_response :ok + end + + def test_head_with_www_authenticate_header + get :head_with_www_authenticate_header + assert_predicate @response.body, :blank? + assert_equal "something", @response.headers["WWW-Authenticate"] + assert_response :ok + end + + def test_head_with_symbolic_status + get :head_with_symbolic_status, params: { status: "ok" } + assert_equal 200, @response.status + assert_response :ok + + get :head_with_symbolic_status, params: { status: "not_found" } + assert_equal 404, @response.status + assert_response :not_found + + get :head_with_symbolic_status, params: { status: "no_content" } + assert_equal 204, @response.status + assert_not_includes @response.headers, "Content-Length" + assert_response :no_content + + Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |status, code| + get :head_with_symbolic_status, params: { status: status.to_s } + assert_equal code, @response.response_code + assert_response status + end + end + + def test_head_with_integer_status + Rack::Utils::HTTP_STATUS_CODES.each do |code, message| + get :head_with_integer_status, params: { status: code.to_s } + assert_equal message, @response.message + end + end + + def test_head_with_no_content + get :head_with_no_content + + assert_equal 204, @response.status + assert_nil @response.headers["Content-Type"] + assert_nil @response.headers["Content-Length"] + end + + def test_head_with_string_status + get :head_with_string_status, params: { status: "404 Eat Dirt" } + assert_equal 404, @response.response_code + assert_equal "Not Found", @response.message + assert_response :not_found + end + + def test_head_with_status_code_first + get :head_with_status_code_first + assert_equal 403, @response.response_code + assert_equal "Forbidden", @response.message + assert_equal "something", @response.headers["X-Custom-Header"] + assert_response :forbidden + end + + def test_head_returns_truthy_value + assert_nothing_raised do + get :head_and_return + end + end + + def test_head_default_content_type + post :head_default_content_type + assert_equal "text/html", @response.header["Content-Type"] + end +end + +class HttpCacheForeverTest < ActionController::TestCase + class HttpCacheForeverController < ActionController::Base + def cache_me_forever + http_cache_forever(public: params[:public]) do + render plain: "hello" + end + end + end + + tests HttpCacheForeverController + + def test_cache_with_public + get :cache_me_forever, params: { public: true } + assert_response :ok + assert_equal "max-age=#{100.years}, public", @response.headers["Cache-Control"] + assert_not_nil @response.etag + assert_predicate @response, :weak_etag? + end + + def test_cache_with_private + get :cache_me_forever + assert_response :ok + assert_equal "max-age=#{100.years}, private", @response.headers["Cache-Control"] + assert_not_nil @response.etag + assert_predicate @response, :weak_etag? + end + + def test_cache_response_code_with_if_modified_since + get :cache_me_forever + assert_response :ok + + @request.if_modified_since = @response.headers["Last-Modified"] + get :cache_me_forever + assert_response :not_modified + end + + def test_cache_response_code_with_etag + get :cache_me_forever + assert_response :ok + + @request.if_none_match = @response.etag + get :cache_me_forever + assert_response :not_modified + end +end diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb new file mode 100644 index 0000000000..a72d14e4bb --- /dev/null +++ b/actionpack/test/controller/render_xml_test.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_models" +require "pathname" + +class RenderXmlTest < ActionController::TestCase + class XmlRenderable + def to_xml(options) + options[:root] ||= "i-am-xml" + "<#{options[:root]}/>" + end + end + + class TestController < ActionController::Base + protect_from_forgery + + def self.controller_path + "test" + end + + def render_with_location + render xml: "<hello/>", location: "http://example.com", status: 201 + end + + def render_with_object_location + customer = Customer.new("Some guy", 1) + render xml: "<customer/>", location: customer, status: :created + end + + def render_with_to_xml + render xml: XmlRenderable.new + end + + def formatted_xml_erb + end + + def render_xml_with_custom_content_type + render xml: "<blah/>", content_type: "application/atomsvc+xml" + end + + def render_xml_with_custom_options + render xml: XmlRenderable.new, root: "i-am-THE-xml" + end + end + + tests TestController + + def setup + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + super + @controller.logger = ActiveSupport::Logger.new(nil) + + @request.host = "www.nextangle.com" + end + + def test_rendering_with_location_should_set_header + get :render_with_location + assert_equal "http://example.com", @response.headers["Location"] + end + + def test_rendering_xml_should_call_to_xml_if_possible + get :render_with_to_xml + assert_equal "<i-am-xml/>", @response.body + end + + def test_rendering_xml_should_call_to_xml_with_extra_options + get :render_xml_with_custom_options + assert_equal "<i-am-THE-xml/>", @response.body + end + + def test_rendering_with_object_location_should_set_header_with_url_for + with_routing do |set| + set.draw do + resources :customers + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + + get :render_with_object_location + assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"] + end + end + + def test_should_render_formatted_xml_erb_template + get :formatted_xml_erb, format: :xml + assert_equal "<test>passed formatted xml erb</test>", @response.body + end + + def test_should_render_xml_but_keep_custom_content_type + get :render_xml_with_custom_content_type + assert_equal "application/atomsvc+xml", @response.content_type + end + + def test_should_use_implicit_content_type + get :implicit_content_type, format: "atom" + assert_equal Mime[:atom], @response.content_type + end +end diff --git a/actionpack/test/controller/renderer_test.rb b/actionpack/test/controller/renderer_test.rb new file mode 100644 index 0000000000..ae8330e029 --- /dev/null +++ b/actionpack/test/controller/renderer_test.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class RendererTest < ActiveSupport::TestCase + test "action controller base has a renderer" do + assert ActionController::Base.renderer + end + + test "creating with a controller" do + controller = CommentsController + renderer = ActionController::Renderer.for controller + + assert_equal controller, renderer.controller + end + + test "creating from a controller" do + controller = AccountsController + renderer = controller.renderer + + assert_equal controller, renderer.controller + end + + test "creating with new defaults" do + renderer = ApplicationController.renderer + + new_defaults = { https: true } + new_renderer = renderer.with_defaults(new_defaults).new + content = new_renderer.render(inline: "<%= request.ssl? %>") + + assert_equal "true", content + end + + test "rendering with a class renderer" do + renderer = ApplicationController.renderer + content = renderer.render template: "ruby_template" + + assert_equal "Hello from Ruby code", content + end + + test "rendering with an instance renderer" do + renderer = ApplicationController.renderer.new + content = renderer.render file: "test/hello_world" + + assert_equal "Hello world!", content + end + + test "rendering with a controller class" do + assert_equal "Hello world!", ApplicationController.render("test/hello_world") + end + + test "rendering with locals" do + renderer = ApplicationController.renderer + content = renderer.render template: "test/render_file_with_locals", + locals: { secret: "bar" } + + assert_equal "The secret is bar\n", content + end + + test "rendering with assigns" do + renderer = ApplicationController.renderer + content = renderer.render template: "test/render_file_with_ivar", + assigns: { secret: "foo" } + + assert_equal "The secret is foo\n", content + end + + test "rendering with custom env" do + renderer = ApplicationController.renderer.new method: "post" + content = renderer.render inline: "<%= request.post? %>" + + assert_equal "true", content + end + + test "rendering with custom env using a key that is not in RACK_KEY_TRANSLATION" do + value = "warden is here" + renderer = ApplicationController.renderer.new warden: value + content = renderer.render inline: "<%= request.env['warden'] %>" + + assert_equal value, content + end + + test "rendering with defaults" do + renderer = ApplicationController.renderer.new https: true + content = renderer.render inline: "<%= request.ssl? %>" + + assert_equal "true", content + end + + test "same defaults from the same controller" do + renderer_defaults = ->(controller) { controller.renderer.defaults } + + assert_equal renderer_defaults[AccountsController], renderer_defaults[AccountsController] + assert_equal renderer_defaults[AccountsController], renderer_defaults[CommentsController] + end + + test "rendering with different formats" do + html = "Hello world!" + xml = "<p>Hello world!</p>\n" + + assert_equal html, render["respond_to/using_defaults"] + assert_equal xml, render["respond_to/using_defaults.xml.builder"] + assert_equal xml, render["respond_to/using_defaults", formats: :xml] + end + + test "rendering with helpers" do + assert_equal "<p>1\n<br />2</p>", render[inline: '<%= simple_format "1\n2" %>'] + end + + test "rendering with user specified defaults" do + ApplicationController.renderer.defaults.merge!(hello: "hello", https: true) + renderer = ApplicationController.renderer.new + content = renderer.render inline: "<%= request.ssl? %>" + + assert_equal "true", content + end + + test "return valid asset url with defaults" do + renderer = ApplicationController.renderer + content = renderer.render inline: "<%= asset_url 'asset.jpg' %>" + + assert_equal "http://example.org/asset.jpg", content + end + + test "return valid asset url when https is true" do + renderer = ApplicationController.renderer.new https: true + content = renderer.render inline: "<%= asset_url 'asset.jpg' %>" + + assert_equal "https://example.org/asset.jpg", content + end + + private + def render + @render ||= ApplicationController.renderer.method(:render) + end +end diff --git a/actionpack/test/controller/renderers_test.rb b/actionpack/test/controller/renderers_test.rb new file mode 100644 index 0000000000..d92de6f5d5 --- /dev/null +++ b/actionpack/test/controller/renderers_test.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_models" +require "active_support/logger" + +class RenderersTest < ActionController::TestCase + class XmlRenderable + def to_xml(options) + options[:root] ||= "i-am-xml" + "<#{options[:root]}/>" + end + end + class JsonRenderable + def as_json(options = {}) + hash = { a: :b, c: :d, e: :f } + hash.except!(*options[:except]) if options[:except] + hash + end + + def to_json(options = {}) + super except: [:c, :e] + end + end + class CsvRenderable + def to_csv + "c,s,v" + end + end + class TestController < ActionController::Base + def render_simon_says + render simon: "foo" + end + + def respond_to_mime + respond_to do |type| + type.json do + render json: JsonRenderable.new + end + type.js { render json: "JS", callback: "alert" } + type.csv { render csv: CsvRenderable.new } + type.xml { render xml: XmlRenderable.new } + type.html { render body: "HTML" } + type.rss { render body: "RSS" } + type.all { render body: "Nothing" } + type.any(:js, :xml) { render body: "Either JS or XML" } + end + end + end + + tests TestController + + def setup + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + super + @controller.logger = ActiveSupport::Logger.new(nil) + end + + def test_using_custom_render_option + ActionController.add_renderer :simon do |says, options| + self.content_type = Mime[:text] + self.response_body = "Simon says: #{says}" + end + + get :render_simon_says + assert_equal "Simon says: foo", @response.body + ensure + ActionController.remove_renderer :simon + end + + def test_raises_missing_template_no_renderer + assert_raise ActionView::MissingTemplate do + get :respond_to_mime, format: "csv" + end + assert_equal Mime[:csv], @response.content_type + assert_equal "", @response.body + end + + def test_adding_csv_rendering_via_renderers_add + ActionController::Renderers.add :csv do |value, options| + send_data value.to_csv, type: Mime[:csv] + end + @request.accept = "text/csv" + get :respond_to_mime, format: "csv" + assert_equal Mime[:csv], @response.content_type + assert_equal "c,s,v", @response.body + ensure + ActionController::Renderers.remove :csv + end +end diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb new file mode 100644 index 0000000000..b8d86696de --- /dev/null +++ b/actionpack/test/controller/request/test_request_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "stringio" + +class ActionController::TestRequestTest < ActionController::TestCase + def test_test_request_has_session_options_initialized + assert @request.session_options + end + + def test_mutating_session_options_does_not_affect_default_options + @request.session_options[:myparam] = 123 + assert_nil ActionController::TestSession::DEFAULT_OPTIONS[:myparam] + end + + def test_content_length_has_bytes_count_value + non_ascii_parameters = { data: { content: "Latin + Кириллица" } } + @request.set_header "REQUEST_METHOD", "POST" + @request.set_header "CONTENT_TYPE", "application/json" + @request.assign_parameters(@routes, "test", "create", non_ascii_parameters, + "/test", [:data, :controller, :action]) + assert_equal(StringIO.new(non_ascii_parameters.to_json).length.to_s, + @request.get_header("CONTENT_LENGTH")) + end + + ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS.each_pair do |key, value| + test "rack default session options #{key} exists in session options and is default" do + if value.nil? + assert_nil(@request.session_options[key], + "Missing rack session default option #{key} in request.session_options") + else + assert_equal(value, @request.session_options[key], + "Missing rack session default option #{key} in request.session_options") + end + end + + test "rack default session options #{key} exists in session options" do + assert(@request.session_options.has_key?(key), + "Missing rack session option #{key} in request.session_options") + end + end +end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb new file mode 100644 index 0000000000..ea94a3e048 --- /dev/null +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -0,0 +1,1018 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/log_subscriber/test_helper" +require "active_support/messages/rotation_configuration" + +# common controller actions +module RequestForgeryProtectionActions + def index + render inline: "<%= form_tag('/') {} %>" + end + + def show_button + render inline: "<%= button_to('New', '/') %>" + end + + def unsafe + render plain: "pwn" + end + + def meta + render inline: "<%= csrf_meta_tags %>" + end + + def form_for_remote + render inline: "<%= form_for(:some_resource, :remote => true ) {} %>" + end + + def form_for_remote_with_token + render inline: "<%= form_for(:some_resource, :remote => true, :authenticity_token => true ) {} %>" + end + + def form_for_with_token + render inline: "<%= form_for(:some_resource, :authenticity_token => true ) {} %>" + end + + def form_for_remote_with_external_token + render inline: "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>" + end + + def form_with_remote + render inline: "<%= form_with(scope: :some_resource) {} %>" + end + + def form_with_remote_with_token + render inline: "<%= form_with(scope: :some_resource, authenticity_token: true) {} %>" + end + + def form_with_local_with_token + render inline: "<%= form_with(scope: :some_resource, local: true, authenticity_token: true) {} %>" + end + + def form_with_remote_with_external_token + render inline: "<%= form_with(scope: :some_resource, authenticity_token: 'external_token') {} %>" + end + + def same_origin_js + render js: "foo();" + end + + def negotiate_same_origin + respond_to do |format| + format.js { same_origin_js } + end + end + + def cross_origin_js + same_origin_js + end + + def negotiate_cross_origin + negotiate_same_origin + end +end + +# sample controllers +class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base + include RequestForgeryProtectionActions + protect_from_forgery only: %w(index meta same_origin_js negotiate_same_origin), with: :reset_session +end + +class RequestForgeryProtectionControllerUsingException < ActionController::Base + include RequestForgeryProtectionActions + protect_from_forgery only: %w(index meta same_origin_js negotiate_same_origin), with: :exception +end + +class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base + protect_from_forgery with: :null_session + + def signed + cookies.signed[:foo] = "bar" + head :ok + end + + def encrypted + cookies.encrypted[:foo] = "bar" + head :ok + end + + def try_to_reset_session + reset_session + head :ok + end +end + +class PrependProtectForgeryBaseController < ActionController::Base + before_action :custom_action + attr_accessor :called_callbacks + + def index + render inline: "OK" + end + + private + + def add_called_callback(name) + @called_callbacks ||= [] + @called_callbacks << name + end + + def custom_action + add_called_callback("custom_action") + end + + def verify_authenticity_token + add_called_callback("verify_authenticity_token") + end +end + +class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession + self.allow_forgery_protection = false + + def index + render inline: "<%= form_tag('/') {} %>" + end + + def show_button + render inline: "<%= button_to('New', '/') %>" + end +end + +class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsingResetSession + def form_authenticity_param + "foobar" + end +end + +class PerFormTokensController < ActionController::Base + protect_from_forgery with: :exception + self.per_form_csrf_tokens = true + + def index + render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>" + end + + def button_to + render inline: "<%= button_to 'Button', (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>" + end + + def post_one + render plain: "" + end + + def post_two + render plain: "" + end +end + +class SkipProtectionController < ActionController::Base + include RequestForgeryProtectionActions + protect_from_forgery with: :exception + skip_forgery_protection if: :skip_requested + attr_accessor :skip_requested +end + +# common test methods +module RequestForgeryProtectionTests + def setup + @token = Base64.strict_encode64("railstestrailstestrailstestrails") + @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token + ActionController::Base.request_forgery_protection_token = :custom_authenticity_token + end + + def teardown + ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token + end + + def test_should_render_form_with_token_tag + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :index + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + end + end + + def test_should_render_button_to_with_token_tag + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :show_button + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + end + end + + def test_should_render_form_without_token_tag_if_remote + assert_not_blocked do + get :form_for_remote + end + assert_no_match(/authenticity_token/, response.body) + end + + def test_should_render_form_with_token_tag_if_remote_and_embedding_token_is_on + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true + assert_not_blocked do + get :form_for_remote + end + assert_match(/authenticity_token/, response.body) + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + + def test_should_render_form_with_token_tag_if_remote_and_external_authenticity_token_requested_and_embedding_is_on + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true + assert_not_blocked do + get :form_for_remote_with_external_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token" + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + + def test_should_render_form_with_token_tag_if_remote_and_external_authenticity_token_requested + assert_not_blocked do + get :form_for_remote_with_external_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token" + end + + def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_for_remote_with_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + end + end + + def test_should_render_form_with_token_tag_with_authenticity_token_requested + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_for_with_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + end + end + + def test_should_render_form_with_with_token_tag_if_remote + assert_not_blocked do + get :form_with_remote + end + assert_match(/authenticity_token/, response.body) + end + + def test_should_render_form_with_without_token_tag_if_remote_and_embedding_token_is_off + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = false + assert_not_blocked do + get :form_with_remote + end + assert_no_match(/authenticity_token/, response.body) + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + + def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested_and_embedding_is_on + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true + assert_not_blocked do + get :form_with_remote_with_external_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token" + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + + def test_should_render_form_with_with_token_tag_if_remote_and_external_authenticity_token_requested + assert_not_blocked do + get :form_with_remote_with_external_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", "external_token" + end + + def test_should_render_form_with_with_token_tag_if_remote_and_authenticity_token_requested + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_with_remote_with_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + end + end + + def test_should_render_form_with_with_token_tag_with_authenticity_token_requested + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_with_local_with_token + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + end + end + + def test_should_render_form_with_with_token_tag_if_remote_and_embedding_token_is_on + original = ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms + begin + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = true + + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + get :form_with_remote + end + end + assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token + ensure + ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original + end + end + + def test_should_allow_get + assert_not_blocked { get :index } + end + + def test_should_allow_head + assert_not_blocked { head :index } + end + + def test_should_allow_post_without_token_on_unsafe_action + assert_not_blocked { post :unsafe } + end + + def test_should_not_allow_post_without_token + assert_blocked { post :index } + end + + def test_should_not_allow_post_without_token_irrespective_of_format + assert_blocked { post :index, format: "xml" } + end + + def test_should_not_allow_patch_without_token + assert_blocked { patch :index } + end + + def test_should_not_allow_put_without_token + assert_blocked { put :index } + end + + def test_should_not_allow_delete_without_token + assert_blocked { delete :index } + end + + def test_should_not_allow_xhr_post_without_token + assert_blocked { post :index, xhr: true } + end + + def test_should_allow_post_with_token + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked { post :index, params: { custom_authenticity_token: @token } } + end + end + + def test_should_allow_patch_with_token + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked { patch :index, params: { custom_authenticity_token: @token } } + end + end + + def test_should_allow_put_with_token + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked { put :index, params: { custom_authenticity_token: @token } } + end + end + + def test_should_allow_delete_with_token + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked { delete :index, params: { custom_authenticity_token: @token } } + end + end + + def test_should_allow_post_with_token_in_header + session[:_csrf_token] = @token + @request.env["HTTP_X_CSRF_TOKEN"] = @token + assert_not_blocked { post :index } + end + + def test_should_allow_delete_with_token_in_header + session[:_csrf_token] = @token + @request.env["HTTP_X_CSRF_TOKEN"] = @token + assert_not_blocked { delete :index } + end + + def test_should_allow_patch_with_token_in_header + session[:_csrf_token] = @token + @request.env["HTTP_X_CSRF_TOKEN"] = @token + assert_not_blocked { patch :index } + end + + def test_should_allow_put_with_token_in_header + session[:_csrf_token] = @token + @request.env["HTTP_X_CSRF_TOKEN"] = @token + assert_not_blocked { put :index } + end + + def test_should_allow_post_with_origin_checking_and_correct_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + @request.set_header "HTTP_ORIGIN", "http://test.host" + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + + def test_should_allow_post_with_origin_checking_and_no_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + + def test_should_raise_for_post_with_null_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + exception = assert_raises(ActionController::InvalidAuthenticityToken) do + @request.set_header "HTTP_ORIGIN", "null" + post :index, params: { custom_authenticity_token: @token } + end + assert_match "The browser returned a 'null' origin for a request", exception.message + end + end + end + + def test_should_block_post_with_origin_checking_and_wrong_origin + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_blocked do + @request.set_header "HTTP_ORIGIN", "http://bad.host" + post :index, params: { custom_authenticity_token: @token } + end + end + end + + assert_match( + "HTTP Origin header (http://bad.host) didn't match request.base_url (http://test.host)", + logger.logged(:warn).last + ) + ensure + ActionController::Base.logger = old_logger + end + + def test_should_warn_on_missing_csrf_token + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + + begin + assert_blocked { post :index } + + assert_equal 1, logger.logged(:warn).size + assert_match(/CSRF token authenticity/, logger.logged(:warn).last) + ensure + ActionController::Base.logger = old_logger + end + end + + def test_should_not_warn_if_csrf_logging_disabled + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + ActionController::Base.log_warning_on_csrf_failure = false + + begin + assert_blocked { post :index } + + assert_equal 0, logger.logged(:warn).size + ensure + ActionController::Base.logger = old_logger + ActionController::Base.log_warning_on_csrf_failure = true + end + end + + def test_should_only_allow_same_origin_js_get_with_xhr_header + assert_cross_origin_blocked { get :same_origin_js } + assert_cross_origin_blocked { get :same_origin_js, format: "js" } + assert_cross_origin_blocked do + @request.accept = "text/javascript" + get :negotiate_same_origin + end + + assert_cross_origin_blocked do + @request.accept = "application/javascript" + get :negotiate_same_origin + end + + assert_cross_origin_not_blocked { get :same_origin_js, xhr: true } + assert_cross_origin_not_blocked { get :same_origin_js, xhr: true, format: "js" } + assert_cross_origin_not_blocked do + @request.accept = "text/javascript" + get :negotiate_same_origin, xhr: true + end + end + + def test_should_warn_on_not_same_origin_js + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + + begin + assert_cross_origin_blocked { get :same_origin_js } + + assert_equal 1, logger.logged(:warn).size + assert_match(/<script> tag on another site requested protected JavaScript/, logger.logged(:warn).last) + ensure + ActionController::Base.logger = old_logger + end + end + + def test_should_not_warn_if_csrf_logging_disabled_and_not_same_origin_js + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + ActionController::Base.log_warning_on_csrf_failure = false + + begin + assert_cross_origin_blocked { get :same_origin_js } + + assert_equal 0, logger.logged(:warn).size + ensure + ActionController::Base.logger = old_logger + ActionController::Base.log_warning_on_csrf_failure = true + end + end + + # Allow non-GET requests since GET is all a remote <script> tag can muster. + def test_should_allow_non_get_js_without_xhr_header + session[:_csrf_token] = @token + assert_cross_origin_not_blocked { post :same_origin_js, params: { custom_authenticity_token: @token } } + assert_cross_origin_not_blocked { post :same_origin_js, params: { format: "js", custom_authenticity_token: @token } } + assert_cross_origin_not_blocked do + @request.accept = "text/javascript" + post :negotiate_same_origin, params: { custom_authenticity_token: @token } + end + end + + def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled + assert_cross_origin_not_blocked { get :cross_origin_js } + assert_cross_origin_not_blocked { get :cross_origin_js, format: "js" } + assert_cross_origin_not_blocked do + @request.accept = "text/javascript" + get :negotiate_cross_origin + end + + assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true } + assert_cross_origin_not_blocked { get :cross_origin_js, xhr: true, format: "js" } + assert_cross_origin_not_blocked do + @request.accept = "text/javascript" + get :negotiate_cross_origin, xhr: true + end + end + + def test_should_not_raise_error_if_token_is_not_a_string + assert_blocked do + patch :index, params: { custom_authenticity_token: { foo: "bar" } } + end + end + + def assert_blocked + session[:something_like_user_id] = 1 + yield + assert_nil session[:something_like_user_id], "session values are still present" + assert_response :success + end + + def assert_not_blocked + assert_nothing_raised { yield } + assert_response :success + end + + def assert_cross_origin_blocked + assert_raises(ActionController::InvalidCrossOriginRequest) do + yield + end + end + + def assert_cross_origin_not_blocked + assert_not_blocked { yield } + end + + def forgery_protection_origin_check + old_setting = ActionController::Base.forgery_protection_origin_check + ActionController::Base.forgery_protection_origin_check = true + begin + yield + ensure + ActionController::Base.forgery_protection_origin_check = old_setting + end + end +end + +# OK let's get our test on + +class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController::TestCase + include RequestForgeryProtectionTests + + test "should emit a csrf-param meta tag and a csrf-token meta tag" do + @controller.stub :form_authenticity_token, @token + "<=?" do + get :meta + assert_select "meta[name=?][content=?]", "csrf-param", "custom_authenticity_token" + assert_select "meta[name=?]", "csrf-token" + regexp = "#{@token}<=\?" + assert_match(/#{regexp}/, @response.body) + end + end +end + +class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase + class NullSessionDummyKeyGenerator + def generate_key(secret, length = nil) + "03312270731a2ed0d11ed091c2338a06" + end + end + + def setup + @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new + @request.env[ActionDispatch::Cookies::COOKIES_ROTATIONS] = ActiveSupport::Messages::RotationConfiguration.new + end + + test "should allow to set signed cookies" do + post :signed + assert_response :ok + end + + test "should allow to set encrypted cookies" do + post :encrypted + assert_response :ok + end + + test "should allow reset_session" do + post :try_to_reset_session + assert_response :ok + end +end + +class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase + include RequestForgeryProtectionTests + def assert_blocked + assert_raises(ActionController::InvalidAuthenticityToken) do + yield + end + end +end + +class PrependProtectForgeryBaseControllerTest < ActionController::TestCase + PrependTrueController = Class.new(PrependProtectForgeryBaseController) do + protect_from_forgery prepend: true + end + + PrependFalseController = Class.new(PrependProtectForgeryBaseController) do + protect_from_forgery prepend: false + end + + PrependDefaultController = Class.new(PrependProtectForgeryBaseController) do + protect_from_forgery + end + + def test_verify_authenticity_token_is_prepended + @controller = PrependTrueController.new + get :index + expected_callback_order = ["verify_authenticity_token", "custom_action"] + assert_equal(expected_callback_order, @controller.called_callbacks) + end + + def test_verify_authenticity_token_is_not_prepended + @controller = PrependFalseController.new + get :index + expected_callback_order = ["custom_action", "verify_authenticity_token"] + assert_equal(expected_callback_order, @controller.called_callbacks) + end + + def test_verify_authenticity_token_is_not_prepended_by_default + @controller = PrependDefaultController.new + get :index + expected_callback_order = ["custom_action", "verify_authenticity_token"] + assert_equal(expected_callback_order, @controller.called_callbacks) + end +end + +class FreeCookieControllerTest < ActionController::TestCase + def setup + @controller = FreeCookieController.new + @token = "cf50faa3fe97702ca1ae" + super + end + + def test_should_not_render_form_with_token_tag + SecureRandom.stub :base64, @token do + get :index + assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false + end + end + + def test_should_not_render_button_to_with_token_tag + SecureRandom.stub :base64, @token do + get :show_button + assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false + end + end + + def test_should_allow_all_methods_without_token + SecureRandom.stub :base64, @token do + [:post, :patch, :put, :delete].each do |method| + assert_nothing_raised { send(method, :index) } + end + end + end + + test "should not emit a csrf-token meta tag" do + SecureRandom.stub :base64, @token do + get :meta + assert_predicate @response.body, :blank? + end + end +end + +class CustomAuthenticityParamControllerTest < ActionController::TestCase + def setup + super + @old_logger = ActionController::Base.logger + @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + @token = Base64.strict_encode64(SecureRandom.random_bytes(32)) + @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token + ActionController::Base.request_forgery_protection_token = @token + end + + def teardown + ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token + super + end + + def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token + ActionController::Base.logger = @logger + begin + @controller.stub :valid_authenticity_token?, :true do + post :index, params: { custom_token_name: "foobar" } + assert_equal 0, @logger.logged(:warn).size + end + ensure + ActionController::Base.logger = @old_logger + end + end + + def test_should_warn_if_form_authenticity_param_does_not_match_form_authenticity_token + ActionController::Base.logger = @logger + + begin + post :index, params: { custom_token_name: "bazqux" } + assert_equal 1, @logger.logged(:warn).size + ensure + ActionController::Base.logger = @old_logger + end + end +end + +class PerFormTokensControllerTest < ActionController::TestCase + def setup + @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token + ActionController::Base.request_forgery_protection_token = :custom_authenticity_token + end + + def teardown + ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token + end + + def test_per_form_token_is_same_size_as_global_token + get :index + expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH + actual = @controller.send(:per_form_csrf_token, session, "/path", "post").size + assert_equal expected, actual + end + + def test_accepts_token_for_correct_path_and_method + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one" + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: form_token } + end + assert_response :success + end + + def test_rejects_token_for_incorrect_path + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_two" + assert_raises(ActionController::InvalidAuthenticityToken) do + post :post_two, params: { custom_authenticity_token: form_token } + end + end + + def test_rejects_token_for_incorrect_method + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one" + assert_raises(ActionController::InvalidAuthenticityToken) do + patch :post_one, params: { custom_authenticity_token: form_token } + end + end + + def test_rejects_token_for_incorrect_method_button_to + get :button_to, params: { form_method: "delete" } + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token, "delete" + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one" + assert_raises(ActionController::InvalidAuthenticityToken) do + patch :post_one, params: { custom_authenticity_token: form_token } + end + end + + test "Accepts proper token for implicit post method on button_to tag" do + get :button_to + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token, "post" + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one" + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: form_token } + end + end + + %w{delete post patch}.each do |verb| + test "Accepts proper token for #{verb} method on button_to tag" do + get :button_to, params: { form_method: verb } + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token, verb + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one" + assert_nothing_raised do + send verb, :post_one, params: { custom_authenticity_token: form_token } + end + end + end + + def test_accepts_global_csrf_token + get :index + + token = @controller.send(:form_authenticity_token) + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one" + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: token } + end + assert_response :success + end + + def test_ignores_params + get :index, params: { form_path: "/per_form_tokens/post_one?foo=bar" } + + form_token = assert_presence_and_fetch_form_csrf_token + + assert_matches_session_token_on_server form_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one?foo=baz" + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: form_token, baz: "foo" } + end + assert_response :success + end + + def test_ignores_trailing_slash_during_generation + get :index, params: { form_path: "/per_form_tokens/post_one/" } + + form_token = assert_presence_and_fetch_form_csrf_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one" + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: form_token } + end + assert_response :success + end + + def test_ignores_origin_during_generation + get :index, params: { form_path: "https://example.com/per_form_tokens/post_one/" } + + form_token = assert_presence_and_fetch_form_csrf_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one" + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: form_token } + end + assert_response :success + end + + def test_ignores_trailing_slash_during_validation + get :index + + form_token = assert_presence_and_fetch_form_csrf_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one/" + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: form_token } + end + assert_response :success + end + + def test_method_is_case_insensitive + get :index, params: { form_method: "POST" } + + form_token = assert_presence_and_fetch_form_csrf_token + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens/post_one/" + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: form_token } + end + assert_response :success + end + + private + def assert_presence_and_fetch_form_csrf_token + assert_select 'input[name="custom_authenticity_token"]' do |input| + form_csrf_token = input.first["value"] + assert_not_nil form_csrf_token + return form_csrf_token + end + end + + def assert_matches_session_token_on_server(form_token, method = "post") + actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) + expected = @controller.send(:per_form_csrf_token, session, "/per_form_tokens/post_one", method) + assert_equal expected, actual + end +end + +class SkipProtectionControllerTest < ActionController::TestCase + def test_should_not_allow_post_without_token_when_not_skipping + @controller.skip_requested = false + assert_blocked { post :index } + end + + def test_should_allow_post_without_token_when_skipping + @controller.skip_requested = true + assert_not_blocked { post :index } + end + + def assert_blocked + assert_raises(ActionController::InvalidAuthenticityToken) do + yield + end + end + + def assert_not_blocked + assert_nothing_raised { yield } + assert_response :success + end +end diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb new file mode 100644 index 0000000000..4a83d07e7d --- /dev/null +++ b/actionpack/test/controller/required_params_test.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class BooksController < ActionController::Base + def create + params.require(:book).require(:name) + head :ok + end +end + +class ActionControllerRequiredParamsTest < ActionController::TestCase + tests BooksController + + test "missing required parameters will raise exception" do + assert_raise ActionController::ParameterMissing do + post :create, params: { magazine: { name: "Mjallo!" } } + end + + assert_raise ActionController::ParameterMissing do + post :create, params: { book: { title: "Mjallo!" } } + end + end + + test "required parameters that are present will not raise" do + post :create, params: { book: { name: "Mjallo!" } } + assert_response :ok + end + + test "required parameters with false value will not raise" do + post :create, params: { book: { name: false } } + assert_response :ok + end +end + +class ParametersRequireTest < ActiveSupport::TestCase + test "required parameters should accept and return false value" do + assert_equal(false, ActionController::Parameters.new(person: false).require(:person)) + end + + test "required parameters must not be nil" do + assert_raises(ActionController::ParameterMissing) do + ActionController::Parameters.new(person: nil).require(:person) + end + end + + test "required parameters must not be empty" do + assert_raises(ActionController::ParameterMissing) do + ActionController::Parameters.new(person: {}).require(:person) + end + end + + test "require array when all required params are present" do + safe_params = ActionController::Parameters.new(person: { first_name: "Gaurish", title: "Mjallo", city: "Barcelona" }) + .require(:person) + .require([:first_name, :title]) + + assert_kind_of Array, safe_params + assert_equal ["Gaurish", "Mjallo"], safe_params + end + + test "require array when a required param is missing" do + assert_raises(ActionController::ParameterMissing) do + ActionController::Parameters.new(person: { first_name: "Gaurish", title: nil }) + .require(:person) + .require([:first_name, :title]) + end + end + + test "value params" do + params = ActionController::Parameters.new(foo: "bar", dog: "cinco") + assert_equal ["bar", "cinco"], params.values + assert params.has_value?("cinco") + assert params.value?("cinco") + end + + test "to_param works like in a Hash" do + params = ActionController::Parameters.new(nested: { key: "value" }).permit! + assert_equal({ nested: { key: "value" } }.to_param, params.to_param) + + params = { root: ActionController::Parameters.new(nested: { key: "value" }).permit! } + assert_equal({ root: { nested: { key: "value" } } }.to_param, params.to_param) + + assert_raise(ActionController::UnfilteredParameters) do + ActionController::Parameters.new(nested: { key: "value" }).to_param + end + end + + test "to_query works like in a Hash" do + params = ActionController::Parameters.new(nested: { key: "value" }).permit! + assert_equal({ nested: { key: "value" } }.to_query, params.to_query) + + params = { root: ActionController::Parameters.new(nested: { key: "value" }).permit! } + assert_equal({ root: { nested: { key: "value" } } }.to_query, params.to_query) + + assert_raise(ActionController::UnfilteredParameters) do + ActionController::Parameters.new(nested: { key: "value" }).to_query + end + end +end diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb new file mode 100644 index 0000000000..089b0b94d4 --- /dev/null +++ b/actionpack/test/controller/rescue_test.rb @@ -0,0 +1,364 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class RescueController < ActionController::Base + class NotAuthorized < StandardError + end + class NotAuthorizedToRescueAsString < StandardError + end + + class RecordInvalid < StandardError + end + class RecordInvalidToRescueAsString < StandardError + end + + class NotAllowed < StandardError + end + class NotAllowedToRescueAsString < StandardError + end + + class InvalidRequest < StandardError + end + class InvalidRequestToRescueAsString < StandardError + end + + class BadGateway < StandardError + end + class BadGatewayToRescueAsString < StandardError + end + + class ResourceUnavailable < StandardError + end + class ResourceUnavailableToRescueAsString < StandardError + end + + # We use a fully qualified name in some strings, and a relative constant + # name in some other to test correct handling of both cases. + + rescue_from NotAuthorized, with: :deny_access + rescue_from "RescueController::NotAuthorizedToRescueAsString", with: :deny_access + + rescue_from RecordInvalid, with: :show_errors + rescue_from "RescueController::RecordInvalidToRescueAsString", with: :show_errors + + rescue_from NotAllowed, with: proc { head :forbidden } + rescue_from "RescueController::NotAllowedToRescueAsString", with: proc { head :forbidden } + + rescue_from InvalidRequest, with: proc { |exception| render plain: exception.message } + rescue_from "InvalidRequestToRescueAsString", with: proc { |exception| render plain: exception.message } + + rescue_from BadGateway do + head 502 + end + rescue_from "BadGatewayToRescueAsString" do + head 502 + end + + rescue_from ResourceUnavailable do |exception| + render plain: exception.message + end + rescue_from "ResourceUnavailableToRescueAsString" do |exception| + render plain: exception.message + end + + rescue_from ActionDispatch::Http::Parameters::ParseError do + render plain: "parse error", status: :bad_request + end + + before_action(only: :before_action_raises) { raise "umm nice" } + + def before_action_raises + end + + def not_authorized + raise NotAuthorized + end + def not_authorized_raise_as_string + raise NotAuthorizedToRescueAsString + end + + def not_allowed + raise NotAllowed + end + def not_allowed_raise_as_string + raise NotAllowedToRescueAsString + end + + def invalid_request + raise InvalidRequest + end + def invalid_request_raise_as_string + raise InvalidRequestToRescueAsString + end + + def record_invalid + raise RecordInvalid + end + def record_invalid_raise_as_string + raise RecordInvalidToRescueAsString + end + + def bad_gateway + raise BadGateway + end + def bad_gateway_raise_as_string + raise BadGatewayToRescueAsString + end + + def resource_unavailable + raise ResourceUnavailable + end + def resource_unavailable_raise_as_string + raise ResourceUnavailableToRescueAsString + end + + def arbitrary_action + params + render plain: "arbitrary action" + end + + def missing_template + end + + def exception_with_more_specific_handler_for_wrapper + raise RecordInvalid + rescue + raise NotAuthorized + end + + def exception_with_more_specific_handler_for_cause + raise NotAuthorized + rescue + raise RecordInvalid + end + + def exception_with_no_handler_for_wrapper + raise RecordInvalid + rescue + raise RangeError + end + + private + def deny_access + head :forbidden + end + + def show_errors(exception) + head :unprocessable_entity + end +end + +class ExceptionInheritanceRescueController < ActionController::Base + class ParentException < StandardError + end + + class ChildException < ParentException + end + + class GrandchildException < ChildException + end + + rescue_from ChildException, with: lambda { head :ok } + rescue_from ParentException, with: lambda { head :created } + rescue_from GrandchildException, with: lambda { head :no_content } + + def raise_parent_exception + raise ParentException + end + + def raise_child_exception + raise ChildException + end + + def raise_grandchild_exception + raise GrandchildException + end +end + +class ExceptionInheritanceRescueControllerTest < ActionController::TestCase + def test_bottom_first + get :raise_grandchild_exception + assert_response :no_content + end + + def test_inheritance_works + get :raise_child_exception + assert_response :created + end +end + +class ControllerInheritanceRescueController < ExceptionInheritanceRescueController + class FirstExceptionInChildController < StandardError + end + + class SecondExceptionInChildController < StandardError + end + + rescue_from FirstExceptionInChildController, "SecondExceptionInChildController", with: lambda { head :gone } + + def raise_first_exception_in_child_controller + raise FirstExceptionInChildController + end + + def raise_second_exception_in_child_controller + raise SecondExceptionInChildController + end +end + +class ControllerInheritanceRescueControllerTest < ActionController::TestCase + def test_first_exception_in_child_controller + get :raise_first_exception_in_child_controller + assert_response :gone + end + + def test_second_exception_in_child_controller + get :raise_second_exception_in_child_controller + assert_response :gone + end + + def test_exception_in_parent_controller + get :raise_parent_exception + assert_response :created + end +end + +class RescueControllerTest < ActionController::TestCase + def test_rescue_handler + get :not_authorized + assert_response :forbidden + end + def test_rescue_handler_string + get :not_authorized_raise_as_string + assert_response :forbidden + end + + def test_rescue_handler_with_argument + assert_called_with @controller, :show_errors, [Exception] do + get :record_invalid + end + end + def test_rescue_handler_with_argument_as_string + assert_called_with @controller, :show_errors, [Exception] do + get :record_invalid_raise_as_string + end + end + + def test_proc_rescue_handler + get :not_allowed + assert_response :forbidden + end + def test_proc_rescue_handler_as_string + get :not_allowed_raise_as_string + assert_response :forbidden + end + + def test_proc_rescue_handle_with_argument + get :invalid_request + assert_equal "RescueController::InvalidRequest", @response.body + end + def test_proc_rescue_handle_with_argument_as_string + get :invalid_request_raise_as_string + assert_equal "RescueController::InvalidRequestToRescueAsString", @response.body + end + + def test_block_rescue_handler + get :bad_gateway + assert_response 502 + end + def test_block_rescue_handler_as_string + get :bad_gateway_raise_as_string + assert_response 502 + end + + def test_block_rescue_handler_with_argument + get :resource_unavailable + assert_equal "RescueController::ResourceUnavailable", @response.body + end + def test_block_rescue_handler_with_argument_as_string + get :resource_unavailable_raise_as_string + assert_equal "RescueController::ResourceUnavailableToRescueAsString", @response.body + end + + test "rescue when wrapper has more specific handler than cause" do + get :exception_with_more_specific_handler_for_wrapper + assert_response :forbidden + end + + test "rescue when cause has more specific handler than wrapper" do + get :exception_with_more_specific_handler_for_cause + assert_response :unprocessable_entity + end + + test "rescue when cause has handler, but wrapper doesnt" do + get :exception_with_no_handler_for_wrapper + assert_response :unprocessable_entity + end + + test "can rescue a ParseError" do + capture_log_output do + post :arbitrary_action, body: "{", as: :json + end + assert_response :bad_request + assert_equal "parse error", response.body + end + + private + + def capture_log_output + output = StringIO.new + request.set_header "action_dispatch.logger", ActiveSupport::Logger.new(output) + yield + output.string + end +end + +class RescueTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + class RecordInvalid < StandardError + def message + "invalid" + end + end + rescue_from RecordInvalid, with: :show_errors + + def foo + render plain: "foo" + end + + def invalid + raise RecordInvalid + end + + private + def show_errors(exception) + render plain: exception.message + end + end + + test "normal request" do + with_test_routing do + get "/foo" + assert_equal "foo", response.body + end + end + + test "rescue exceptions inside controller" do + with_test_routing do + get "/invalid" + assert_equal "invalid", response.body + end + end + + private + + def with_test_routing + with_routing do |set| + set.draw do + get "foo", to: ::RescueTest::TestController.action(:foo) + get "invalid", to: ::RescueTest::TestController.action(:invalid) + end + yield + end + end +end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb new file mode 100644 index 0000000000..d2146f12a5 --- /dev/null +++ b/actionpack/test/controller/resources_test.rb @@ -0,0 +1,1390 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/object/try" +require "active_support/core_ext/object/with_options" +require "active_support/core_ext/array/extract_options" + +class AdminController < ResourcesController; end +class MessagesController < ResourcesController; end +class ProductsController < ResourcesController; end +class ThreadsController < ResourcesController; end + +module Backoffice + class ProductsController < ResourcesController; end + class ImagesController < ResourcesController; end + + module Admin + class ProductsController < ResourcesController; end + class ImagesController < ResourcesController; end + end +end + +class ResourcesTest < ActionController::TestCase + def test_default_restful_routes + with_restful_routing :messages do + assert_simply_restful_for :messages + end + end + + def test_override_paths_for_member_and_collection_methods + collection_methods = { rss: :get, reorder: :post, csv: :post } + member_methods = { rss: :get, atom: :get, upload: :post, fix: :post } + path_names = { new: "nuevo", rss: "canal", fix: "corrigir" } + + with_restful_routing :messages, + collection: collection_methods, + member: member_methods, + path_names: path_names do + + assert_restful_routes_for :messages, + collection: collection_methods, + member: member_methods, + path_names: path_names do |options| + member_methods.each do |action, method| + assert_recognizes(options.merge(action: action.to_s, id: "1"), + path: "/messages/1/#{path_names[action] || action}", + method: method) + end + + collection_methods.each do |action, method| + assert_recognizes(options.merge(action: action.to_s), + path: "/messages/#{path_names[action] || action}", + method: method) + end + end + + assert_restful_named_routes_for :messages, + collection: collection_methods, + member: member_methods, + path_names: path_names do |options| + + collection_methods.each_key do |action| + assert_named_route "/messages/#{path_names[action] || action}", "#{action}_messages_path", action: action + end + + member_methods.each_key do |action| + assert_named_route "/messages/1/#{path_names[action] || action}", "#{action}_message_path", action: action, id: "1" + end + end + end + end + + def test_multiple_default_restful_routes + with_restful_routing :messages, :comments do + assert_simply_restful_for :messages + assert_simply_restful_for :comments + end + end + + def test_multiple_resources_with_options + expected_options = { controller: "threads", action: "index" } + + with_restful_routing :messages, :comments, expected_options.slice(:controller) do + assert_recognizes(expected_options, path: "comments") + assert_recognizes(expected_options, path: "messages") + end + end + + def test_with_custom_conditions + with_restful_routing :messages, conditions: { subdomain: "app" } do + assert @routes.recognize_path("/messages", method: :get, subdomain: "app") + end + end + + def test_irregular_id_with_no_constraints_should_raise_error + expected_options = { controller: "messages", action: "show", id: "1.1.1" } + + with_restful_routing :messages do + assert_raise(Assertion) do + assert_recognizes(expected_options, path: "messages/1.1.1", method: :get) + end + end + end + + def test_irregular_id_with_constraints_should_pass + expected_options = { controller: "messages", action: "show", id: "1.1.1" } + + with_restful_routing(:messages, constraints: { id: /[0-9]\.[0-9]\.[0-9]/ }) do + assert_recognizes(expected_options, path: "messages/1.1.1", method: :get) + end + end + + def test_with_path_prefix_constraints + expected_options = { controller: "messages", action: "show", thread_id: "1.1.1", id: "1" } + with_restful_routing :messages, path_prefix: "/thread/:thread_id", constraints: { thread_id: /[0-9]\.[0-9]\.[0-9]/ } do + assert_recognizes(expected_options, path: "thread/1.1.1/messages/1", method: :get) + end + end + + def test_irregular_id_constraints_should_get_passed_to_member_actions + expected_options = { controller: "messages", action: "custom", id: "1.1.1" } + + with_restful_routing(:messages, member: { custom: :get }, constraints: { id: /[0-9]\.[0-9]\.[0-9]/ }) do + assert_recognizes(expected_options, path: "messages/1.1.1/custom", method: :get) + end + end + + def test_with_path_prefix + with_restful_routing :messages, path_prefix: "/thread/:thread_id" do + assert_simply_restful_for :messages, path_prefix: "thread/5/", options: { thread_id: "5" } + end + end + + def test_multiple_with_path_prefix + with_restful_routing :messages, :comments, path_prefix: "/thread/:thread_id" do + assert_simply_restful_for :messages, path_prefix: "thread/5/", options: { thread_id: "5" } + assert_simply_restful_for :comments, path_prefix: "thread/5/", options: { thread_id: "5" } + end + end + + def test_with_name_prefix + with_restful_routing :messages, as: "post_messages" do + assert_simply_restful_for :messages, name_prefix: "post_" + end + end + + def test_with_collection_actions + actions = { "a" => :get, "b" => :put, "c" => :post, "d" => :delete, "e" => :patch } + + with_routing do |set| + set.draw do + resources :messages do + get :a, on: :collection + put :b, on: :collection + post :c, on: :collection + delete :d, on: :collection + patch :e, on: :collection + end + end + + assert_restful_routes_for :messages do |options| + actions.each do |action, method| + assert_recognizes(options.merge(action: action), path: "/messages/#{action}", method: method) + end + end + + assert_restful_named_routes_for :messages do + actions.each_key do |action| + assert_named_route "/messages/#{action}", "#{action}_messages_path", action: action + end + end + end + end + + def test_with_collection_actions_and_name_prefix + actions = { "a" => :get, "b" => :put, "c" => :post, "d" => :delete, "e" => :patch } + + with_routing do |set| + set.draw do + scope "/threads/:thread_id" do + resources :messages, as: "thread_messages" do + get :a, on: :collection + put :b, on: :collection + post :c, on: :collection + delete :d, on: :collection + patch :e, on: :collection + end + end + end + + assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options| + actions.each do |action, method| + assert_recognizes(options.merge(action: action), path: "/threads/1/messages/#{action}", method: method) + end + end + + assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do + actions.each_key do |action| + assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", action: action + end + end + end + end + + def test_with_collection_actions_and_name_prefix_and_member_action_with_same_name + actions = { "a" => :get } + + with_routing do |set| + set.draw do + scope "/threads/:thread_id" do + resources :messages, as: "thread_messages" do + get :a, on: :collection + get :a, on: :member + end + end + end + + assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options| + actions.each do |action, method| + assert_recognizes(options.merge(action: action), path: "/threads/1/messages/#{action}", method: method) + end + end + + assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do + actions.each_key do |action| + assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", action: action + end + end + end + end + + def test_with_collection_action_and_name_prefix_and_formatted + actions = { "a" => :get, "b" => :put, "c" => :post, "d" => :delete, "e" => :patch } + + with_routing do |set| + set.draw do + scope "/threads/:thread_id" do + resources :messages, as: "thread_messages" do + get :a, on: :collection + put :b, on: :collection + post :c, on: :collection + delete :d, on: :collection + patch :e, on: :collection + end + end + end + + assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options| + actions.each do |action, method| + assert_recognizes(options.merge(action: action, format: "xml"), path: "/threads/1/messages/#{action}.xml", method: method) + end + end + + assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do + actions.each_key do |action| + assert_named_route "/threads/1/messages/#{action}.xml", "#{action}_thread_messages_path", action: action, format: "xml" + end + end + end + end + + def test_with_member_action + [:patch, :put, :post].each do |method| + with_restful_routing :messages, member: { mark: method } do + mark_options = { action: "mark", id: "1" } + mark_path = "/messages/1/mark" + assert_restful_routes_for :messages do |options| + assert_recognizes(options.merge(mark_options), path: mark_path, method: method) + end + + assert_restful_named_routes_for :messages do + assert_named_route mark_path, :mark_message_path, mark_options + end + end + end + end + + def test_with_member_action_and_requirement + expected_options = { controller: "messages", action: "mark", id: "1.1.1" } + + with_restful_routing(:messages, constraints: { id: /[0-9]\.[0-9]\.[0-9]/ }, member: { mark: :get }) do + assert_recognizes(expected_options, path: "messages/1.1.1/mark", method: :get) + end + end + + def test_member_when_override_paths_for_default_restful_actions_with + [:patch, :put, :post].each do |method| + with_restful_routing :messages, member: { mark: method }, path_names: { new: "nuevo" } do + mark_options = { action: "mark", id: "1", controller: "messages" } + mark_path = "/messages/1/mark" + + assert_restful_routes_for :messages, path_names: { new: "nuevo" } do |options| + assert_recognizes(options.merge(mark_options), path: mark_path, method: method) + end + + assert_restful_named_routes_for :messages, path_names: { new: "nuevo" } do + assert_named_route mark_path, :mark_message_path, mark_options + end + end + end + end + + def test_with_two_member_actions_with_same_method + [:patch, :put, :post].each do |method| + with_routing do |set| + set.draw do + resources :messages do + member do + match :mark, via: method + match :unmark, via: method + end + end + end + + %w(mark unmark).each do |action| + action_options = { action: action, id: "1" } + action_path = "/messages/1/#{action}" + assert_restful_routes_for :messages do |options| + assert_recognizes(options.merge(action_options), path: action_path, method: method) + end + + assert_restful_named_routes_for :messages do + assert_named_route action_path, "#{action}_message_path".to_sym, action_options + end + end + end + end + end + + def test_array_as_collection_or_member_method_value + with_routing do |set| + set.draw do + resources :messages do + collection do + match :search, via: [:post, :get] + end + + member do + match :toggle, via: [:post, :get] + end + end + end + + assert_restful_routes_for :messages do |options| + [:get, :post].each do |method| + assert_recognizes(options.merge(action: "search"), path: "/messages/search", method: method) + end + [:get, :post].each do |method| + assert_recognizes(options.merge(action: "toggle", id: "1"), path: "/messages/1/toggle", method: method) + end + end + end + end + + def test_with_new_action + with_routing do |set| + set.draw do + resources :messages do + post :preview, on: :new + end + end + + preview_options = { action: "preview" } + preview_path = "/messages/new/preview" + assert_restful_routes_for :messages do |options| + assert_recognizes(options.merge(preview_options), path: preview_path, method: :post) + end + + assert_restful_named_routes_for :messages do + assert_named_route preview_path, :preview_new_message_path, preview_options + end + end + end + + def test_with_new_action_with_name_prefix + with_routing do |set| + set.draw do + scope("/threads/:thread_id") do + resources :messages, as: "thread_messages" do + post :preview, on: :new + end + end + end + + preview_options = { action: "preview", thread_id: "1" } + preview_path = "/threads/1/messages/new/preview" + assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options| + assert_recognizes(options.merge(preview_options), path: preview_path, method: :post) + end + + assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do + assert_named_route preview_path, :preview_new_thread_message_path, preview_options + end + end + end + + def test_with_formatted_new_action_with_name_prefix + with_routing do |set| + set.draw do + scope("/threads/:thread_id") do + resources :messages, as: "thread_messages" do + post :preview, on: :new + end + end + end + + preview_options = { action: "preview", thread_id: "1", format: "xml" } + preview_path = "/threads/1/messages/new/preview.xml" + assert_restful_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do |options| + assert_recognizes(options.merge(preview_options), path: preview_path, method: :post) + end + + assert_restful_named_routes_for :messages, path_prefix: "threads/1/", name_prefix: "thread_", options: { thread_id: "1" } do + assert_named_route preview_path, :preview_new_thread_message_path, preview_options + end + end + end + + def test_override_new_method + with_restful_routing :messages do + assert_restful_routes_for :messages do |options| + assert_recognizes(options.merge(action: "new"), path: "/messages/new", method: :get) + assert_raise(ActionController::RoutingError) do + @routes.recognize_path("/messages/new", method: :post) + end + end + end + + with_routing do |set| + set.draw do + resources :messages do + match :new, via: [:post, :get], on: :new + end + end + + assert_restful_routes_for :messages do |options| + assert_recognizes(options.merge(action: "new"), path: "/messages/new", method: :post) + assert_recognizes(options.merge(action: "new"), path: "/messages/new", method: :get) + end + end + end + + def test_nested_restful_routes + with_routing do |set| + set.draw do + resources :threads do + resources :messages do + resources :comments + end + end + end + + assert_simply_restful_for :threads + assert_simply_restful_for :messages, + name_prefix: "thread_", + path_prefix: "threads/1/", + options: { thread_id: "1" } + assert_simply_restful_for :comments, + name_prefix: "thread_message_", + path_prefix: "threads/1/messages/2/", + options: { thread_id: "1", message_id: "2" } + end + end + + def test_shallow_nested_restful_routes + with_routing do |set| + set.draw do + resources :threads, shallow: true do + resources :messages do + resources :comments + end + end + end + + assert_simply_restful_for :threads, + shallow: true + assert_simply_restful_for :messages, + name_prefix: "thread_", + path_prefix: "threads/1/", + shallow: true, + options: { thread_id: "1" } + assert_simply_restful_for :comments, + name_prefix: "message_", + path_prefix: "messages/2/", + shallow: true, + options: { message_id: "2" } + end + end + + def test_shallow_nested_restful_routes_with_namespaces + with_routing do |set| + set.draw do + namespace :backoffice do + namespace :admin do + resources :products, shallow: true do + resources :images + end + end + end + end + + assert_simply_restful_for :products, + controller: "backoffice/admin/products", + namespace: "backoffice/admin/", + name_prefix: "backoffice_admin_", + path_prefix: "backoffice/admin/", + shallow: true + assert_simply_restful_for :images, + controller: "backoffice/admin/images", + namespace: "backoffice/admin/", + name_prefix: "backoffice_admin_product_", + path_prefix: "backoffice/admin/products/1/", + shallow: true, + options: { product_id: "1" } + end + end + + def test_restful_routes_dont_generate_duplicates + with_restful_routing :messages do + routes = @routes.routes + routes.each do |route| + routes.each do |r| + next if route == r # skip the comparison instance + assert_not_equal [route.conditions, route.path.spec.to_s, route.verb], [r.conditions, r.path.spec.to_s, r.verb] + end + end + end + end + + def test_should_create_singleton_resource_routes + with_singleton_resources :account do + assert_singleton_restful_for :account + end + end + + def test_should_create_multiple_singleton_resource_routes + with_singleton_resources :account, :product do + assert_singleton_restful_for :account + assert_singleton_restful_for :product + end + end + + def test_should_create_nested_singleton_resource_routes + with_routing do |set| + set.draw do + resource :admin, controller: "admin" do + resource :account + end + end + + assert_singleton_restful_for :admin, controller: "admin" + assert_singleton_restful_for :account, name_prefix: "admin_", path_prefix: "admin/" + end + end + + def test_singleton_resource_with_member_action + [:patch, :put, :post].each do |method| + with_routing do |set| + set.draw do + resource :account do + match :reset, on: :member, via: method + end + end + + reset_options = { action: "reset" } + reset_path = "/account/reset" + assert_singleton_routes_for :account do |options| + assert_recognizes(options.merge(reset_options), path: reset_path, method: method) + end + + assert_singleton_named_routes_for :account do + assert_named_route reset_path, :reset_account_path, reset_options + end + end + end + end + + def test_singleton_resource_with_two_member_actions_with_same_method + [:patch, :put, :post].each do |method| + with_routing do |set| + set.draw do + resource :account do + match :reset, on: :member, via: method + match :disable, on: :member, via: method + end + end + + %w(reset disable).each do |action| + action_options = { action: action } + action_path = "/account/#{action}" + assert_singleton_routes_for :account do |options| + assert_recognizes(options.merge(action_options), path: action_path, method: method) + end + + assert_singleton_named_routes_for :account do + assert_named_route action_path, "#{action}_account_path".to_sym, action_options + end + end + end + end + end + + def test_should_nest_resources_in_singleton_resource + with_routing do |set| + set.draw do + resource :account do + resources :messages + end + end + + assert_singleton_restful_for :account + assert_simply_restful_for :messages, name_prefix: "account_", path_prefix: "account/" + end + end + + def test_should_nest_resources_in_singleton_resource_with_path_scope + with_routing do |set| + set.draw do + scope ":site_id" do + resource(:account) do + resources :messages + end + end + end + + assert_singleton_restful_for :account, path_prefix: "7/", options: { site_id: "7" } + assert_simply_restful_for :messages, name_prefix: "account_", path_prefix: "7/account/", options: { site_id: "7" } + end + end + + def test_should_nest_singleton_resource_in_resources + with_routing do |set| + set.draw do + resources :threads do + resource :admin, controller: "admin" + end + end + + assert_simply_restful_for :threads + assert_singleton_restful_for :admin, controller: "admin", name_prefix: "thread_", path_prefix: "threads/5/", options: { thread_id: "5" } + end + end + + def test_should_not_allow_delete_or_patch_or_put_on_collection_path + controller_name = :messages + with_restful_routing controller_name do + options = { controller: controller_name.to_s } + collection_path = "/#{controller_name}" + + assert_raise(Assertion) do + assert_recognizes(options.merge(action: "update"), path: collection_path, method: :patch) + end + + assert_raise(Assertion) do + assert_recognizes(options.merge(action: "update"), path: collection_path, method: :put) + end + + assert_raise(Assertion) do + assert_recognizes(options.merge(action: "destroy"), path: collection_path, method: :delete) + end + end + end + + def test_new_style_named_routes_for_resource + with_routing do |set| + set.draw do + scope "/threads/:thread_id" do + resources :messages, as: "thread_messages" do + get :search, on: :collection + get :preview, on: :new + end + end + end + + assert_simply_restful_for :messages, name_prefix: "thread_", path_prefix: "threads/1/", options: { thread_id: "1" } + assert_named_route "/threads/1/messages/search", "search_thread_messages_path", {} + assert_named_route "/threads/1/messages/new", "new_thread_message_path", {} + assert_named_route "/threads/1/messages/new/preview", "preview_new_thread_message_path", {} + end + end + + def test_new_style_named_routes_for_singleton_resource + with_routing do |set| + set.draw do + scope "/admin" do + resource :account, as: :admin_account do + get :login, on: :member + get :preview, on: :new + end + end + end + assert_singleton_restful_for :account, name_prefix: "admin_", path_prefix: "admin/" + assert_named_route "/admin/account/login", "login_admin_account_path", {} + assert_named_route "/admin/account/new", "new_admin_account_path", {} + assert_named_route "/admin/account/new/preview", "preview_new_admin_account_path", {} + end + end + + def test_resources_in_namespace + with_routing do |set| + set.draw do + namespace :backoffice do + resources :products + end + end + + assert_simply_restful_for :products, controller: "backoffice/products", name_prefix: "backoffice_", path_prefix: "backoffice/" + end + end + + def test_resources_in_nested_namespace + with_routing do |set| + set.draw do + namespace :backoffice do + namespace :admin do + resources :products + end + end + end + + assert_simply_restful_for :products, controller: "backoffice/admin/products", name_prefix: "backoffice_admin_", path_prefix: "backoffice/admin/" + end + end + + def test_resources_using_namespace + with_routing do |set| + set.draw do + namespace :backoffice, path: nil, as: nil do + resources :products + end + end + + assert_simply_restful_for :products, controller: "backoffice/products" + end + end + + def test_nested_resources_using_namespace + with_routing do |set| + set.draw do + namespace :backoffice do + resources :products do + resources :images + end + end + end + + assert_simply_restful_for :images, controller: "backoffice/images", name_prefix: "backoffice_product_", path_prefix: "backoffice/products/1/", options: { product_id: "1" } + end + end + + def test_nested_resources_in_nested_namespace + with_routing do |set| + set.draw do + namespace :backoffice do + namespace :admin do + resources :products do + resources :images + end + end + end + end + + assert_simply_restful_for :images, controller: "backoffice/admin/images", name_prefix: "backoffice_admin_product_", path_prefix: "backoffice/admin/products/1/", options: { product_id: "1" } + end + end + + def test_with_path_segment + with_restful_routing :messages do + assert_simply_restful_for :messages + assert_recognizes({ controller: "messages", action: "index" }, "/messages") + assert_recognizes({ controller: "messages", action: "index" }, "/messages/") + end + + with_routing do |set| + set.draw do + resources :messages, path: "reviews" + end + assert_simply_restful_for :messages, as: "reviews" + assert_recognizes({ controller: "messages", action: "index" }, "/reviews") + assert_recognizes({ controller: "messages", action: "index" }, "/reviews/") + end + end + + def test_multiple_with_path_segment_and_controller + with_routing do |set| + set.draw do + resources :products do + resources :product_reviews, path: "reviews", controller: "messages" + end + resources :tutors do + resources :tutor_reviews, path: "reviews", controller: "comments" + end + end + + assert_simply_restful_for :product_reviews, controller: "messages", as: "reviews", name_prefix: "product_", path_prefix: "products/1/", options: { product_id: "1" } + assert_simply_restful_for :tutor_reviews, controller: "comments", as: "reviews", name_prefix: "tutor_", path_prefix: "tutors/1/", options: { tutor_id: "1" } + end + end + + def test_with_path_segment_path_prefix_constraints + expected_options = { controller: "messages", action: "show", thread_id: "1.1.1", id: "1" } + with_routing do |set| + set.draw do + scope "/thread/:thread_id", constraints: { thread_id: /[0-9]\.[0-9]\.[0-9]/ } do + resources :messages, path: "comments" + end + end + assert_recognizes(expected_options, path: "thread/1.1.1/comments/1", method: :get) + end + end + + def test_resource_has_only_show_action + with_routing do |set| + set.draw do + resources :products, only: :show + end + + assert_resource_allowed_routes("products", {}, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy]) + assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy]) + end + end + + def test_singleton_resource_has_only_show_action + with_routing do |set| + set.draw do + resource :account, only: :show + end + + assert_singleton_resource_allowed_routes("accounts", {}, :show, [:index, :new, :create, :edit, :update, :destroy]) + assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :show, [:index, :new, :create, :edit, :update, :destroy]) + end + end + + def test_resource_does_not_have_destroy_action + with_routing do |set| + set.draw do + resources :products, except: :destroy + end + + assert_resource_allowed_routes("products", {}, { id: "1" }, [:index, :new, :create, :show, :edit, :update], :destroy) + assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, [:index, :new, :create, :show, :edit, :update], :destroy) + end + end + + def test_singleton_resource_does_not_have_destroy_action + with_routing do |set| + set.draw do + resource :account, except: :destroy + end + + assert_singleton_resource_allowed_routes("accounts", {}, [:new, :create, :show, :edit, :update], :destroy) + assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, [:new, :create, :show, :edit, :update], :destroy) + end + end + + def test_resource_has_show_action_but_does_not_have_destroy_action + with_routing do |set| + set.draw do + resources :products, only: [:show, :destroy], except: :destroy + end + + assert_resource_allowed_routes("products", {}, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy]) + assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :show, [:index, :new, :create, :edit, :update, :destroy]) + end + end + + def test_singleton_resource_has_show_action_but_does_not_have_destroy_action + with_routing do |set| + set.draw do + resource :account, only: [:show, :destroy], except: :destroy + end + + assert_singleton_resource_allowed_routes("accounts", {}, :show, [:new, :create, :edit, :update, :destroy]) + assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :show, [:new, :create, :edit, :update, :destroy]) + end + end + + def test_resource_has_only_create_action_and_named_route + with_routing do |set| + set.draw do + resources :products, only: :create + end + + assert_resource_allowed_routes("products", {}, { id: "1" }, :create, [:index, :new, :show, :edit, :update, :destroy]) + assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :create, [:index, :new, :show, :edit, :update, :destroy]) + + assert_not_nil set.named_routes[:products] + end + end + + def test_resource_has_only_update_action_and_named_route + with_routing do |set| + set.draw do + resources :products, only: :update + end + + assert_resource_allowed_routes("products", {}, { id: "1" }, :update, [:index, :new, :create, :show, :edit, :destroy]) + assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :update, [:index, :new, :create, :show, :edit, :destroy]) + + assert_not_nil set.named_routes[:product] + end + end + + def test_resource_has_only_destroy_action_and_named_route + with_routing do |set| + set.draw do + resources :products, only: :destroy + end + + assert_resource_allowed_routes("products", {}, { id: "1" }, :destroy, [:index, :new, :create, :show, :edit, :update]) + assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, :destroy, [:index, :new, :create, :show, :edit, :update]) + + assert_not_nil set.named_routes[:product] + end + end + + def test_singleton_resource_has_only_create_action_and_named_route + with_routing do |set| + set.draw do + resource :account, only: :create + end + + assert_singleton_resource_allowed_routes("accounts", {}, :create, [:new, :show, :edit, :update, :destroy]) + assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :create, [:new, :show, :edit, :update, :destroy]) + + assert_not_nil set.named_routes[:account] + end + end + + def test_singleton_resource_has_only_update_action_and_named_route + with_routing do |set| + set.draw do + resource :account, only: :update + end + + assert_singleton_resource_allowed_routes("accounts", {}, :update, [:new, :create, :show, :edit, :destroy]) + assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :update, [:new, :create, :show, :edit, :destroy]) + + assert_not_nil set.named_routes[:account] + end + end + + def test_singleton_resource_has_only_destroy_action_and_named_route + with_routing do |set| + set.draw do + resource :account, only: :destroy + end + + assert_singleton_resource_allowed_routes("accounts", {}, :destroy, [:new, :create, :show, :edit, :update]) + assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, :destroy, [:new, :create, :show, :edit, :update]) + + assert_not_nil set.named_routes[:account] + end + end + + def test_resource_has_only_collection_action + with_routing do |set| + set.draw do + resources :products, only: [] do + get :sale, on: :collection + end + end + + assert_resource_allowed_routes("products", {}, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy]) + assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy]) + + assert_recognizes({ controller: "products", action: "sale" }, { path: "products/sale", method: :get }) + assert_recognizes({ controller: "products", action: "sale", format: "xml" }, { path: "products/sale.xml", method: :get }) + end + end + + def test_resource_has_only_member_action + with_routing do |set| + set.draw do + resources :products, only: [] do + get :preview, on: :member + end + end + + assert_resource_allowed_routes("products", {}, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy]) + assert_resource_allowed_routes("products", { format: "xml" }, { id: "1" }, [], [:index, :new, :create, :show, :edit, :update, :destroy]) + + assert_recognizes({ controller: "products", action: "preview", id: "1" }, { path: "products/1/preview", method: :get }) + assert_recognizes({ controller: "products", action: "preview", id: "1", format: "xml" }, { path: "products/1/preview.xml", method: :get }) + end + end + + def test_singleton_resource_has_only_member_action + with_routing do |set| + set.draw do + resource :account, only: [] do + member do + get :signup + end + end + end + + assert_singleton_resource_allowed_routes("accounts", {}, [], [:new, :create, :show, :edit, :update, :destroy]) + assert_singleton_resource_allowed_routes("accounts", { format: "xml" }, [], [:new, :create, :show, :edit, :update, :destroy]) + + assert_recognizes({ controller: "accounts", action: "signup" }, { path: "account/signup", method: :get }) + assert_recognizes({ controller: "accounts", action: "signup", format: "xml" }, { path: "account/signup.xml", method: :get }) + end + end + + def test_nested_resource_has_only_show_and_member_action + with_routing do |set| + set.draw do + resources :products, only: [:index, :show] do + resources :images, only: :show do + get :thumbnail, on: :member + end + end + end + + assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, :show, [:index, :new, :create, :edit, :update, :destroy], "products/1/images") + assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, :show, [:index, :new, :create, :edit, :update, :destroy], "products/1/images") + + assert_recognizes({ controller: "images", action: "thumbnail", product_id: "1", id: "2" }, { path: "products/1/images/2/thumbnail", method: :get }) + assert_recognizes({ controller: "images", action: "thumbnail", product_id: "1", id: "2", format: "jpg" }, { path: "products/1/images/2/thumbnail.jpg", method: :get }) + end + end + + def test_nested_resource_does_not_inherit_only_option + with_routing do |set| + set.draw do + resources :products, only: :show do + resources :images, except: :destroy + end + end + + assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update], :destroy, "products/1/images") + assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update], :destroy, "products/1/images") + end + end + + def test_nested_resource_does_not_inherit_only_option_by_default + with_routing do |set| + set.draw do + resources :products, only: :show do + resources :images + end + end + + assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images") + assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images") + end + end + + def test_nested_resource_does_not_inherit_except_option + with_routing do |set| + set.draw do + resources :products, except: :show do + resources :images, only: :destroy + end + end + + assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, :destroy, [:index, :new, :create, :show, :edit, :update], "products/1/images") + assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, :destroy, [:index, :new, :create, :show, :edit, :update], "products/1/images") + end + end + + def test_nested_resource_does_not_inherit_except_option_by_default + with_routing do |set| + set.draw do + resources :products, except: :show do + resources :images + end + end + + assert_resource_allowed_routes("images", { product_id: "1" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images") + assert_resource_allowed_routes("images", { product_id: "1", format: "xml" }, { id: "2" }, [:index, :new, :create, :show, :edit, :update, :destroy], [], "products/1/images") + end + end + + def test_default_singleton_restful_route_uses_get + with_routing do |set| + set.draw do + resource :product + end + + assert_routing "/product", controller: "products", action: "show" + assert set.recognize_path("/product", method: :get) + end + end + + def test_assert_routing_accepts_all_as_a_valid_method + with_routing do |set| + set.draw do + match "/products", to: "products#show", via: :all + end + + assert_routing({ method: "all", path: "/products" }, { controller: "products", action: "show" }) + end + end + + def test_assert_routing_fails_when_not_all_http_methods_are_recognized + with_routing do |set| + set.draw do + match "/products", to: "products#show", via: [:get, :post, :put] + end + + assert_raises(Minitest::Assertion) do + assert_routing({ method: "all", path: "/products" }, { controller: "products", action: "show" }) + end + end + end + + def test_singleton_resource_name_is_not_singularized + with_singleton_resources(:products) do + assert_singleton_restful_for :products + end + end + + private + def with_restful_routing(*args) + options = args.extract_options! + collection_methods = options.delete(:collection) + member_methods = options.delete(:member) + path_prefix = options.delete(:path_prefix) + args.push(options) + + with_routing do |set| + set.draw do + scope(path_prefix || "") do + resources(*args) do + if collection_methods + collection do + collection_methods.each do |name, method| + send(method, name) + end + end + end + + if member_methods + member do + member_methods.each do |name, method| + send(method, name) + end + end + end + end + end + end + yield + end + end + + def with_singleton_resources(*args) + with_routing do |set| + set.draw { resource(*args) } + yield + end + end + + # runs assert_restful_routes_for and assert_restful_named_routes for on the controller_name and options, without passing a block. + def assert_simply_restful_for(controller_name, options = {}) + assert_restful_routes_for controller_name, options + assert_restful_named_routes_for controller_name, nil, options + end + + def assert_singleton_restful_for(singleton_name, options = {}) + assert_singleton_routes_for singleton_name, options + assert_singleton_named_routes_for singleton_name, options + end + + def assert_restful_routes_for(controller_name, options = {}) + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || controller_name.to_s + + if options[:shallow] + options[:shallow_options] ||= {} + options[:shallow_options][:controller] = route_options[:controller] + else + options[:shallow_options] = route_options + end + + new_action = @routes.resources_path_names[:new] || "new" + edit_action = @routes.resources_path_names[:edit] || "edit" + + if options[:path_names] + new_action = options[:path_names][:new] if options[:path_names][:new] + edit_action = options[:path_names][:edit] if options[:path_names][:edit] + end + + path = "#{options[:as] || controller_name}" + collection_path = "/#{options[:path_prefix]}#{path}" + shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}" + member_path = "#{shallow_path}/1" + new_path = "#{collection_path}/#{new_action}" + edit_member_path = "#{member_path}/#{edit_action}" + formatted_edit_member_path = "#{member_path}/#{edit_action}.xml" + + with_options(route_options) do |controller| + controller.assert_routing collection_path, action: "index" + controller.assert_routing new_path, action: "new" + controller.assert_routing "#{collection_path}.xml", action: "index", format: "xml" + controller.assert_routing "#{new_path}.xml", action: "new", format: "xml" + end + + with_options(options[:shallow_options]) do |controller| + controller.assert_routing member_path, action: "show", id: "1" + controller.assert_routing edit_member_path, action: "edit", id: "1" + controller.assert_routing "#{member_path}.xml", action: "show", id: "1", format: "xml" + controller.assert_routing formatted_edit_member_path, action: "edit", id: "1", format: "xml" + end + + assert_recognizes(route_options.merge(action: "index"), path: collection_path, method: :get) + assert_recognizes(route_options.merge(action: "new"), path: new_path, method: :get) + assert_recognizes(route_options.merge(action: "create"), path: collection_path, method: :post) + assert_recognizes(options[:shallow_options].merge(action: "show", id: "1"), path: member_path, method: :get) + assert_recognizes(options[:shallow_options].merge(action: "edit", id: "1"), path: edit_member_path, method: :get) + assert_recognizes(options[:shallow_options].merge(action: "update", id: "1"), path: member_path, method: :put) + assert_recognizes(options[:shallow_options].merge(action: "destroy", id: "1"), path: member_path, method: :delete) + + assert_recognizes(route_options.merge(action: "index", format: "xml"), path: "#{collection_path}.xml", method: :get) + assert_recognizes(route_options.merge(action: "new", format: "xml"), path: "#{new_path}.xml", method: :get) + assert_recognizes(route_options.merge(action: "create", format: "xml"), path: "#{collection_path}.xml", method: :post) + assert_recognizes(options[:shallow_options].merge(action: "show", id: "1", format: "xml"), path: "#{member_path}.xml", method: :get) + assert_recognizes(options[:shallow_options].merge(action: "edit", id: "1", format: "xml"), path: formatted_edit_member_path, method: :get) + assert_recognizes(options[:shallow_options].merge(action: "update", id: "1", format: "xml"), path: "#{member_path}.xml", method: :put) + assert_recognizes(options[:shallow_options].merge(action: "destroy", id: "1", format: "xml"), path: "#{member_path}.xml", method: :delete) + + yield route_options if block_given? + end + + # test named routes like foo_path and foos_path map to the correct options. + def assert_restful_named_routes_for(controller_name, singular_name = nil, options = {}) + if singular_name.is_a?(Hash) + options = singular_name + singular_name = nil + end + singular_name ||= controller_name.to_s.singularize + + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || controller_name.to_s + + if options[:shallow] + options[:shallow_options] ||= {} + options[:shallow_options][:controller] = route_options[:controller] + else + options[:shallow_options] = route_options + end + + @controller = "#{route_options[:controller].camelize}Controller".constantize.new + @controller.singleton_class.include(@routes.url_helpers) + get :index, params: route_options + route_options.delete :action + + path = "#{options[:as] || controller_name}" + shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}" + full_path = "/#{options[:path_prefix]}#{path}" + name_prefix = options[:name_prefix] + shallow_prefix = options[:shallow] ? options[:namespace].try(:gsub, /\//, "_") : options[:name_prefix] + + new_action = "new" + edit_action = "edit" + if options[:path_names] + new_action = options[:path_names][:new] || "new" + edit_action = options[:path_names][:edit] || "edit" + end + + assert_named_route "#{full_path}", "#{name_prefix}#{controller_name}_path", route_options + assert_named_route "#{full_path}.xml", "#{name_prefix}#{controller_name}_path", route_options.merge(format: "xml") + assert_named_route "#{shallow_path}/1", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1") + assert_named_route "#{shallow_path}/1.xml", "#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1", format: "xml") + + assert_named_route "#{full_path}/#{new_action}", "new_#{name_prefix}#{singular_name}_path", route_options + assert_named_route "#{full_path}/#{new_action}.xml", "new_#{name_prefix}#{singular_name}_path", route_options.merge(format: "xml") + assert_named_route "#{shallow_path}/1/#{edit_action}", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1") + assert_named_route "#{shallow_path}/1/#{edit_action}.xml", "edit_#{shallow_prefix}#{singular_name}_path", options[:shallow_options].merge(id: "1", format: "xml") + + yield route_options if block_given? + end + + def assert_singleton_routes_for(singleton_name, options = {}) + route_options = (options[:options] ||= {}).dup + route_options[:controller] = options[:controller] || singleton_name.to_s.pluralize + + full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}" + new_path = "#{full_path}/new" + edit_path = "#{full_path}/edit" + formatted_edit_path = "#{full_path}/edit.xml" + + with_options route_options do |controller| + controller.assert_routing full_path, action: "show" + controller.assert_routing new_path, action: "new" + controller.assert_routing edit_path, action: "edit" + controller.assert_routing "#{full_path}.xml", action: "show", format: "xml" + controller.assert_routing "#{new_path}.xml", action: "new", format: "xml" + controller.assert_routing formatted_edit_path, action: "edit", format: "xml" + end + + assert_recognizes(route_options.merge(action: "show"), path: full_path, method: :get) + assert_recognizes(route_options.merge(action: "new"), path: new_path, method: :get) + assert_recognizes(route_options.merge(action: "edit"), path: edit_path, method: :get) + assert_recognizes(route_options.merge(action: "create"), path: full_path, method: :post) + assert_recognizes(route_options.merge(action: "update"), path: full_path, method: :put) + assert_recognizes(route_options.merge(action: "destroy"), path: full_path, method: :delete) + + assert_recognizes(route_options.merge(action: "show", format: "xml"), path: "#{full_path}.xml", method: :get) + assert_recognizes(route_options.merge(action: "new", format: "xml"), path: "#{new_path}.xml", method: :get) + assert_recognizes(route_options.merge(action: "edit", format: "xml"), path: formatted_edit_path, method: :get) + assert_recognizes(route_options.merge(action: "create", format: "xml"), path: "#{full_path}.xml", method: :post) + assert_recognizes(route_options.merge(action: "update", format: "xml"), path: "#{full_path}.xml", method: :put) + assert_recognizes(route_options.merge(action: "destroy", format: "xml"), path: "#{full_path}.xml", method: :delete) + + yield route_options if block_given? + end + + def assert_singleton_named_routes_for(singleton_name, options = {}) + route_options = (options[:options] ||= {}).dup + controller_name = route_options[:controller] || options[:controller] || singleton_name.to_s.pluralize + @controller = "#{controller_name.camelize}Controller".constantize.new + @controller.singleton_class.include(@routes.url_helpers) + get :show, params: route_options + route_options.delete :action + + full_path = "/#{options[:path_prefix]}#{options[:as] || singleton_name}" + name_prefix = options[:name_prefix] + + assert_named_route "#{full_path}", "#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}.xml", "#{name_prefix}#{singleton_name}_path", route_options.merge(format: "xml") + + assert_named_route "#{full_path}/new", "new_#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}/new.xml", "new_#{name_prefix}#{singleton_name}_path", route_options.merge(format: "xml") + assert_named_route "#{full_path}/edit", "edit_#{name_prefix}#{singleton_name}_path", route_options + assert_named_route "#{full_path}/edit.xml", "edit_#{name_prefix}#{singleton_name}_path", route_options.merge(format: "xml") + end + + def assert_named_route(expected, route, options) + actual = @controller.send(route, options) rescue $!.class.name + assert_equal expected, actual, "Error on route: #{route}(#{options.inspect})" + end + + def assert_resource_methods(expected, resource, action_method, method) + assert_equal expected.length, resource.send("#{action_method}_methods")[method].size, "#{resource.send("#{action_method}_methods")[method].inspect}" + expected.each do |action| + assert_includes resource.send("#{action_method}_methods")[method], action, + "#{method} not in #{action_method} methods: #{resource.send("#{action_method}_methods")[method].inspect}" + end + end + + def assert_resource_allowed_routes(controller, options, shallow_options, allowed, not_allowed, path = controller) + shallow_path = "#{path}/#{shallow_options[:id]}" + format = options[:format] && ".#{options[:format]}" + options[:controller] = controller + shallow_options.merge!(options) + + assert_whether_allowed(allowed, not_allowed, options, "index", "#{path}#{format}", :get) + assert_whether_allowed(allowed, not_allowed, options, "new", "#{path}/new#{format}", :get) + assert_whether_allowed(allowed, not_allowed, options, "create", "#{path}#{format}", :post) + assert_whether_allowed(allowed, not_allowed, shallow_options, "show", "#{shallow_path}#{format}", :get) + assert_whether_allowed(allowed, not_allowed, shallow_options, "edit", "#{shallow_path}/edit#{format}", :get) + assert_whether_allowed(allowed, not_allowed, shallow_options, "update", "#{shallow_path}#{format}", :put) + assert_whether_allowed(allowed, not_allowed, shallow_options, "destroy", "#{shallow_path}#{format}", :delete) + end + + def assert_singleton_resource_allowed_routes(controller, options, allowed, not_allowed, path = controller.singularize) + format = options[:format] && ".#{options[:format]}" + options[:controller] = controller + + assert_whether_allowed(allowed, not_allowed, options, "new", "#{path}/new#{format}", :get) + assert_whether_allowed(allowed, not_allowed, options, "create", "#{path}#{format}", :post) + assert_whether_allowed(allowed, not_allowed, options, "show", "#{path}#{format}", :get) + assert_whether_allowed(allowed, not_allowed, options, "edit", "#{path}/edit#{format}", :get) + assert_whether_allowed(allowed, not_allowed, options, "update", "#{path}#{format}", :put) + assert_whether_allowed(allowed, not_allowed, options, "destroy", "#{path}#{format}", :delete) + end + + def assert_whether_allowed(allowed, not_allowed, options, action, path, method) + action = action.to_sym + options = options.merge(action: action.to_s) + path_options = { path: path, method: method } + + if Array(allowed).include?(action) + assert_recognizes options, path_options + elsif Array(not_allowed).include?(action) + assert_not_recognizes options, path_options + else + raise Assertion, "Invalid Action has passed" + end + end + + def assert_not_recognizes(expected_options, path) + assert_raise Assertion do + assert_recognizes(expected_options, path) + end + end +end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb new file mode 100644 index 0000000000..b378bb80b8 --- /dev/null +++ b/actionpack/test/controller/routing_test.rb @@ -0,0 +1,2105 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_controllers" +require "active_support/core_ext/object/with_options" +require "active_support/core_ext/object/json" + +class MilestonesController < ActionController::Base + def index() head :ok end + alias_method :show, :index +end + +# See RFC 3986, section 3.3 for allowed path characters. +class UriReservedCharactersRoutingTest < ActiveSupport::TestCase + include RoutingTestHelpers + + def setup + @set = ActionDispatch::Routing::RouteSet.new + @set.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action/:variable/*additional" + end + end + + safe, unsafe = %w(: @ & = + $ , ;), %w(^ ? # [ ]) + hex = unsafe.map { |char| "%" + char.unpack1("H2").upcase } + + @segment = "#{safe.join}#{unsafe.join}" + @escaped = "#{safe.join}#{hex.join}" + end + + def test_route_generation_escapes_unsafe_path_characters + assert_equal "/content/act#{@escaped}ion/var#{@escaped}iable/add#{@escaped}itional-1/add#{@escaped}itional-2", + url_for(@set, + controller: "content", + action: "act#{@segment}ion", + variable: "var#{@segment}iable", + additional: ["add#{@segment}itional-1", "add#{@segment}itional-2"]) + end + + def test_route_recognition_unescapes_path_components + options = { controller: "content", + action: "act#{@segment}ion", + variable: "var#{@segment}iable", + additional: "add#{@segment}itional-1/add#{@segment}itional-2" } + assert_equal options, @set.recognize_path("/content/act#{@escaped}ion/var#{@escaped}iable/add#{@escaped}itional-1/add#{@escaped}itional-2") + end + + def test_route_generation_allows_passing_non_string_values_to_generated_helper + assert_equal "/content/action/variable/1/2", + url_for(@set, + controller: "content", + action: "action", + variable: "variable", + additional: [1, 2]) + end +end + +class MockController + def self.build(helpers, additional_options = {}) + Class.new do + define_method :url_options do + options = super() + options[:protocol] ||= "http" + options[:host] ||= "test.host" + options.merge(additional_options) + end + + include helpers + end + end +end + +class LegacyRouteSetTests < ActiveSupport::TestCase + include RoutingTestHelpers + include ActionDispatch::RoutingVerbs + + attr_reader :rs + attr_accessor :controller + alias :routes :rs + + def setup + @rs = make_set + @response = nil + end + + def test_symbols_with_dashes + rs.draw do + get "/:artist/:song-omg", to: lambda { |env| + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters + [200, {}, [resp]] + } + end + + hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/faithfully-omg")) + assert_equal({ "artist" => "journey", "song" => "faithfully" }, hash) + end + + def test_id_encoding + rs.draw do + get "/journey/:id", to: lambda { |env| + param = ActionDispatch::Request.new(env).path_parameters + resp = ActiveSupport::JSON.encode param + [200, {}, [resp]] + } + end + + # The encoding of the URL in production is *binary*, so we add a + # .b here. + hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/%E5%A4%AA%E9%83%8E".b)) + assert_equal({ "id" => "太郎" }, hash) + assert_equal ::Encoding::UTF_8, hash["id"].encoding + end + + def test_id_with_dash + rs.draw do + get "/journey/:id", to: lambda { |env| + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters + [200, {}, [resp]] + } + end + + hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/faithfully-omg")) + assert_equal({ "id" => "faithfully-omg" }, hash) + end + + def test_dash_with_custom_regexp + rs.draw do + get "/:artist/:song-omg", constraints: { song: /\d+/ }, to: lambda { |env| + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters + [200, {}, [resp]] + } + end + + hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/123-omg")) + assert_equal({ "artist" => "journey", "song" => "123" }, hash) + assert_equal "Not Found", get(URI("http://example.org/journey/faithfully-omg")) + end + + def test_pre_dash + rs.draw do + get "/:artist/omg-:song", to: lambda { |env| + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters + [200, {}, [resp]] + } + end + + hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/omg-faithfully")) + assert_equal({ "artist" => "journey", "song" => "faithfully" }, hash) + end + + def test_pre_dash_with_custom_regexp + rs.draw do + get "/:artist/omg-:song", constraints: { song: /\d+/ }, to: lambda { |env| + resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters + [200, {}, [resp]] + } + end + + hash = ActiveSupport::JSON.decode get(URI("http://example.org/journey/omg-123")) + assert_equal({ "artist" => "journey", "song" => "123" }, hash) + assert_equal "Not Found", get(URI("http://example.org/journey/omg-faithfully")) + end + + def test_star_paths_are_greedy + rs.draw do + get "/*path", to: lambda { |env| + x = env["action_dispatch.request.path_parameters"][:path] + [200, {}, [x]] + }, format: false + end + + u = URI("http://example.org/foo/bar.html") + assert_equal u.path.sub(/^\//, ""), get(u) + end + + def test_star_paths_are_greedy_but_not_too_much + rs.draw do + get "/*path", to: lambda { |env| + x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"] + [200, {}, [x]] + } + end + + expected = { "path" => "foo/bar", "format" => "html" } + u = URI("http://example.org/foo/bar.html") + assert_equal expected, ActiveSupport::JSON.decode(get(u)) + end + + def test_optional_star_paths_are_greedy + rs.draw do + get "/(*filters)", to: lambda { |env| + x = env["action_dispatch.request.path_parameters"][:filters] + [200, {}, [x]] + }, format: false + end + + u = URI("http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794") + assert_equal u.path.sub(/^\//, ""), get(u) + end + + def test_optional_star_paths_are_greedy_but_not_too_much + rs.draw do + get "/(*filters)", to: lambda { |env| + x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"] + [200, {}, [x]] + } + end + + expected = { "filters" => "ne_27.065938,-80.6092/sw_25.489856,-82", + "format" => "542794" } + u = URI("http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794") + assert_equal expected, ActiveSupport::JSON.decode(get(u)) + end + + def test_regexp_precedence + rs.draw do + get "/whois/:domain", constraints: { + domain: /\w+\.[\w\.]+/ }, + to: lambda { |env| [200, {}, %w{regexp}] } + + get "/whois/:id", to: lambda { |env| [200, {}, %w{id}] } + end + + assert_equal "regexp", get(URI("http://example.org/whois/example.org")) + assert_equal "id", get(URI("http://example.org/whois/123")) + end + + def test_class_and_lambda_constraints + subdomain = Class.new { + def matches?(request) + request.subdomain.present? && request.subdomain != "clients" + end + } + + rs.draw do + get "/", constraints: subdomain.new, + to: lambda { |env| [200, {}, %w{default}] } + get "/", constraints: { subdomain: "clients" }, + to: lambda { |env| [200, {}, %w{clients}] } + end + + assert_equal "default", get(URI("http://www.example.org/")) + assert_equal "clients", get(URI("http://clients.example.org/")) + end + + def test_lambda_constraints + rs.draw do + get "/", constraints: lambda { |req| + req.subdomain.present? && req.subdomain != "clients" }, + to: lambda { |env| [200, {}, %w{default}] } + + get "/", constraints: lambda { |req| + req.subdomain.present? && req.subdomain == "clients" }, + to: lambda { |env| [200, {}, %w{clients}] } + end + + assert_equal "default", get(URI("http://www.example.org/")) + assert_equal "clients", get(URI("http://clients.example.org/")) + end + + def test_scoped_lambda + scope_called = false + rs.draw do + scope "/foo", constraints: lambda { |req| scope_called = true } do + get "/", to: lambda { |env| [200, {}, %w{default}] } + end + end + + assert_equal "default", get(URI("http://www.example.org/foo/")) + assert scope_called, "scope constraint should be called" + end + + def test_scoped_lambda_with_get_lambda + inner_called = false + + rs.draw do + scope "/foo", constraints: lambda { |req| flunk "should not be called" } do + get "/", constraints: lambda { |req| inner_called = true }, + to: lambda { |env| [200, {}, %w{default}] } + end + end + + assert_equal "default", get(URI("http://www.example.org/foo/")) + assert inner_called, "inner constraint should be called" + end + + def test_empty_string_match + rs.draw do + get "/:username", constraints: { username: /[^\/]+/ }, + to: lambda { |e| [200, {}, ["foo"]] } + end + assert_equal "Not Found", get(URI("http://example.org/")) + assert_equal "foo", get(URI("http://example.org/hello")) + end + + def test_non_greedy_glob_regexp + params = nil + rs.draw do + get "/posts/:id(/*filters)", constraints: { filters: /.+?/ }, + to: lambda { |e| + params = e["action_dispatch.request.path_parameters"] + [200, {}, ["foo"]] + } + end + assert_equal "foo", get(URI("http://example.org/posts/1/foo.js")) + assert_equal({ id: "1", filters: "foo", format: "js" }, params) + end + + def test_specific_controller_action_failure + rs.draw do + mount lambda { } => "/foo" + end + + assert_raises(ActionController::UrlGenerationError) do + url_for(rs, controller: "omg", action: "lol") + end + end + + def test_default_setup + rs.draw { ActiveSupport::Deprecation.silence { get "/:controller(/:action(/:id))" } } + assert_equal({ controller: "content", action: "index" }, rs.recognize_path("/content")) + assert_equal({ controller: "content", action: "list" }, rs.recognize_path("/content/list")) + assert_equal({ controller: "content", action: "show", id: "10" }, rs.recognize_path("/content/show/10")) + + assert_equal({ controller: "admin/user", action: "show", id: "10" }, rs.recognize_path("/admin/user/show/10")) + + assert_equal "/admin/user/show/10", url_for(rs, controller: "admin/user", action: "show", id: 10) + + get URI("http://test.host/admin/user/list/10") + + assert_equal({ controller: "admin/user", action: "list", id: "10" }, + controller.request.path_parameters) + + assert_equal "/admin/user/show", controller.url_for(action: "show", only_path: true) + assert_equal "/admin/user/list/10", controller.url_for(only_path: true) + + assert_equal "/admin/stuff", controller.url_for(controller: "stuff", only_path: true) + assert_equal "/stuff", controller.url_for(controller: "/stuff", only_path: true) + end + + def test_route_with_colon_first + rs.draw do + ActiveSupport::Deprecation.silence do + get "/:controller/:action/:id", action: "index", id: nil + end + + get ":url", controller: "content", action: "translate" + end + + assert_equal({ controller: "content", action: "translate", url: "example" }, rs.recognize_path("/example")) + end + + def test_route_with_regexp_for_action + rs.draw { ActiveSupport::Deprecation.silence { get "/:controller/:action", action: /auth[-|_].+/ } } + + assert_equal({ action: "auth_google", controller: "content" }, rs.recognize_path("/content/auth_google")) + assert_equal({ action: "auth-twitter", controller: "content" }, rs.recognize_path("/content/auth-twitter")) + + assert_equal "/content/auth_google", url_for(rs, controller: "content", action: "auth_google") + assert_equal "/content/auth-twitter", url_for(rs, controller: "content", action: "auth-twitter") + end + + def test_route_with_regexp_for_controller + rs.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:admintoken(/:action(/:id))", controller: /admin\/.+/ + get "/:controller(/:action(/:id))" + end + end + + assert_equal({ controller: "admin/user", admintoken: "foo", action: "index" }, + rs.recognize_path("/admin/user/foo")) + assert_equal({ controller: "content", action: "foo" }, + rs.recognize_path("/content/foo")) + + assert_equal "/admin/user/foo", url_for(rs, controller: "admin/user", admintoken: "foo", action: "index") + assert_equal "/content/foo", url_for(rs, controller: "content", action: "foo") + end + + def test_route_with_regexp_and_captures_for_controller + rs.draw do + ActiveSupport::Deprecation.silence do + get "/:controller(/:action(/:id))", controller: /admin\/(accounts|users)/ + end + end + assert_equal({ controller: "admin/accounts", action: "index" }, rs.recognize_path("/admin/accounts")) + assert_equal({ controller: "admin/users", action: "index" }, rs.recognize_path("/admin/users")) + assert_raise(ActionController::RoutingError) { rs.recognize_path("/admin/products") } + end + + def test_route_with_regexp_and_dot + rs.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action/:file", + controller: /admin|user/, + action: /upload|download/, + defaults: { file: nil }, + constraints: { file: %r{[^/]+(\.[^/]+)?} } + end + end + # Without a file extension + assert_equal "/user/download/file", + url_for(rs, controller: "user", action: "download", file: "file") + + assert_equal({ controller: "user", action: "download", file: "file" }, + rs.recognize_path("/user/download/file")) + + # Now, let's try a file with an extension, really a dot (.) + assert_equal "/user/download/file.jpg", + url_for(rs, controller: "user", action: "download", file: "file.jpg") + + assert_equal({ controller: "user", action: "download", file: "file.jpg" }, + rs.recognize_path("/user/download/file.jpg")) + end + + def test_basic_named_route + rs.draw do + root to: "content#list", as: "home" + end + assert_equal("http://test.host/", setup_for_named_route.send(:home_url)) + end + + def test_named_route_with_option + rs.draw do + get "page/:title" => "content#show_page", :as => "page" + end + + assert_equal("http://test.host/page/new%20stuff", + setup_for_named_route.send(:page_url, title: "new stuff")) + end + + def test_named_route_with_default + rs.draw do + get "page/:title" => "content#show_page", :title => "AboutPage", :as => "page" + end + + assert_equal("http://test.host/page/AboutRails", + setup_for_named_route.send(:page_url, title: "AboutRails")) + end + + def test_named_route_with_path_prefix + rs.draw do + scope "my" do + get "page" => "content#show_page", :as => "page" + end + end + + assert_equal("http://test.host/my/page", + setup_for_named_route.send(:page_url)) + end + + def test_named_route_with_blank_path_prefix + rs.draw do + scope "" do + get "page" => "content#show_page", :as => "page" + end + end + + assert_equal("http://test.host/page", + setup_for_named_route.send(:page_url)) + end + + def test_named_route_with_nested_controller + rs.draw do + get "admin/user" => "admin/user#index", :as => "users" + end + + assert_equal("http://test.host/admin/user", + setup_for_named_route.send(:users_url)) + end + + def test_optimised_named_route_with_host + rs.draw do + get "page" => "content#show_page", :as => "pages", :host => "foo.com" + end + routes = setup_for_named_route + assert_equal "http://foo.com/page", routes.pages_url + end + + def setup_for_named_route(options = {}) + MockController.build(rs.url_helpers, options).new + end + + def test_named_route_without_hash + rs.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id", as: "normal" + end + end + end + + def test_named_route_root + rs.draw do + root to: "hello#index" + end + routes = setup_for_named_route + assert_equal("http://test.host/", routes.send(:root_url)) + assert_equal("/", routes.send(:root_path)) + end + + def test_named_route_root_without_hash + rs.draw do + root "hello#index" + end + routes = setup_for_named_route + assert_equal("http://test.host/", routes.send(:root_url)) + assert_equal("/", routes.send(:root_path)) + end + + def test_named_route_root_with_hash + rs.draw do + root "hello#index", as: :index + end + + routes = setup_for_named_route + assert_equal("http://test.host/", routes.send(:index_url)) + assert_equal("/", routes.send(:index_path)) + end + + def test_root_without_path_raises_argument_error + assert_raises ArgumentError do + rs.draw { root nil } + end + end + + def test_named_route_root_with_trailing_slash + rs.draw do + root "hello#index" + end + + routes = setup_for_named_route(trailing_slash: true) + assert_equal("http://test.host/", routes.send(:root_url)) + assert_equal("http://test.host/?foo=bar", routes.send(:root_url, foo: :bar)) + end + + def test_named_route_with_regexps + rs.draw do + get "page/:year/:month/:day/:title" => "page#show", :as => "article", + :year => /\d+/, :month => /\d+/, :day => /\d+/ + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + + routes = setup_for_named_route + + assert_equal "http://test.host/page/2005/6/10/hi", + routes.send(:article_url, title: "hi", day: 10, year: 2005, month: 6) + end + + def test_changing_controller + rs.draw { ActiveSupport::Deprecation.silence { get ":controller/:action/:id" } } + + get URI("http://test.host/admin/user/index/10") + + assert_equal "/admin/stuff/show/10", + controller.url_for(controller: "stuff", action: "show", id: 10, only_path: true) + end + + def test_paths_escaped + rs.draw do + get "file/*path" => "content#show_file", :as => "path" + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + + # No + to space in URI escaping, only for query params. + results = rs.recognize_path "/file/hello+world/how+are+you%3F" + assert results, "Recognition should have succeeded" + assert_equal "hello+world/how+are+you?", results[:path] + + # Use %20 for space instead. + results = rs.recognize_path "/file/hello%20world/how%20are%20you%3F" + assert results, "Recognition should have succeeded" + assert_equal "hello world/how are you?", results[:path] + end + + def test_paths_slashes_unescaped_with_ordered_parameters + rs.draw do + get "/file/*path" => "content#index", :as => "path" + end + + # No / to %2F in URI, only for query params. + assert_equal("/file/hello/world", setup_for_named_route.send(:path_path, ["hello", "world"])) + end + + def test_non_controllers_cannot_be_matched + rs.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + assert_raise(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") } + end + + def test_should_list_options_diff_when_routing_constraints_dont_match + rs.draw do + get "post/:id" => "post#show", :constraints => { id: /\d+/ }, :as => "post" + end + assert_raise(ActionController::UrlGenerationError) do + url_for(rs, controller: "post", action: "show", bad_param: "foo", use_route: "post") + end + end + + def test_dynamic_path_allowed + rs.draw do + get "*path" => "content#show_file" + end + + assert_equal "/pages/boo", + url_for(rs, controller: "content", action: "show_file", path: %w(pages boo)) + end + + def test_dynamic_recall_paths_allowed + rs.draw do + get "*path" => "content#show_file" + end + + get URI("http://test.host/pages/boo") + assert_equal({ controller: "content", action: "show_file", path: "pages/boo" }, + controller.request.path_parameters) + + assert_equal "/pages/boo", + controller.url_for(only_path: true) + end + + def test_backwards + rs.draw do + ActiveSupport::Deprecation.silence do + get "page/:id(/:action)" => "pages#show" + get ":controller(/:action(/:id))" + end + end + + get URI("http://test.host/pages/show") + assert_equal "/page/20", controller.url_for(id: 20, only_path: true) + assert_equal "/page/20", url_for(rs, controller: "pages", id: 20, action: "show") + assert_equal "/pages/boo", url_for(rs, controller: "pages", action: "boo") + end + + def test_route_with_integer_default + rs.draw do + get "page(/:id)" => "content#show_page", :id => 1 + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + + assert_equal "/page", url_for(rs, controller: "content", action: "show_page") + assert_equal "/page", url_for(rs, controller: "content", action: "show_page", id: 1) + assert_equal "/page", url_for(rs, controller: "content", action: "show_page", id: "1") + assert_equal "/page/10", url_for(rs, controller: "content", action: "show_page", id: 10) + + assert_equal({ controller: "content", action: "show_page", id: 1 }, rs.recognize_path("/page")) + assert_equal({ controller: "content", action: "show_page", id: "1" }, rs.recognize_path("/page/1")) + assert_equal({ controller: "content", action: "show_page", id: "10" }, rs.recognize_path("/page/10")) + end + + # For newer revision + def test_route_with_text_default + rs.draw do + get "page/:id" => "content#show_page", :id => 1 + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + + assert_equal "/page/foo", url_for(rs, controller: "content", action: "show_page", id: "foo") + assert_equal({ controller: "content", action: "show_page", id: "foo" }, rs.recognize_path("/page/foo")) + + token = +"\321\202\320\265\320\272\321\201\321\202" # 'text' in Russian + token.force_encoding(Encoding::BINARY) + escaped_token = CGI.escape(token) + + assert_equal "/page/" + escaped_token, url_for(rs, controller: "content", action: "show_page", id: token) + assert_equal({ controller: "content", action: "show_page", id: token }, rs.recognize_path("/page/#{escaped_token}")) + end + + def test_action_expiry + rs.draw { ActiveSupport::Deprecation.silence { get ":controller(/:action(/:id))" } } + get URI("http://test.host/content/show") + assert_equal "/content", controller.url_for(controller: "content", only_path: true) + end + + def test_requirement_should_prevent_optional_id + rs.draw do + get "post/:id" => "post#show", :constraints => { id: /\d+/ }, :as => "post" + end + + assert_equal "/post/10", url_for(rs, controller: "post", action: "show", id: 10) + + assert_raise(ActionController::UrlGenerationError) do + url_for(rs, controller: "post", action: "show") + end + end + + def test_both_requirement_and_optional + rs.draw do + get("test(/:year)" => "post#show", :as => "blog", + :defaults => { year: nil }, + :constraints => { year: /\d{4}/ } + ) + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + + assert_equal "/test", url_for(rs, controller: "post", action: "show") + assert_equal "/test", url_for(rs, controller: "post", action: "show", year: nil) + + assert_equal("http://test.host/test", setup_for_named_route.send(:blog_url)) + end + + def test_set_to_nil_forgets + rs.draw do + get "pages(/:year(/:month(/:day)))" => "content#list_pages", :month => nil, :day => nil + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + + assert_equal "/pages/2005", + url_for(rs, controller: "content", action: "list_pages", year: 2005) + assert_equal "/pages/2005/6", + url_for(rs, controller: "content", action: "list_pages", year: 2005, month: 6) + assert_equal "/pages/2005/6/12", + url_for(rs, controller: "content", action: "list_pages", year: 2005, month: 6, day: 12) + + get URI("http://test.host/pages/2005/6/12") + assert_equal({ controller: "content", action: "list_pages", year: "2005", month: "6", day: "12" }, + controller.request.path_parameters) + + assert_equal "/pages/2005/6/4", + controller.url_for(day: 4, only_path: true) + + assert_equal "/pages/2005/6", + controller.url_for(day: nil, only_path: true) + + assert_equal "/pages/2005", + controller.url_for(day: nil, month: nil, only_path: true) + end + + def test_root_url_generation_with_controller_and_action + rs.draw do + root to: "content#index" + end + + assert_equal "/", url_for(rs, controller: "content", action: "index") + assert_equal "/", url_for(rs, controller: "content") + end + + def test_named_root_url_generation_with_controller_and_action + rs.draw do + root to: "content#index", as: "home" + end + + assert_equal "/", url_for(rs, controller: "content", action: "index") + assert_equal "/", url_for(rs, controller: "content") + + assert_equal("http://test.host/", setup_for_named_route.send(:home_url)) + end + + def test_named_route_method + rs.draw do + get "categories" => "content#categories", :as => "categories" + + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))" + end + end + + assert_equal "/categories", url_for(rs, controller: "content", action: "categories") + assert_equal "/content/hi", url_for(rs, controller: "content", action: "hi") + end + + def test_named_routes_array + test_named_route_method + assert_equal [:categories], rs.named_routes.names + end + + def test_nil_defaults + rs.draw do + get "journal" => "content#list_journal", + :date => nil, :user_id => nil + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + + assert_equal "/journal", url_for(rs, + controller: "content", + action: "list_journal", + date: nil, + user_id: nil) + end + + def setup_request_method_routes_for(method) + rs.draw do + match "/match" => "books##{method}", :via => method.to_sym + end + end + + %w(GET PATCH POST PUT DELETE).each do |request_method| + define_method("test_request_method_recognized_with_#{request_method}") do + setup_request_method_routes_for(request_method.downcase) + params = rs.recognize_path("/match", method: request_method) + assert_equal request_method.downcase, params[:action] + end + end + + def test_recognize_array_of_methods + rs.draw do + match "/match" => "books#get_or_post", :via => [:get, :post] + put "/match" => "books#not_get_or_post" + end + + params = rs.recognize_path("/match", method: :post) + assert_equal "get_or_post", params[:action] + + params = rs.recognize_path("/match", method: :put) + assert_equal "not_get_or_post", params[:action] + end + + def test_subpath_recognized + rs.draw do + ActiveSupport::Deprecation.silence do + get "/books/:id/edit" => "subpath_books#edit" + get "/items/:id/:action" => "subpath_books" + get "/posts/new/:action" => "subpath_books" + get "/posts/:id" => "subpath_books#show" + end + end + + hash = rs.recognize_path "/books/17/edit" + assert_not_nil hash + assert_equal %w(subpath_books 17 edit), [hash[:controller], hash[:id], hash[:action]] + + hash = rs.recognize_path "/items/3/complete" + assert_not_nil hash + assert_equal %w(subpath_books 3 complete), [hash[:controller], hash[:id], hash[:action]] + + hash = rs.recognize_path "/posts/new/preview" + assert_not_nil hash + assert_equal %w(subpath_books preview), [hash[:controller], hash[:action]] + + hash = rs.recognize_path "/posts/7" + assert_not_nil hash + assert_equal %w(subpath_books show 7), [hash[:controller], hash[:action], hash[:id]] + end + + def test_subpath_generated + rs.draw do + ActiveSupport::Deprecation.silence do + get "/books/:id/edit" => "subpath_books#edit" + get "/items/:id/:action" => "subpath_books" + get "/posts/new/:action" => "subpath_books" + end + end + + assert_equal "/books/7/edit", url_for(rs, controller: "subpath_books", id: 7, action: "edit") + assert_equal "/items/15/complete", url_for(rs, controller: "subpath_books", id: 15, action: "complete") + assert_equal "/posts/new/preview", url_for(rs, controller: "subpath_books", action: "preview") + end + + def test_failed_constraints_raises_exception_with_violated_constraints + rs.draw do + get "foos/:id" => "foos#show", :as => "foo_with_requirement", :constraints => { id: /\d+/ } + end + + assert_raise(ActionController::UrlGenerationError) do + setup_for_named_route.send(:foo_with_requirement_url, "I am Against the constraints") + end + end + + def test_routes_changed_correctly_after_clear + rs = ::ActionDispatch::Routing::RouteSet.new + rs.draw do + get "ca" => "ca#aa" + get "cb" => "cb#ab" + get "cc" => "cc#ac" + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + get ":controller/:action/:id.:format" + end + end + + hash = rs.recognize_path "/cc" + + assert_not_nil hash + assert_equal %w(cc ac), [hash[:controller], hash[:action]] + + rs.draw do + get "cb" => "cb#ab" + get "cc" => "cc#ac" + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + get ":controller/:action/:id.:format" + end + end + + hash = rs.recognize_path "/cc" + + assert_not_nil hash + assert_equal %w(cc ac), [hash[:controller], hash[:action]] + end +end + +class RouteSetTest < ActiveSupport::TestCase + include RoutingTestHelpers + include ActionDispatch::RoutingVerbs + + attr_reader :set + alias :routes :set + attr_accessor :controller + + def setup + super + @set = make_set + end + + def request + @request ||= ActionController::TestRequest.new + end + + def default_route_set + @default_route_set ||= begin + set = ActionDispatch::Routing::RouteSet.new + set.draw do + ActiveSupport::Deprecation.silence do + get "/:controller(/:action(/:id))" + end + end + set + end + end + + def test_generate_extras + set.draw { ActiveSupport::Deprecation.silence { get ":controller/(:action(/:id))" } } + path, extras = set.generate_extras(controller: "foo", action: "bar", id: 15, this: "hello", that: "world") + assert_equal "/foo/bar/15", path + assert_equal %w(that this), extras.map(&:to_s).sort + end + + def test_extra_keys + set.draw { ActiveSupport::Deprecation.silence { get ":controller/:action/:id" } } + extras = set.extra_keys(controller: "foo", action: "bar", id: 15, this: "hello", that: "world") + assert_equal %w(that this), extras.map(&:to_s).sort + end + + def test_generate_extras_not_first + set.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id.:format" + get ":controller/:action/:id" + end + end + path, extras = set.generate_extras(controller: "foo", action: "bar", id: 15, this: "hello", that: "world") + assert_equal "/foo/bar/15", path + assert_equal %w(that this), extras.map(&:to_s).sort + end + + def test_generate_not_first + set.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id.:format" + get ":controller/:action/:id" + end + end + assert_equal "/foo/bar/15?this=hello", + url_for(set, controller: "foo", action: "bar", id: 15, this: "hello") + end + + def test_extra_keys_not_first + set.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id.:format" + get ":controller/:action/:id" + end + end + extras = set.extra_keys(controller: "foo", action: "bar", id: 15, this: "hello", that: "world") + assert_equal %w(that this), extras.map(&:to_s).sort + end + + def test_draw + assert_equal 0, set.routes.size + set.draw do + get "/hello/world" => "a#b" + end + assert_equal 1, set.routes.size + end + + def test_draw_symbol_controller_name + assert_equal 0, set.routes.size + set.draw do + get "/users/index" => "users#index" + end + set.recognize_path("/users/index", method: :get) + assert_equal 1, set.routes.size + end + + def test_named_draw + assert_equal 0, set.routes.size + set.draw do + get "/hello/world" => "a#b", :as => "hello" + end + assert_equal 1, set.routes.size + assert_equal set.routes.first, set.named_routes[:hello] + end + + def test_duplicate_named_route_raises_rather_than_pick_precedence + assert_raise ArgumentError do + set.draw do + get "/hello/world" => "a#b", :as => "hello" + get "/hello" => "a#b", :as => "hello" + end + end + end + + def setup_named_route_test + set.draw do + get "/people(/:id)" => "people#show", :as => "show" + get "/people" => "people#index", :as => "index" + get "/people/go/:foo/:bar/joe(/:id)" => "people#multi", :as => "multi" + get "/admin/users" => "admin/users#index", :as => "users" + end + + get URI("http://test.host/people") + controller + end + + def test_named_route_url_method + controller = setup_named_route_test + + assert_equal "http://test.host/people/5", controller.send(:show_url, id: 5) + assert_equal "/people/5", controller.send(:show_path, id: 5) + + assert_equal "http://test.host/people", controller.send(:index_url) + assert_equal "/people", controller.send(:index_path) + + assert_equal "http://test.host/admin/users", controller.send(:users_url) + assert_equal "/admin/users", controller.send(:users_path) + end + + def test_named_route_url_method_with_anchor + controller = setup_named_route_test + + assert_equal "http://test.host/people/5#location", controller.send(:show_url, id: 5, anchor: "location") + assert_equal "/people/5#location", controller.send(:show_path, id: 5, anchor: "location") + + assert_equal "http://test.host/people#location", controller.send(:index_url, anchor: "location") + assert_equal "/people#location", controller.send(:index_path, anchor: "location") + + assert_equal "http://test.host/admin/users#location", controller.send(:users_url, anchor: "location") + assert_equal "/admin/users#location", controller.send(:users_path, anchor: "location") + + assert_equal "http://test.host/people/go/7/hello/joe/5#location", + controller.send(:multi_url, 7, "hello", 5, anchor: "location") + + assert_equal "http://test.host/people/go/7/hello/joe/5?baz=bar#location", + controller.send(:multi_url, 7, "hello", 5, baz: "bar", anchor: "location") + + assert_equal "http://test.host/people?baz=bar#location", + controller.send(:index_url, baz: "bar", anchor: "location") + + assert_equal "http://test.host/people", controller.send(:index_url, anchor: nil) + assert_equal "http://test.host/people", controller.send(:index_url, anchor: false) + end + + def test_named_route_url_method_with_port + controller = setup_named_route_test + assert_equal "http://test.host:8080/people/5", controller.send(:show_url, 5, port: 8080) + end + + def test_named_route_url_method_with_host + controller = setup_named_route_test + assert_equal "http://some.example.com/people/5", controller.send(:show_url, 5, host: "some.example.com") + end + + def test_named_route_url_method_with_protocol + controller = setup_named_route_test + assert_equal "https://test.host/people/5", controller.send(:show_url, 5, protocol: "https") + end + + def test_named_route_url_method_with_ordered_parameters + controller = setup_named_route_test + assert_equal "http://test.host/people/go/7/hello/joe/5", + controller.send(:multi_url, 7, "hello", 5) + end + + def test_named_route_url_method_with_ordered_parameters_and_hash + controller = setup_named_route_test + assert_equal "http://test.host/people/go/7/hello/joe/5?baz=bar", + controller.send(:multi_url, 7, "hello", 5, baz: "bar") + end + + def test_named_route_url_method_with_ordered_parameters_and_empty_hash + controller = setup_named_route_test + assert_equal "http://test.host/people/go/7/hello/joe/5", + controller.send(:multi_url, 7, "hello", 5, {}) + end + + def test_named_route_url_method_with_no_positional_arguments + controller = setup_named_route_test + assert_equal "http://test.host/people?baz=bar", + controller.send(:index_url, baz: "bar") + end + + def test_draw_default_route + set.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + + assert_equal 1, set.routes.size + + assert_equal "/users/show/10", url_for(set, controller: "users", action: "show", id: 10) + assert_equal "/users/index/10", url_for(set, controller: "users", id: 10) + + assert_equal({ controller: "users", action: "index", id: "10" }, set.recognize_path("/users/index/10")) + assert_equal({ controller: "users", action: "index", id: "10" }, set.recognize_path("/users/index/10/")) + end + + def test_route_with_parameter_shell + set.draw do + get "page/:id" => "pages#show", :id => /\d+/ + + ActiveSupport::Deprecation.silence do + get "/:controller(/:action(/:id))" + end + end + + assert_equal({ controller: "pages", action: "index" }, request_path_params("/pages")) + assert_equal({ controller: "pages", action: "index" }, request_path_params("/pages/index")) + assert_equal({ controller: "pages", action: "list" }, request_path_params("/pages/list")) + + assert_equal({ controller: "pages", action: "show", id: "10" }, request_path_params("/pages/show/10")) + assert_equal({ controller: "pages", action: "show", id: "10" }, request_path_params("/page/10")) + end + + def test_route_constraints_on_request_object_with_anchors_are_valid + assert_nothing_raised do + set.draw do + get "page/:id" => "pages#show", :constraints => { host: /^foo$/ } + end + end + end + + def test_route_constraints_with_anchor_chars_are_invalid + assert_raise ArgumentError do + set.draw do + get "page/:id" => "pages#show", :id => /^\d+/ + end + end + assert_raise ArgumentError do + set.draw do + get "page/:id" => "pages#show", :id => /\A\d+/ + end + end + assert_raise ArgumentError do + set.draw do + get "page/:id" => "pages#show", :id => /\d+$/ + end + end + assert_raise ArgumentError do + set.draw do + get "page/:id" => "pages#show", :id => /\d+\Z/ + end + end + assert_raise ArgumentError do + set.draw do + get "page/:id" => "pages#show", :id => /\d+\z/ + end + end + end + + def test_route_constraints_with_options_method_condition_is_valid + assert_nothing_raised do + set.draw do + match "valid/route" => "pages#show", :via => :options + end + end + end + + def test_route_error_with_missing_controller + set.draw do + get "/people" => "missing#index" + end + + assert_raises(ActionController::RoutingError) { request_path_params "/people" } + end + + def test_recognize_with_encoded_id_and_regex + set.draw do + get "page/:id" => "pages#show", :id => /[a-zA-Z0-9\+]+/ + end + + assert_equal({ controller: "pages", action: "show", id: "10" }, request_path_params("/page/10")) + assert_equal({ controller: "pages", action: "show", id: "hello+world" }, request_path_params("/page/hello+world")) + end + + def test_recognize_with_http_methods + set.draw do + get "/people" => "people#index", :as => "people" + post "/people" => "people#create" + get "/people/:id" => "people#show", :as => "person" + put "/people/:id" => "people#update" + patch "/people/:id" => "people#update" + delete "/people/:id" => "people#destroy" + end + + params = request_path_params("/people", method: :get) + assert_equal("index", params[:action]) + + params = request_path_params("/people", method: :post) + assert_equal("create", params[:action]) + + params = request_path_params("/people/5", method: :put) + assert_equal("update", params[:action]) + + params = request_path_params("/people/5", method: :patch) + assert_equal("update", params[:action]) + + assert_raise(ActionController::UnknownHttpMethod) { + request_path_params("/people", method: :bacon) + } + + params = request_path_params("/people/5", method: :get) + assert_equal("show", params[:action]) + assert_equal("5", params[:id]) + + params = request_path_params("/people/5", method: :put) + assert_equal("update", params[:action]) + assert_equal("5", params[:id]) + + params = request_path_params("/people/5", method: :patch) + assert_equal("update", params[:action]) + assert_equal("5", params[:id]) + + params = request_path_params("/people/5", method: :delete) + assert_equal("destroy", params[:action]) + assert_equal("5", params[:id]) + + assert_raise(ActionController::RoutingError) { + request_path_params("/people/5", method: :post) + } + end + + def test_recognize_with_alias_in_conditions + set.draw do + match "/people" => "people#index", :as => "people", :via => :get + root to: "people#index" + end + + params = request_path_params("/people", method: :get) + assert_equal("people", params[:controller]) + assert_equal("index", params[:action]) + + params = request_path_params("/", method: :get) + assert_equal("people", params[:controller]) + assert_equal("index", params[:action]) + end + + def test_typo_recognition + set.draw do + get "articles/:year/:month/:day/:title" => "articles#permalink", + :year => /\d{4}/, :day => /\d{1,2}/, :month => /\d{1,2}/ + end + + params = request_path_params("/articles/2005/11/05/a-very-interesting-article", method: :get) + assert_equal("permalink", params[:action]) + assert_equal("2005", params[:year]) + assert_equal("11", params[:month]) + assert_equal("05", params[:day]) + assert_equal("a-very-interesting-article", params[:title]) + end + + def test_routing_traversal_does_not_load_extra_classes + assert_not Object.const_defined?("Profiler__"), "Profiler should not be loaded" + set.draw do + get "/profile" => "profile#index" + end + + request_path_params("/profile") rescue nil + + assert_not Object.const_defined?("Profiler__"), "Profiler should not be loaded" + end + + def test_recognize_with_conditions_and_format + set.draw do + get "people/:id" => "people#show", :as => "person" + put "people/:id" => "people#update" + patch "people/:id" => "people#update" + get "people/:id(.:format)" => "people#show" + end + + params = request_path_params("/people/5", method: :get) + assert_equal("show", params[:action]) + assert_equal("5", params[:id]) + + params = request_path_params("/people/5", method: :put) + assert_equal("update", params[:action]) + + params = request_path_params("/people/5", method: :patch) + assert_equal("update", params[:action]) + + params = request_path_params("/people/5.png", method: :get) + assert_equal("show", params[:action]) + assert_equal("5", params[:id]) + assert_equal("png", params[:format]) + end + + def test_generate_with_default_action + set.draw do + get "/people", controller: "people", action: "index" + get "/people/list", controller: "people", action: "list" + end + + url = url_for(set, controller: "people", action: "list") + assert_equal "/people/list", url + end + + def test_root_map + set.draw { root to: "people#index" } + + params = request_path_params("", method: :get) + assert_equal("people", params[:controller]) + assert_equal("index", params[:action]) + end + + def test_namespace + set.draw do + namespace "api" do + get "inventory" => "products#inventory" + end + end + + params = request_path_params("/api/inventory", method: :get) + assert_equal("api/products", params[:controller]) + assert_equal("inventory", params[:action]) + end + + def test_namespaced_root_map + set.draw do + namespace "api" do + root to: "products#index" + end + end + + params = request_path_params("/api", method: :get) + assert_equal("api/products", params[:controller]) + assert_equal("index", params[:action]) + end + + def test_namespace_with_path_prefix + set.draw do + scope module: "api", path: "prefix" do + get "inventory" => "products#inventory" + end + end + + params = request_path_params("/prefix/inventory", method: :get) + assert_equal("api/products", params[:controller]) + assert_equal("inventory", params[:action]) + end + + def test_namespace_with_blank_path_prefix + set.draw do + scope module: "api", path: "" do + get "inventory" => "products#inventory" + end + end + + params = request_path_params("/inventory", method: :get) + assert_equal("api/products", params[:controller]) + assert_equal("inventory", params[:action]) + end + + def test_id_is_sticky_when_it_ought_to_be + @set = make_set false + + set.draw do + ActiveSupport::Deprecation.silence do + get ":controller/:id/:action" + end + end + + get URI("http://test.host/people/7/show") + + assert_equal "/people/7/destroy", controller.url_for(action: "destroy", only_path: true) + end + + def test_use_static_path_when_possible + @set = make_set false + + set.draw do + get "about" => "welcome#about" + + ActiveSupport::Deprecation.silence do + get ":controller/:id/:action" + end + end + + get URI("http://test.host/welcom/get/7") + + assert_equal "/about", controller.url_for(controller: "welcome", + action: "about", + only_path: true) + end + + def test_generate + set.draw { ActiveSupport::Deprecation.silence { get ":controller/:action/:id" } } + + args = { controller: "foo", action: "bar", id: "7", x: "y" } + assert_equal "/foo/bar/7?x=y", url_for(set, args) + assert_equal ["/foo/bar/7", [:x]], set.generate_extras(args) + assert_equal [:x], set.extra_keys(args) + end + + def test_generate_with_path_prefix + set.draw do + scope "my" do + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))" + end + end + end + + args = { controller: "foo", action: "bar", id: "7", x: "y" } + assert_equal "/my/foo/bar/7?x=y", url_for(set, args) + end + + def test_generate_with_blank_path_prefix + set.draw do + scope "" do + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))" + end + end + end + + args = { controller: "foo", action: "bar", id: "7", x: "y" } + assert_equal "/foo/bar/7?x=y", url_for(set, args) + end + + def test_named_routes_are_never_relative_to_modules + @set = make_set false + + set.draw do + ActiveSupport::Deprecation.silence do + get "/connection/manage(/:action)" => "connection/manage#index" + get "/connection/connection" => "connection/connection#index" + get "/connection" => "connection#index", :as => "family_connection" + end + end + + assert_equal({ controller: "connection/manage", + action: "index", }, request_path_params("/connection/manage")) + + url = controller.url_for(controller: "connection", only_path: true) + assert_equal "/connection/connection", url + + url = controller.url_for(use_route: "family_connection", + controller: "connection", only_path: true) + assert_equal "/connection", url + end + + def test_action_left_off_when_id_is_recalled + @set = make_set false + + set.draw do + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))" + end + end + + get URI("http://test.host/books/show/10") + + assert_equal "/books", controller.url_for(controller: "books", + only_path: true, + action: "index") + end + + def test_query_params_will_be_shown_when_recalled + @set = make_set false + + set.draw do + get "show_weblog/:parameter" => "weblog#show" + + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))" + end + end + + get URI("http://test.host/weblog/show/1") + + assert_equal "/weblog/edit?parameter=1", controller.url_for( + action: "edit", parameter: 1, only_path: true) + end + + def test_format_is_not_inherit + set.draw do + get "/posts(.:format)" => "posts#index" + end + + get URI("http://test.host/posts.xml") + assert_equal({ controller: "posts", action: "index", format: "xml" }, + controller.request.path_parameters) + + assert_equal "/posts", controller.url_for( + controller: "posts", only_path: true) + + assert_equal "/posts.xml", controller.url_for( + controller: "posts", format: "xml", only_path: true) + end + + def test_expiry_determination_should_consider_values_with_to_param + @set = make_set false + + set.draw { ActiveSupport::Deprecation.silence { get "projects/:project_id/:controller/:action" } } + + get URI("http://test.host/projects/1/weblog/show") + + assert_equal( + { controller: "weblog", action: "show", project_id: "1" }, + controller.request.path_parameters) + + assert_equal "/projects/1/weblog/show", + controller.url_for(action: "show", project_id: 1, only_path: true) + end + + def test_named_route_in_nested_resource + set.draw do + resources :projects do + member do + get "milestones" => "milestones#index", :as => "milestones" + end + end + end + + params = set.recognize_path("/projects/1/milestones", method: :get) + assert_equal("milestones", params[:controller]) + assert_equal("index", params[:action]) + end + + def test_setting_root_in_namespace_using_symbol + assert_nothing_raised do + set.draw do + namespace :admin do + root to: "home#index" + end + end + end + end + + def test_setting_root_in_namespace_using_string + assert_nothing_raised do + set.draw do + namespace "admin" do + root to: "home#index" + end + end + end + end + + def test_route_constraints_with_unsupported_regexp_options_must_error + assert_raise ArgumentError do + set.draw do + get "page/:name" => "pages#show", + :constraints => { name: /(david|jamis)/m } + end + end + end + + def test_route_constraints_with_supported_options_must_not_error + assert_nothing_raised do + set.draw do + get "page/:name" => "pages#show", + :constraints => { name: /(david|jamis)/i } + end + end + assert_nothing_raised do + set.draw do + get "page/:name" => "pages#show", + :constraints => { name: / # Desperately overcommented regexp + ( #Either + david #The Creator + | #Or + jamis #The Deployer + )/x } + end + end + end + + def test_route_with_subdomain_and_constraints_must_receive_params + name_param = nil + set.draw do + get "page/:name" => "pages#show", :constraints => lambda { |request| + name_param = request.params[:name] + return true + } + end + assert_equal({ controller: "pages", action: "show", name: "mypage" }, + set.recognize_path("http://subdomain.example.org/page/mypage")) + assert_equal(name_param, "mypage") + end + + def test_route_requirement_recognize_with_ignore_case + set.draw do + get "page/:name" => "pages#show", + :constraints => { name: /(david|jamis)/i } + end + assert_equal({ controller: "pages", action: "show", name: "jamis" }, set.recognize_path("/page/jamis")) + assert_raise ActionController::RoutingError do + set.recognize_path("/page/davidjamis") + end + assert_equal({ controller: "pages", action: "show", name: "DAVID" }, set.recognize_path("/page/DAVID")) + end + + def test_route_requirement_generate_with_ignore_case + set.draw do + get "page/:name" => "pages#show", + :constraints => { name: /(david|jamis)/i } + end + + url = url_for(set, controller: "pages", action: "show", name: "david") + assert_equal "/page/david", url + assert_raise(ActionController::UrlGenerationError) do + url_for(set, controller: "pages", action: "show", name: "davidjamis") + end + url = url_for(set, controller: "pages", action: "show", name: "JAMIS") + assert_equal "/page/JAMIS", url + end + + def test_route_requirement_recognize_with_extended_syntax + set.draw do + get "page/:name" => "pages#show", + :constraints => { name: / # Desperately overcommented regexp + ( #Either + david #The Creator + | #Or + jamis #The Deployer + )/x } + end + assert_equal({ controller: "pages", action: "show", name: "jamis" }, set.recognize_path("/page/jamis")) + assert_equal({ controller: "pages", action: "show", name: "david" }, set.recognize_path("/page/david")) + assert_raise ActionController::RoutingError do + set.recognize_path("/page/david #The Creator") + end + assert_raise ActionController::RoutingError do + set.recognize_path("/page/David") + end + end + + def test_route_requirement_with_xi_modifiers + set.draw do + get "page/:name" => "pages#show", + :constraints => { name: / # Desperately overcommented regexp + ( #Either + david #The Creator + | #Or + jamis #The Deployer + )/xi } + end + + assert_equal({ controller: "pages", action: "show", name: "JAMIS" }, + set.recognize_path("/page/JAMIS")) + + assert_equal "/page/JAMIS", + url_for(set, controller: "pages", action: "show", name: "JAMIS") + end + + def test_routes_with_symbols + set.draw do + get "unnamed", controller: :pages, action: :show, name: :as_symbol + get "named", controller: :pages, action: :show, name: :as_symbol, as: :named + end + assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/unnamed")) + assert_equal({ controller: "pages", action: "show", name: :as_symbol }, set.recognize_path("/named")) + end + + def test_regexp_chunk_should_add_question_mark_for_optionals + set.draw do + get "/" => "foo#index" + get "/hello" => "bar#index" + end + + assert_equal "/", url_for(set, controller: "foo") + assert_equal "/hello", url_for(set, controller: "bar") + + assert_equal({ controller: "foo", action: "index" }, set.recognize_path("/")) + assert_equal({ controller: "bar", action: "index" }, set.recognize_path("/hello")) + end + + def test_assign_route_options_with_anchor_chars + set.draw do + ActiveSupport::Deprecation.silence do + get "/cars/:action/:person/:car/", controller: "cars" + end + end + + assert_equal "/cars/buy/1/2", url_for(set, controller: "cars", action: "buy", person: "1", car: "2") + + assert_equal({ controller: "cars", action: "buy", person: "1", car: "2" }, set.recognize_path("/cars/buy/1/2")) + end + + def test_segmentation_of_dot_path + set.draw do + ActiveSupport::Deprecation.silence do + get "/books/:action.rss", controller: "books" + end + end + + assert_equal "/books/list.rss", url_for(set, controller: "books", action: "list") + + assert_equal({ controller: "books", action: "list" }, set.recognize_path("/books/list.rss")) + end + + def test_segmentation_of_dynamic_dot_path + set.draw do + ActiveSupport::Deprecation.silence do + get "/books(/:action(.:format))", controller: "books" + end + end + + assert_equal "/books/list.rss", url_for(set, controller: "books", action: "list", format: "rss") + assert_equal "/books/list.xml", url_for(set, controller: "books", action: "list", format: "xml") + assert_equal "/books/list", url_for(set, controller: "books", action: "list") + assert_equal "/books", url_for(set, controller: "books", action: "index") + + assert_equal({ controller: "books", action: "list", format: "rss" }, set.recognize_path("/books/list.rss")) + assert_equal({ controller: "books", action: "list", format: "xml" }, set.recognize_path("/books/list.xml")) + assert_equal({ controller: "books", action: "list" }, set.recognize_path("/books/list")) + assert_equal({ controller: "books", action: "index" }, set.recognize_path("/books")) + end + + def test_slashes_are_implied + set.draw { ActiveSupport::Deprecation.silence { get("/:controller(/:action(/:id))") } } + + assert_equal "/content", url_for(set, controller: "content", action: "index") + assert_equal "/content/list", url_for(set, controller: "content", action: "list") + assert_equal "/content/show/1", url_for(set, controller: "content", action: "show", id: "1") + + assert_equal({ controller: "content", action: "index" }, set.recognize_path("/content")) + assert_equal({ controller: "content", action: "index" }, set.recognize_path("/content/index")) + assert_equal({ controller: "content", action: "list" }, set.recognize_path("/content/list")) + assert_equal({ controller: "content", action: "show", id: "1" }, set.recognize_path("/content/show/1")) + end + + def test_default_route_recognition + expected = { controller: "pages", action: "show", id: "10" } + assert_equal expected, default_route_set.recognize_path("/pages/show/10") + assert_equal expected, default_route_set.recognize_path("/pages/show/10/") + + expected[:id] = "jamis" + assert_equal expected, default_route_set.recognize_path("/pages/show/jamis/") + + expected.delete :id + assert_equal expected, default_route_set.recognize_path("/pages/show") + assert_equal expected, default_route_set.recognize_path("/pages/show/") + + expected[:action] = "index" + assert_equal expected, default_route_set.recognize_path("/pages/") + assert_equal expected, default_route_set.recognize_path("/pages") + + assert_raise(ActionController::RoutingError) { default_route_set.recognize_path("/") } + assert_raise(ActionController::RoutingError) { default_route_set.recognize_path("/pages/how/goood/it/is/to/be/free") } + end + + def test_default_route_should_omit_default_action + assert_equal "/accounts", url_for(default_route_set, controller: "accounts", action: "index") + end + + def test_default_route_should_include_default_action_when_id_present + assert_equal "/accounts/index/20", url_for(default_route_set, controller: "accounts", action: "index", id: "20") + end + + def test_default_route_should_work_with_action_but_no_id + assert_equal "/accounts/list_all", url_for(default_route_set, controller: "accounts", action: "list_all") + end + + def test_default_route_should_uri_escape_pluses + expected = { controller: "pages", action: "show", id: "hello world" } + assert_equal expected, default_route_set.recognize_path("/pages/show/hello%20world") + assert_equal "/pages/show/hello%20world", url_for(default_route_set, expected) + + expected[:id] = "hello+world" + assert_equal expected, default_route_set.recognize_path("/pages/show/hello+world") + assert_equal expected, default_route_set.recognize_path("/pages/show/hello%2Bworld") + assert_equal "/pages/show/hello+world", url_for(default_route_set, expected) + end + + def test_build_empty_query_string + assert_uri_equal "/foo", url_for(default_route_set, controller: "foo") + end + + def test_build_query_string_with_nil_value + assert_uri_equal "/foo", url_for(default_route_set, controller: "foo", x: nil) + end + + def test_simple_build_query_string + assert_uri_equal "/foo?x=1&y=2", url_for(default_route_set, controller: "foo", x: "1", y: "2") + end + + def test_convert_ints_build_query_string + assert_uri_equal "/foo?x=1&y=2", url_for(default_route_set, controller: "foo", x: 1, y: 2) + end + + def test_escape_spaces_build_query_string + assert_uri_equal "/foo?x=hello+world&y=goodbye+world", url_for(default_route_set, controller: "foo", x: "hello world", y: "goodbye world") + end + + def test_expand_array_build_query_string + assert_uri_equal "/foo?x%5B%5D=1&x%5B%5D=2", url_for(default_route_set, controller: "foo", x: [1, 2]) + end + + def test_escape_spaces_build_query_string_selected_keys + assert_uri_equal "/foo?x=hello+world", url_for(default_route_set, controller: "foo", x: "hello world") + end + + def test_generate_with_default_params + set.draw do + get "dummy/page/:page" => "dummy#show" + get "dummy/dots/page.:page" => "dummy#dots" + get "ibocorp(/:page)" => "ibocorp#show", + :constraints => { page: /\d+/ }, + :defaults => { page: 1 } + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + + assert_equal "/ibocorp", url_for(set, controller: "ibocorp", action: "show", page: 1) + end + + include ActionDispatch::RoutingVerbs + + alias :routes :set + + def test_generate_with_optional_params_recalls_last_request + @set = make_set false + + set.draw do + get "blog/", controller: "blog", action: "index" + + get "blog(/:year(/:month(/:day)))", + controller: "blog", + action: "show_date", + constraints: { year: /(19|20)\d\d/, month: /[01]?\d/, day: /[0-3]?\d/ }, + day: nil, month: nil + + get "blog/show/:id", controller: "blog", action: "show", id: /\d+/ + + ActiveSupport::Deprecation.silence do + get "blog/:controller/:action(/:id)" + end + + get "*anything", controller: "blog", action: "unknown_request" + end + + recognize_path = ->(path) { + get(URI("http://example.org" + path)) + controller.request.path_parameters + } + + assert_equal({ controller: "blog", action: "index" }, recognize_path.("/blog")) + assert_equal({ controller: "blog", action: "show", id: "123" }, recognize_path.("/blog/show/123")) + assert_equal({ controller: "blog", action: "show_date", year: "2004", day: nil, month: nil }, recognize_path.("/blog/2004")) + assert_equal({ controller: "blog", action: "show_date", year: "2004", month: "12", day: nil }, recognize_path.("/blog/2004/12")) + assert_equal({ controller: "blog", action: "show_date", year: "2004", month: "12", day: "25" }, recognize_path.("/blog/2004/12/25")) + assert_equal({ controller: "articles", action: "edit", id: "123" }, recognize_path.("/blog/articles/edit/123")) + assert_equal({ controller: "articles", action: "show_stats" }, recognize_path.("/blog/articles/show_stats")) + assert_equal({ controller: "blog", action: "unknown_request", anything: "blog/wibble" }, recognize_path.("/blog/wibble")) + assert_equal({ controller: "blog", action: "unknown_request", anything: "junk" }, recognize_path.("/junk")) + + get URI("http://example.org/blog/2006/07/28") + + assert_equal({ controller: "blog", action: "show_date", year: "2006", month: "07", day: "28" }, controller.request.path_parameters) + assert_equal("/blog/2006/07/25", controller.url_for(day: 25, only_path: true)) + assert_equal("/blog/2005", controller.url_for(year: 2005, only_path: true)) + assert_equal("/blog/show/123", controller.url_for(action: "show", id: 123, only_path: true)) + assert_equal("/blog/2006", controller.url_for(year: 2006, only_path: true)) + assert_equal("/blog/2006", controller.url_for(year: 2006, month: nil, only_path: true)) + end + + private + def assert_uri_equal(expected, actual) + assert_equal(sort_query_string_params(expected), sort_query_string_params(actual)) + end + + def sort_query_string_params(uri) + path, qs = uri.split("?") + qs = qs.split("&").sort.join("&") if qs + qs ? "#{path}?#{qs}" : path + end +end + +class RackMountIntegrationTests < ActiveSupport::TestCase + include RoutingTestHelpers + + Model = Struct.new(:to_param) + + Mapping = lambda { + namespace :admin do + resources :users, :posts + end + + namespace "api" do + root to: "users#index" + end + + get "/blog(/:year(/:month(/:day)))" => "posts#show_date", + :constraints => { + year: /(19|20)\d\d/, + month: /[01]?\d/, + day: /[0-3]?\d/ + }, + :day => nil, + :month => nil + + get "archive/:year", controller: "archive", action: "index", + defaults: { year: nil }, + constraints: { year: /\d{4}/ }, + as: "blog" + + resources :people + get "legacy/people" => "people#index", :legacy => "true" + + get "symbols", controller: :symbols, action: :show, name: :as_symbol + get "id_default(/:id)" => "foo#id_default", :id => 1 + match "get_or_post" => "foo#get_or_post", :via => [:get, :post] + get "optional/:optional" => "posts#index" + get "projects/:project_id" => "project#index", :as => "project" + get "clients" => "projects#index" + + get "ignorecase/geocode/:postalcode" => "geocode#show", :postalcode => /hx\d\d-\d[a-z]{2}/i + get "extended/geocode/:postalcode" => "geocode#show", :constraints => { + postalcode: /# Postcode format + \d{5} #Prefix + (-\d{4})? #Suffix + /x + }, :as => "geocode" + + get "news(.:format)" => "news#index" + + ActiveSupport::Deprecation.silence do + get "comment/:id(/:action)" => "comments#show" + get "ws/:controller(/:action(/:id))", ws: true + get "account(/:action)" => "account#subscription" + get "pages/:page_id/:controller(/:action(/:id))" + get ":controller/ping", action: "ping" + end + + get "こんにちは/世界", controller: "news", action: "index" + + ActiveSupport::Deprecation.silence do + match ":controller(/:action(/:id))(.:format)", via: :all + end + + root to: "news#index" + } + + attr_reader :routes + attr_reader :controller + + def setup + @routes = ActionDispatch::Routing::RouteSet.new + @routes.draw(&Mapping) + end + + def test_recognize_path + assert_equal({ controller: "admin/users", action: "index" }, @routes.recognize_path("/admin/users", method: :get)) + assert_equal({ controller: "admin/users", action: "create" }, @routes.recognize_path("/admin/users", method: :post)) + assert_equal({ controller: "admin/users", action: "new" }, @routes.recognize_path("/admin/users/new", method: :get)) + assert_equal({ controller: "admin/users", action: "show", id: "1" }, @routes.recognize_path("/admin/users/1", method: :get)) + assert_equal({ controller: "admin/users", action: "update", id: "1" }, @routes.recognize_path("/admin/users/1", method: :put)) + assert_equal({ controller: "admin/users", action: "destroy", id: "1" }, @routes.recognize_path("/admin/users/1", method: :delete)) + assert_equal({ controller: "admin/users", action: "edit", id: "1" }, @routes.recognize_path("/admin/users/1/edit", method: :get)) + + assert_equal({ controller: "admin/posts", action: "index" }, @routes.recognize_path("/admin/posts", method: :get)) + assert_equal({ controller: "admin/posts", action: "new" }, @routes.recognize_path("/admin/posts/new", method: :get)) + + assert_equal({ controller: "api/users", action: "index" }, @routes.recognize_path("/api", method: :get)) + assert_equal({ controller: "api/users", action: "index" }, @routes.recognize_path("/api/", method: :get)) + + assert_equal({ controller: "posts", action: "show_date", year: "2009", month: nil, day: nil }, @routes.recognize_path("/blog/2009", method: :get)) + assert_equal({ controller: "posts", action: "show_date", year: "2009", month: "01", day: nil }, @routes.recognize_path("/blog/2009/01", method: :get)) + assert_equal({ controller: "posts", action: "show_date", year: "2009", month: "01", day: "01" }, @routes.recognize_path("/blog/2009/01/01", method: :get)) + + assert_equal({ controller: "archive", action: "index", year: "2010" }, @routes.recognize_path("/archive/2010")) + assert_equal({ controller: "archive", action: "index" }, @routes.recognize_path("/archive")) + + assert_equal({ controller: "people", action: "index" }, @routes.recognize_path("/people", method: :get)) + assert_equal({ controller: "people", action: "index", format: "xml" }, @routes.recognize_path("/people.xml", method: :get)) + assert_equal({ controller: "people", action: "create" }, @routes.recognize_path("/people", method: :post)) + assert_equal({ controller: "people", action: "new" }, @routes.recognize_path("/people/new", method: :get)) + assert_equal({ controller: "people", action: "show", id: "1" }, @routes.recognize_path("/people/1", method: :get)) + assert_equal({ controller: "people", action: "show", id: "1", format: "xml" }, @routes.recognize_path("/people/1.xml", method: :get)) + assert_equal({ controller: "people", action: "update", id: "1" }, @routes.recognize_path("/people/1", method: :put)) + assert_equal({ controller: "people", action: "destroy", id: "1" }, @routes.recognize_path("/people/1", method: :delete)) + assert_equal({ controller: "people", action: "edit", id: "1" }, @routes.recognize_path("/people/1/edit", method: :get)) + assert_equal({ controller: "people", action: "edit", id: "1", format: "xml" }, @routes.recognize_path("/people/1/edit.xml", method: :get)) + + assert_equal({ controller: "symbols", action: "show", name: :as_symbol }, @routes.recognize_path("/symbols")) + assert_equal({ controller: "foo", action: "id_default", id: "1" }, @routes.recognize_path("/id_default/1")) + assert_equal({ controller: "foo", action: "id_default", id: "2" }, @routes.recognize_path("/id_default/2")) + assert_equal({ controller: "foo", action: "id_default", id: 1 }, @routes.recognize_path("/id_default")) + assert_equal({ controller: "foo", action: "get_or_post" }, @routes.recognize_path("/get_or_post", method: :get)) + assert_equal({ controller: "foo", action: "get_or_post" }, @routes.recognize_path("/get_or_post", method: :post)) + assert_raise(ActionController::RoutingError) { @routes.recognize_path("/get_or_post", method: :put) } + assert_raise(ActionController::RoutingError) { @routes.recognize_path("/get_or_post", method: :delete) } + + assert_equal({ controller: "posts", action: "index", optional: "bar" }, @routes.recognize_path("/optional/bar")) + assert_raise(ActionController::RoutingError) { @routes.recognize_path("/optional") } + + assert_equal({ controller: "posts", action: "show", id: "1", ws: true }, @routes.recognize_path("/ws/posts/show/1", method: :get)) + assert_equal({ controller: "posts", action: "list", ws: true }, @routes.recognize_path("/ws/posts/list", method: :get)) + assert_equal({ controller: "posts", action: "index", ws: true }, @routes.recognize_path("/ws/posts", method: :get)) + + assert_equal({ controller: "account", action: "subscription" }, @routes.recognize_path("/account", method: :get)) + assert_equal({ controller: "account", action: "subscription" }, @routes.recognize_path("/account/subscription", method: :get)) + assert_equal({ controller: "account", action: "billing" }, @routes.recognize_path("/account/billing", method: :get)) + + assert_equal({ page_id: "1", controller: "notes", action: "index" }, @routes.recognize_path("/pages/1/notes", method: :get)) + assert_equal({ page_id: "1", controller: "notes", action: "list" }, @routes.recognize_path("/pages/1/notes/list", method: :get)) + assert_equal({ page_id: "1", controller: "notes", action: "show", id: "2" }, @routes.recognize_path("/pages/1/notes/show/2", method: :get)) + + assert_equal({ controller: "posts", action: "ping" }, @routes.recognize_path("/posts/ping", method: :get)) + assert_equal({ controller: "posts", action: "index" }, @routes.recognize_path("/posts", method: :get)) + assert_equal({ controller: "posts", action: "index" }, @routes.recognize_path("/posts/index", method: :get)) + assert_equal({ controller: "posts", action: "show" }, @routes.recognize_path("/posts/show", method: :get)) + assert_equal({ controller: "posts", action: "show", id: "1" }, @routes.recognize_path("/posts/show/1", method: :get)) + assert_equal({ controller: "posts", action: "create" }, @routes.recognize_path("/posts/create", method: :post)) + + assert_equal({ controller: "geocode", action: "show", postalcode: "hx12-1az" }, @routes.recognize_path("/ignorecase/geocode/hx12-1az")) + assert_equal({ controller: "geocode", action: "show", postalcode: "hx12-1AZ" }, @routes.recognize_path("/ignorecase/geocode/hx12-1AZ")) + assert_equal({ controller: "geocode", action: "show", postalcode: "12345-1234" }, @routes.recognize_path("/extended/geocode/12345-1234")) + assert_equal({ controller: "geocode", action: "show", postalcode: "12345" }, @routes.recognize_path("/extended/geocode/12345")) + + assert_equal({ controller: "news", action: "index" }, @routes.recognize_path("/", method: :get)) + assert_equal({ controller: "news", action: "index", format: "rss" }, @routes.recognize_path("/news.rss", method: :get)) + + assert_raise(ActionController::RoutingError) { @routes.recognize_path("/none", method: :get) } + end + + def test_generate_extras + assert_equal ["/people", []], @routes.generate_extras(controller: "people") + assert_equal ["/people", [:foo]], @routes.generate_extras(controller: "people", foo: "bar") + assert_equal ["/people", []], @routes.generate_extras(controller: "people", action: "index") + assert_equal ["/people", [:foo]], @routes.generate_extras(controller: "people", action: "index", foo: "bar") + assert_equal ["/people/new", []], @routes.generate_extras(controller: "people", action: "new") + assert_equal ["/people/new", [:foo]], @routes.generate_extras(controller: "people", action: "new", foo: "bar") + assert_equal ["/people/1", []], @routes.generate_extras(controller: "people", action: "show", id: "1") + assert_equal ["/people/1", [:bar, :foo]], sort_extras!(@routes.generate_extras(controller: "people", action: "show", id: "1", foo: "2", bar: "3")) + assert_equal ["/people", [:person]], @routes.generate_extras(controller: "people", action: "create", person: { first_name: "Josh", last_name: "Peek" }) + assert_equal ["/people", [:people]], @routes.generate_extras(controller: "people", action: "create", people: ["Josh", "Dave"]) + + assert_equal ["/posts/show/1", []], @routes.generate_extras(controller: "posts", action: "show", id: "1") + assert_equal ["/posts/show/1", [:bar, :foo]], sort_extras!(@routes.generate_extras(controller: "posts", action: "show", id: "1", foo: "2", bar: "3")) + assert_equal ["/posts", []], @routes.generate_extras(controller: "posts", action: "index") + assert_equal ["/posts", [:foo]], @routes.generate_extras(controller: "posts", action: "index", foo: "bar") + end + + def test_extras + params = { controller: "people" } + assert_equal [], @routes.extra_keys(params) + assert_equal({ controller: "people", action: "index" }, params) + + params = { controller: "people", foo: "bar" } + assert_equal [:foo], @routes.extra_keys(params) + assert_equal({ controller: "people", action: "index", foo: "bar" }, params) + + params = { controller: "people", action: "create", person: { name: "Josh" } } + assert_equal [:person], @routes.extra_keys(params) + assert_equal({ controller: "people", action: "create", person: { name: "Josh" } }, params) + end + + def test_unicode_path + assert_equal({ controller: "news", action: "index" }, @routes.recognize_path(URI.parser.escape("こんにちは/世界"), method: :get)) + end + + def test_downcased_unicode_path + assert_equal({ controller: "news", action: "index" }, @routes.recognize_path(URI.parser.escape("こんにちは/世界").downcase, method: :get)) + end + + private + def sort_extras!(extras) + if extras.length == 2 + extras[1].sort! { |a, b| a.to_s <=> b.to_s } + end + extras + end +end diff --git a/actionpack/test/controller/runner_test.rb b/actionpack/test/controller/runner_test.rb new file mode 100644 index 0000000000..1709ab5f6d --- /dev/null +++ b/actionpack/test/controller/runner_test.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_dispatch/testing/integration" + +module ActionDispatch + class RunnerTest < ActiveSupport::TestCase + class MyRunner + include Integration::Runner + + def initialize(session) + @integration_session = session + end + + def hi; end + end + + def test_respond_to? + runner = MyRunner.new(Class.new { def x; end }.new) + assert_respond_to runner, :hi + assert_respond_to runner, :x + end + end +end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb new file mode 100644 index 0000000000..c917cdf761 --- /dev/null +++ b/actionpack/test/controller/send_file_test.rb @@ -0,0 +1,259 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module TestFileUtils + def file_name() File.basename(__FILE__) end + def file_path() __FILE__ end + def file_data() @data ||= File.open(file_path, "rb") { |f| f.read } end +end + +class SendFileController < ActionController::Base + include TestFileUtils + include ActionController::Testing + layout "layouts/standard" # to make sure layouts don't interfere + + before_action :file, only: :file_from_before_action + + attr_writer :options + def options + @options ||= {} + end + + def file + send_file(file_path, options) + end + + def file_from_before_action + raise "No file sent from before action." + end + + def test_send_file_headers_bang + options = { + type: Mime[:png], + disposition: "disposition", + filename: "filename" + } + + send_data "foo", options + end + + def test_send_file_headers_with_disposition_as_a_symbol + options = { + type: Mime[:png], + disposition: :disposition, + filename: "filename" + } + + send_data "foo", options + end + + def test_send_file_headers_with_mime_lookup_with_symbol + options = { type: :png } + + send_data "foo", options + end + + def test_send_file_headers_with_bad_symbol + options = { type: :this_type_is_not_registered } + send_data "foo", options + end + + def test_send_file_headers_with_nil_content_type + options = { type: nil } + send_data "foo", options + end + + def test_send_file_headers_guess_type_from_extension + options = { filename: params[:filename] } + send_data "foo", options + end + + def data + send_data(file_data, options) + end +end + +class SendFileWithActionControllerLive < SendFileController + include ActionController::Live +end + +class SendFileTest < ActionController::TestCase + include TestFileUtils + + def setup + @controller = SendFileController.new + end + + def test_file_nostream + @controller.options = { stream: false } + response = nil + assert_nothing_raised { response = process("file") } + assert_not_nil response + body = response.body + assert_kind_of String, body + assert_equal file_data, body + end + + def test_file_stream + response = nil + assert_nothing_raised { response = process("file") } + assert_not_nil response + assert_respond_to response.stream, :each + assert_respond_to response.stream, :to_path + + require "stringio" + output = StringIO.new + output.binmode + output.string.force_encoding(file_data.encoding) + response.body_parts.each { |part| output << part.to_s } + assert_equal file_data, output.string + end + + def test_file_url_based_filename + @controller.options = { url_based_filename: true } + response = nil + assert_nothing_raised { response = process("file") } + assert_not_nil response + assert_equal "attachment", response.headers["Content-Disposition"] + end + + def test_data + response = nil + assert_nothing_raised { response = process("data") } + assert_not_nil response + + assert_kind_of String, response.body + assert_equal file_data, response.body + end + + def test_headers_after_send_shouldnt_include_charset + response = process("data") + assert_equal "application/octet-stream", response.headers["Content-Type"] + + response = process("file") + assert_equal "application/octet-stream", response.headers["Content-Type"] + end + + # Test that send_file_headers! is setting the correct HTTP headers. + def test_send_file_headers_bang + # Do it a few times: the resulting headers should be identical + # no matter how many times you send with the same options. + # Test resolving Ticket #458. + 5.times do + get :test_send_file_headers_bang + + assert_equal "image/png", response.content_type + assert_equal %(disposition; filename="filename"; filename*=UTF-8''filename), response.get_header("Content-Disposition") + assert_equal "binary", response.get_header("Content-Transfer-Encoding") + assert_equal "private", response.get_header("Cache-Control") + end + end + + def test_send_file_headers_with_disposition_as_a_symbol + get :test_send_file_headers_with_disposition_as_a_symbol + + assert_equal %(disposition; filename="filename"; filename*=UTF-8''filename), response.get_header("Content-Disposition") + end + + def test_send_file_headers_with_mime_lookup_with_symbol + get __method__ + assert_equal "image/png", response.content_type + end + + def test_send_file_headers_with_bad_symbol + error = assert_raise(ArgumentError) { get __method__ } + assert_equal "Unknown MIME type this_type_is_not_registered", error.message + end + + def test_send_file_headers_with_nil_content_type + error = assert_raise(ArgumentError) { get __method__ } + assert_equal ":type option required", error.message + end + + def test_send_file_headers_guess_type_from_extension + { + "image.png" => "image/png", + "image.jpeg" => "image/jpeg", + "image.jpg" => "image/jpeg", + "image.tif" => "image/tiff", + "image.gif" => "image/gif", + "movie.mp4" => "video/mp4", + "file.zip" => "application/zip", + "file.unk" => "application/octet-stream", + "zip" => "application/octet-stream" + }.each do |filename, expected_type| + get __method__, params: { filename: filename } + assert_equal expected_type, response.content_type + end + end + + def test_send_file_with_default_content_disposition_header + process("data") + assert_equal "attachment", @controller.headers["Content-Disposition"] + end + + def test_send_file_without_content_disposition_header + @controller.options = { disposition: nil } + process("data") + assert_nil @controller.headers["Content-Disposition"] + end + + def test_send_file_from_before_action + response = nil + assert_nothing_raised { response = process("file_from_before_action") } + assert_not_nil response + + assert_kind_of String, response.body + assert_equal file_data, response.body + end + + %w(file data).each do |method| + define_method "test_send_#{method}_status" do + @controller.options = { stream: false, status: 500 } + assert_not_nil process(method) + assert_equal 500, @response.status + end + + define_method "test_send_#{method}_content_type" do + @controller.options = { stream: false, content_type: "application/x-ruby" } + assert_nothing_raised { assert_not_nil process(method) } + assert_equal "application/x-ruby", @response.content_type + end + + define_method "test_default_send_#{method}_status" do + @controller.options = { stream: false } + assert_nothing_raised { assert_not_nil process(method) } + assert_equal 200, @response.status + end + end + + def test_send_file_with_action_controller_live + @controller = SendFileWithActionControllerLive.new + @controller.options = { content_type: "application/x-ruby" } + + response = process("file") + assert_equal 200, response.status + end + + def test_send_file_charset_with_type_options_key + @controller = SendFileWithActionControllerLive.new + @controller.options = { type: "text/calendar; charset=utf-8" } + response = process("file") + assert_equal "text/calendar; charset=utf-8", response.headers["Content-Type"] + end + + def test_send_file_charset_with_type_options_key_without_charset + @controller = SendFileWithActionControllerLive.new + @controller.options = { type: "image/png" } + response = process("file") + assert_equal "image/png", response.headers["Content-Type"] + end + + def test_send_file_charset_with_content_type_options_key + @controller = SendFileWithActionControllerLive.new + @controller.options = { content_type: "text/calendar" } + response = process("file") + assert_equal "text/calendar", response.headers["Content-Type"] + end +end diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb new file mode 100644 index 0000000000..2094aa1aed --- /dev/null +++ b/actionpack/test/controller/show_exceptions_test.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ShowExceptions + class ShowExceptionsController < ActionController::Base + use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public") + use ActionDispatch::DebugExceptions + + before_action only: :another_boom do + request.env["action_dispatch.show_detailed_exceptions"] = true + end + + def boom + raise "boom!" + end + + def another_boom + raise "boom!" + end + + def show_detailed_exceptions? + request.local? + end + end + + class ShowExceptionsTest < ActionDispatch::IntegrationTest + test "show error page from a remote ip" do + @app = ShowExceptionsController.action(:boom) + self.remote_addr = "208.77.188.166" + get "/" + assert_equal "500 error fixture\n", body + end + + test "show diagnostics from a local ip if show_detailed_exceptions? is set to request.local?" do + @app = ShowExceptionsController.action(:boom) + ["127.0.0.1", "127.0.0.127", "127.12.1.1", "::1", "0:0:0:0:0:0:0:1", "0:0:0:0:0:0:0:1%0"].each do |ip_address| + self.remote_addr = ip_address + get "/" + assert_match(/boom/, body) + end + end + + test "show diagnostics from a remote ip when env is already set" do + @app = ShowExceptionsController.action(:another_boom) + self.remote_addr = "208.77.188.166" + get "/" + assert_match(/boom/, body) + end + end + + class ShowExceptionsOverriddenController < ShowExceptionsController + private + + def show_detailed_exceptions? + params["detailed"] == "1" + end + end + + class ShowExceptionsOverriddenTest < ActionDispatch::IntegrationTest + test "show error page" do + @app = ShowExceptionsOverriddenController.action(:boom) + get "/", params: { "detailed" => "0" } + assert_equal "500 error fixture\n", body + end + + test "show diagnostics message" do + @app = ShowExceptionsOverriddenController.action(:boom) + get "/", params: { "detailed" => "1" } + assert_match(/boom/, body) + end + end + + class ShowExceptionsFormatsTest < ActionDispatch::IntegrationTest + def test_render_json_exception + @app = ShowExceptionsOverriddenController.action(:boom) + get "/", headers: { "HTTP_ACCEPT" => "application/json" } + assert_response :internal_server_error + assert_equal "application/json", response.content_type.to_s + assert_equal({ status: 500, error: "Internal Server Error" }.to_json, response.body) + end + + def test_render_xml_exception + @app = ShowExceptionsOverriddenController.action(:boom) + get "/", headers: { "HTTP_ACCEPT" => "application/xml" } + assert_response :internal_server_error + assert_equal "application/xml", response.content_type.to_s + assert_equal({ status: 500, error: "Internal Server Error" }.to_xml, response.body) + end + + def test_render_fallback_exception + @app = ShowExceptionsOverriddenController.action(:boom) + get "/", headers: { "HTTP_ACCEPT" => "text/csv" } + assert_response :internal_server_error + assert_equal "text/html", response.content_type.to_s + end + end + + class ShowFailsafeExceptionsTest < ActionDispatch::IntegrationTest + def test_render_failsafe_exception + @app = ShowExceptionsOverriddenController.action(:boom) + @exceptions_app = @app.instance_variable_get(:@exceptions_app) + @app.instance_variable_set(:@exceptions_app, nil) + $stderr = StringIO.new + + get "/", headers: { "HTTP_ACCEPT" => "text/json" } + assert_response :internal_server_error + assert_equal "text/plain", response.content_type.to_s + ensure + @app.instance_variable_set(:@exceptions_app, @exceptions_app) + $stderr = STDERR + end + end +end diff --git a/actionpack/test/controller/streaming_test.rb b/actionpack/test/controller/streaming_test.rb new file mode 100644 index 0000000000..5a42e2ae6d --- /dev/null +++ b/actionpack/test/controller/streaming_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionController + class StreamingResponseTest < ActionController::TestCase + class TestController < ActionController::Base + def self.controller_path + "test" + end + + def basic_stream + %w{ hello world }.each do |word| + response.stream.write word + response.stream.write "\n" + end + response.stream.close + end + end + + tests TestController + + def test_write_to_stream + get :basic_stream + assert_equal "hello\nworld\n", @response.body + end + end +end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb new file mode 100644 index 0000000000..d1cd190747 --- /dev/null +++ b/actionpack/test/controller/test_case_test.rb @@ -0,0 +1,1197 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_controllers" +require "active_support/json/decoding" +require "rails/engine" + +class TestCaseTest < ActionController::TestCase + def self.fixture_path; end + + class TestController < ActionController::Base + def no_op + render plain: "dummy" + end + + def set_flash + flash["test"] = ">#{flash["test"]}<" + render plain: "ignore me" + end + + def delete_flash + flash.delete("test") + render plain: "ignore me" + end + + def set_flash_now + flash.now["test_now"] = ">#{flash["test_now"]}<" + render plain: "ignore me" + end + + def set_session + session["string"] = "A wonder" + session[:symbol] = "it works" + render plain: "Success" + end + + def reset_the_session + reset_session + render plain: "ignore me" + end + + def render_raw_post + raise ActiveSupport::TestCase::Assertion, "#raw_post is blank" if request.raw_post.blank? + render plain: request.raw_post + end + + def render_body + render plain: request.body.read + end + + def test_params + render plain: ::JSON.dump(params.to_unsafe_h) + end + + def test_query_parameters + render plain: ::JSON.dump(request.query_parameters) + end + + def test_request_parameters + render plain: request.request_parameters.inspect + end + + def test_uri + render plain: request.fullpath + end + + def test_format + render plain: request.format + end + + def test_query_string + render plain: request.query_string + end + + def test_protocol + render plain: request.protocol + end + + def test_headers + render plain: ::JSON.dump(request.headers.env) + end + + def test_html_output + render plain: <<HTML +<html> + <body> + <a href="/"><img src="/images/button.png" /></a> + <div id="foo"> + <ul> + <li class="item">hello</li> + <li class="item">goodbye</li> + </ul> + </div> + <div id="bar"> + <form action="/somewhere"> + Name: <input type="text" name="person[name]" id="person_name" /> + </form> + </div> + </body> +</html> +HTML + end + + def test_xml_output + response.content_type = params[:response_as] + render plain: <<XML +<?xml version="1.0" encoding="UTF-8"?> +<root> + <area><p>area is an empty tag in HTML, so it won't contain this content</p></area> +</root> +XML + end + + def test_only_one_param + render plain: (params[:left] && params[:right]) ? "EEP, Both here!" : "OK" + end + + def test_remote_addr + render plain: (request.remote_addr || "not specified") + end + + def test_file_upload + render plain: params[:file].size + end + + def test_send_file + send_file(__FILE__) + end + + def redirect_to_same_controller + redirect_to controller: "test", action: "test_uri", id: 5 + end + + def redirect_to_different_controller + redirect_to controller: "fail", id: 5 + end + + def create + head :created, location: "/resource" + end + + def render_cookie + render plain: cookies["foo"] + end + + def delete_cookie + cookies.delete("foo") + render plain: "ok" + end + + def test_without_body + render html: '<div class="foo"></div>'.html_safe + end + + def test_with_body + render html: '<body class="foo"></body>'.html_safe + end + + def render_json + render json: request.raw_post + end + + def boom + raise "boom!" + end + + private + + def generate_url(opts) + url_for(opts.merge(action: "test_uri")) + end + end + + def setup + super + @controller = TestController.new + @request.delete_header "PATH_INFO" + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw do + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))" + end + end + end + end + + class DefaultUrlOptionsCachingController < ActionController::Base + before_action { @dynamic_opt = "opt" } + + def test_url_options_reset + render plain: url_for + end + + def default_url_options + if defined?(@dynamic_opt) + super.merge dynamic_opt: @dynamic_opt + else + super + end + end + end + + def test_assert_select_without_body + get :test_without_body + + assert_select "body", 0 + assert_select "div.foo" + end + + def test_assert_select_with_body + get :test_with_body + + assert_select "body.foo" + end + + def test_url_options_reset + @controller = DefaultUrlOptionsCachingController.new + get :test_url_options_reset + assert_nil @request.params["dynamic_opt"] + assert_match(/dynamic_opt=opt/, @response.body) + end + + def test_raw_post_handling + params = Hash[:page, { name: "page name" }, "some key", 123] + post :render_raw_post, params: params.dup + + assert_equal params.to_query, @response.body + end + + def test_params_round_trip + params = { "foo" => { "contents" => [{ "name" => "gorby", "id" => "123" }, { "name" => "puff", "d" => "true" }] } } + post :test_params, params: params.dup + + controller_info = { "controller" => "test_case_test/test", "action" => "test_params" } + assert_equal params.merge(controller_info), JSON.parse(@response.body) + end + + def test_handle_to_params + klass = Class.new do + def to_param + "bar" + end + end + + post :test_params, params: { foo: klass.new } + + assert_equal JSON.parse(@response.body)["foo"], "bar" + end + + + def test_body_stream + params = Hash[:page, { name: "page name" }, "some key", 123] + + post :render_body, params: params.dup + + assert_equal params.to_query, @response.body + end + + def test_document_body_and_params_with_post + post :test_params, params: { id: 1 } + assert_equal({ "id" => "1", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body)) + end + + def test_document_body_with_post + post :render_body, body: "document body" + assert_equal "document body", @response.body + end + + def test_document_body_with_put + put :render_body, body: "document body" + assert_equal "document body", @response.body + end + + def test_head + head :test_params + assert_equal 200, @response.status + end + + def test_process_without_flash + process :set_flash + assert_equal "><", flash["test"] + end + + def test_process_with_flash + process :set_flash, + method: "GET", + flash: { "test" => "value" } + assert_equal ">value<", flash["test"] + end + + def test_process_with_flash_now + process :set_flash_now, + method: "GET", + flash: { "test_now" => "value_now" } + assert_equal ">value_now<", flash["test_now"] + end + + def test_process_delete_flash + process :set_flash + process :delete_flash + assert_empty flash + assert_empty session + end + + def test_process_with_session + process :set_session + assert_equal "A wonder", session["string"], "A value stored in the session should be available by string key" + assert_equal "A wonder", session[:string], "Test session hash should allow indifferent access" + assert_equal "it works", session["symbol"], "Test session hash should allow indifferent access" + assert_equal "it works", session[:symbol], "Test session hash should allow indifferent access" + end + + def test_process_with_session_kwarg + process :no_op, method: "GET", session: { "string" => "value1", symbol: "value2" } + assert_equal "value1", session["string"] + assert_equal "value1", session[:string] + assert_equal "value2", session["symbol"] + assert_equal "value2", session[:symbol] + end + + def test_process_merges_session_arg + session[:foo] = "bar" + get :no_op, session: { bar: "baz" } + assert_equal "bar", session[:foo] + assert_equal "baz", session[:bar] + end + + def test_merged_session_arg_is_retained_across_requests + get :no_op, session: { foo: "bar" } + assert_equal "bar", session[:foo] + get :no_op + assert_equal "bar", session[:foo] + end + + def test_process_overwrites_existing_session_arg + session[:foo] = "bar" + get :no_op, session: { foo: "baz" } + assert_equal "baz", session[:foo] + end + + def test_session_is_cleared_from_controller_after_reset_session + process :set_session + process :reset_the_session + assert_equal Hash.new, @controller.session.to_hash + end + + def test_session_is_cleared_from_request_after_reset_session + process :set_session + process :reset_the_session + assert_equal Hash.new, @request.session.to_hash + end + + def test_response_and_request_have_nice_accessors + process :no_op + assert_equal @response, response + assert_equal @request, request + end + + def test_process_with_request_uri_with_no_params + process :test_uri + assert_equal "/test_case_test/test/test_uri", @response.body + end + + def test_process_with_symbol_method + process :test_uri, method: :get + assert_equal "/test_case_test/test/test_uri", @response.body + end + + def test_process_with_request_uri_with_params + process :test_uri, + method: "GET", + params: { id: 7 } + + assert_equal "/test_case_test/test/test_uri/7", @response.body + end + + def test_process_with_request_uri_with_params_with_explicit_uri + @request.env["PATH_INFO"] = "/explicit/uri" + process :test_uri, method: "GET", params: { id: 7 } + assert_equal "/explicit/uri", @response.body + end + + def test_process_with_query_string + process :test_query_string, + method: "GET", + params: { q: "test" } + assert_equal "q=test", @response.body + end + + def test_process_with_query_string_with_explicit_uri + @request.env["PATH_INFO"] = "/explicit/uri" + @request.env["QUERY_STRING"] = "q=test?extra=question" + process :test_query_string + assert_equal "q=test?extra=question", @response.body + end + + def test_multiple_calls + process :test_only_one_param, method: "GET", params: { left: true } + assert_equal "OK", @response.body + process :test_only_one_param, method: "GET", params: { right: true } + assert_equal "OK", @response.body + end + + def test_should_impose_childless_html_tags_in_html + process :test_xml_output, params: { response_as: "text/html" } + + # <area> auto-closes, so the <p> becomes a sibling + if defined?(JRUBY_VERSION) + # https://github.com/sparklemotion/nokogiri/issues/1653 + # HTML parser "fixes" "broken" markup in slightly different ways + assert_select "root > map > area + p" + else + assert_select "root > area + p" + end + end + + def test_should_not_impose_childless_html_tags_in_xml + process :test_xml_output, params: { response_as: "application/xml" } + + # <area> is not special, so the <p> is its child + assert_select "root > area > p" + end + + def test_assert_generates + assert_generates "controller/action/5", controller: "controller", action: "action", id: "5" + assert_generates "controller/action/7", { id: "7" }, { controller: "controller", action: "action" } + assert_generates "controller/action/5", { controller: "controller", action: "action", id: "5", name: "bob" }, {}, { name: "bob" } + assert_generates "controller/action/7", { id: "7", name: "bob" }, { controller: "controller", action: "action" }, { name: "bob" } + assert_generates "controller/action/7", { id: "7" }, { controller: "controller", action: "action", name: "bob" }, {} + end + + def test_assert_routing + assert_routing "content", controller: "content", action: "index" + end + + def test_assert_routing_with_method + with_routing do |set| + set.draw { resources(:content) } + assert_routing({ method: "post", path: "content" }, { controller: "content", action: "create" }) + end + end + + def test_assert_routing_in_module + with_routing do |set| + set.draw do + namespace :admin do + get "user" => "user#index" + end + end + + assert_routing "admin/user", controller: "admin/user", action: "index" + end + end + + def test_assert_routing_with_glob + with_routing do |set| + set.draw { get("*path" => "pages#show") } + assert_routing("/company/about", controller: "pages", action: "show", path: "company/about") + end + end + + def test_params_passing + get :test_params, params: { + page: { + name: "Page name", + month: "4", + year: "2004", + day: "6" + } + } + parsed_params = ::JSON.parse(@response.body) + assert_equal( + { + "controller" => "test_case_test/test", "action" => "test_params", + "page" => { "name" => "Page name", "month" => "4", "year" => "2004", "day" => "6" } + }, + parsed_params + ) + end + + def test_nil_params + get :test_params, params: nil + parsed_params = JSON.parse(@response.body) + assert_equal( + { + "action" => "test_params", + "controller" => "test_case_test/test" + }, + parsed_params + ) + end + + def test_query_param_named_action + get :test_query_parameters, params: { action: "foobar" } + parsed_params = JSON.parse(@response.body) + assert_equal({ "action" => "foobar" }, parsed_params) + end + + def test_request_param_named_action + post :test_request_parameters, params: { action: "foobar" } + parsed_params = eval(@response.body) + assert_equal({ "action" => "foobar" }, parsed_params) + end + + def test_kwarg_params_passing_with_session_and_flash + get :test_params, params: { + page: { + name: "Page name", + month: "4", + year: "2004", + day: "6" + } + }, session: { "foo" => "bar" }, flash: { notice: "created" } + + parsed_params = ::JSON.parse(@response.body) + assert_equal( + { "controller" => "test_case_test/test", "action" => "test_params", + "page" => { "name" => "Page name", "month" => "4", "year" => "2004", "day" => "6" } }, + parsed_params + ) + + assert_equal "bar", session[:foo] + assert_equal "created", flash[:notice] + end + + def test_params_passing_with_integer + get :test_params, params: { + page: { name: "Page name", month: 4, year: 2004, day: 6 } + } + parsed_params = ::JSON.parse(@response.body) + assert_equal( + { "controller" => "test_case_test/test", "action" => "test_params", + "page" => { "name" => "Page name", "month" => "4", "year" => "2004", "day" => "6" } }, + parsed_params + ) + end + + def test_params_passing_with_integers_when_not_html_request + get :test_params, params: { format: "json", count: 999 } + parsed_params = ::JSON.parse(@response.body) + assert_equal( + { "controller" => "test_case_test/test", "action" => "test_params", + "format" => "json", "count" => "999" }, + parsed_params + ) + end + + def test_params_passing_path_parameter_is_string_when_not_html_request + get :test_params, params: { format: "json", id: 1 } + parsed_params = ::JSON.parse(@response.body) + assert_equal( + { "controller" => "test_case_test/test", "action" => "test_params", + "format" => "json", "id" => "1" }, + parsed_params + ) + end + + def test_params_passing_with_frozen_values + assert_nothing_raised do + get :test_params, params: { + frozen: -"icy", frozens: [-"icy"].freeze, deepfreeze: { frozen: -"icy" }.freeze + } + end + parsed_params = ::JSON.parse(@response.body) + assert_equal( + { "controller" => "test_case_test/test", "action" => "test_params", + "frozen" => "icy", "frozens" => ["icy"], "deepfreeze" => { "frozen" => "icy" } }, + parsed_params + ) + end + + def test_params_passing_doesnt_modify_in_place + page = { name: "Page name", month: 4, year: 2004, day: 6 } + get :test_params, params: { page: page } + assert_equal 2004, page[:year] + end + + test "set additional HTTP headers" do + @request.headers["Referer"] = "http://nohost.com/home" + @request.headers["Content-Type"] = "application/rss+xml" + get :test_headers + parsed_env = ActiveSupport::JSON.decode(@response.body) + assert_equal "http://nohost.com/home", parsed_env["HTTP_REFERER"] + assert_equal "application/rss+xml", parsed_env["CONTENT_TYPE"] + end + + test "set additional env variables" do + @request.headers["HTTP_REFERER"] = "http://example.com/about" + @request.headers["CONTENT_TYPE"] = "application/json" + get :test_headers + parsed_env = ActiveSupport::JSON.decode(@response.body) + assert_equal "http://example.com/about", parsed_env["HTTP_REFERER"] + assert_equal "application/json", parsed_env["CONTENT_TYPE"] + end + + def test_using_as_json_sets_request_content_type_to_json + post :render_body, params: { bool_value: true, str_value: "string", num_value: 2 }, as: :json + + assert_equal "application/json", @request.headers["CONTENT_TYPE"] + assert_equal true, @request.request_parameters[:bool_value] + assert_equal "string", @request.request_parameters[:str_value] + assert_equal 2, @request.request_parameters[:num_value] + end + + def test_using_as_json_sets_format_json + post :render_body, params: { bool_value: true, str_value: "string", num_value: 2 }, as: :json + assert_equal "json", @request.format + end + + def test_mutating_content_type_headers_for_plain_text_files_sets_the_header + @request.headers["Content-Type"] = "text/plain" + post :render_body, params: { name: "foo.txt" } + + assert_equal "text/plain", @request.headers["Content-type"] + assert_equal "foo.txt", @request.request_parameters[:name] + assert_equal "render_body", @request.path_parameters[:action] + end + + def test_mutating_content_type_headers_for_html_files_sets_the_header + @request.headers["Content-Type"] = "text/html" + post :render_body, params: { name: "foo.html" } + + assert_equal "text/html", @request.headers["Content-type"] + assert_equal "foo.html", @request.request_parameters[:name] + assert_equal "render_body", @request.path_parameters[:action] + end + + def test_mutating_content_type_headers_for_non_registered_mime_type_raises_an_error + assert_raises(RuntimeError) do + @request.headers["Content-Type"] = "type/fake" + post :render_body, params: { name: "foo.fake" } + end + end + + def test_id_converted_to_string + get :test_params, params: { + id: 20, foo: Object.new + } + assert_kind_of String, @request.path_parameters[:id] + end + + def test_array_path_parameter_handled_properly + with_routing do |set| + set.draw do + get "file/*path", to: "test_case_test/test#test_params" + + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + + get :test_params, params: { path: ["hello", "world"] } + assert_equal ["hello", "world"], @request.path_parameters[:path] + assert_equal "hello/world", @request.path_parameters[:path].to_param + end + end + + def test_assert_realistic_path_parameters + get :test_params, params: { id: 20, foo: Object.new } + + # All elements of path_parameters should use Symbol keys + @request.path_parameters.each_key do |key| + assert_kind_of Symbol, key + end + end + + def test_with_routing_places_routes_back + assert @routes + routes_id = @routes.object_id + + begin + with_routing { raise "fail" } + fail "Should not be here." + rescue RuntimeError + end + + assert @routes + assert_equal routes_id, @routes.object_id + end + + def test_remote_addr + get :test_remote_addr + assert_equal "0.0.0.0", @response.body + + @request.remote_addr = "192.0.0.1" + get :test_remote_addr + assert_equal "192.0.0.1", @response.body + end + + def test_header_properly_reset_after_remote_http_request + get :test_params, xhr: true + assert_nil @request.env["HTTP_X_REQUESTED_WITH"] + assert_nil @request.env["HTTP_ACCEPT"] + end + + def test_xhr_with_params + get :test_params, params: { id: 1 }, xhr: true + + assert_equal({ "id" => "1", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body)) + end + + def test_xhr_with_session + get :set_session, xhr: true + + assert_equal "A wonder", session["string"], "A value stored in the session should be available by string key" + assert_equal "A wonder", session[:string], "Test session hash should allow indifferent access" + assert_equal "it works", session["symbol"], "Test session hash should allow indifferent access" + assert_equal "it works", session[:symbol], "Test session hash should allow indifferent access" + end + + def test_params_reset_between_post_requests + post :no_op, params: { foo: "bar" } + assert_equal "bar", @request.params[:foo] + + post :no_op + assert_predicate @request.params[:foo], :blank? + end + + def test_filtered_parameters_reset_between_requests + get :no_op, params: { foo: "bar" } + assert_equal "bar", @request.filtered_parameters[:foo] + + get :no_op, params: { foo: "baz" } + assert_equal "baz", @request.filtered_parameters[:foo] + end + + def test_raw_post_reset_between_post_requests + post :no_op, params: { foo: "bar" } + assert_equal "foo=bar", @request.raw_post + + post :no_op, params: { foo: "baz" } + assert_equal "foo=baz", @request.raw_post + end + + def test_content_length_reset_after_post_request + post :no_op, params: { foo: "bar" } + assert_not_equal 0, @request.content_length + + get :no_op + assert_equal 0, @request.content_length + end + + def test_path_is_kept_after_the_request + get :test_params, params: { id: "foo" } + assert_equal "/test_case_test/test/test_params/foo", @request.path + end + + def test_path_params_reset_between_request + get :test_params, params: { id: "foo" } + assert_equal "foo", @request.path_parameters[:id] + + get :test_params + assert_nil @request.path_parameters[:id] + end + + def test_request_protocol_is_reset_after_request + get :test_protocol + assert_equal "http://", @response.body + + @request.env["HTTPS"] = "on" + get :test_protocol + assert_equal "https://", @response.body + + @request.env.delete("HTTPS") + get :test_protocol + assert_equal "http://", @response.body + end + + def test_request_format + get :test_format, params: { format: "html" } + assert_equal "text/html", @response.body + + get :test_format, params: { format: "json" } + assert_equal "application/json", @response.body + + get :test_format, params: { format: "xml" } + assert_equal "application/xml", @response.body + + get :test_format + assert_equal "text/html", @response.body + end + + def test_request_format_kwarg + get :test_format, format: "html" + assert_equal "text/html", @response.body + + get :test_format, format: "json" + assert_equal "application/json", @response.body + + get :test_format, format: "xml" + assert_equal "application/xml", @response.body + + get :test_format + assert_equal "text/html", @response.body + end + + def test_request_format_kwarg_overrides_params + get :test_format, format: "json", params: { format: "html" } + assert_equal "application/json", @response.body + end + + def test_request_format_kwarg_doesnt_mutate_params + params = { foo: "bar" }.freeze + + assert_nothing_raised do + get :test_format, format: "json", params: params + end + end + + def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set + cookies["foo"] = "bar" + get :no_op + assert_equal "bar", cookies["foo"] + end + + def test_cookies_should_be_escaped_properly + cookies["foo"] = "+" + get :render_cookie + assert_equal "+", @response.body + end + + def test_should_detect_if_cookie_is_deleted + cookies["foo"] = "bar" + get :delete_cookie + assert_nil cookies["foo"] + end + + def test_multiple_mixed_method_process_should_scrub_rack_input + post :test_params, params: { id: 1, foo: "an foo" } + assert_equal({ "id" => "1", "foo" => "an foo", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body)) + + get :test_params, params: { bar: "an bar" } + assert_equal({ "bar" => "an bar", "controller" => "test_case_test/test", "action" => "test_params" }, ::JSON.parse(@response.body)) + end + + %w(controller response request).each do |variable| + %w(get post put delete head process).each do |method| + define_method("test_#{variable}_missing_for_#{method}_raises_error") do + remove_instance_variable "@#{variable}" + begin + send(method, :test_remote_addr) + assert false, "expected RuntimeError, got nothing" + rescue RuntimeError => error + assert_match(%r{@#{variable} is nil}, error.message) + rescue => error + assert false, "expected RuntimeError, got #{error.class}" + end + end + end + end + + FILES_DIR = File.expand_path("../fixtures/multipart", __dir__) + + READ_BINARY = "rb:binary" + READ_PLAIN = "r:binary" + + def test_test_uploaded_file + filename = "ruby_on_rails.jpg" + path = "#{FILES_DIR}/#{filename}" + content_type = "image/png" + expected = File.read(path) + expected.force_encoding(Encoding::BINARY) + + file = Rack::Test::UploadedFile.new(path, content_type) + assert_equal filename, file.original_filename + assert_equal content_type, file.content_type + assert_equal file.path, file.local_path + assert_equal expected, file.read + + new_content_type = "new content_type" + file.content_type = new_content_type + assert_equal new_content_type, file.content_type + end + + def test_fixture_path_is_accessed_from_self_instead_of_active_support_test_case + TestCaseTest.stub :fixture_path, FILES_DIR do + uploaded_file = fixture_file_upload("/ruby_on_rails.jpg", "image/png") + assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read + end + end + + def test_test_uploaded_file_with_binary + filename = "ruby_on_rails.jpg" + path = "#{FILES_DIR}/#{filename}" + content_type = "image/png" + + binary_uploaded_file = Rack::Test::UploadedFile.new(path, content_type, :binary) + assert_equal File.open(path, READ_BINARY).read, binary_uploaded_file.read + + plain_uploaded_file = Rack::Test::UploadedFile.new(path, content_type) + assert_equal File.open(path, READ_PLAIN).read, plain_uploaded_file.read + end + + def test_fixture_file_upload_with_binary + filename = "ruby_on_rails.jpg" + path = "#{FILES_DIR}/#{filename}" + content_type = "image/jpg" + + binary_file_upload = fixture_file_upload(path, content_type, :binary) + assert_equal File.open(path, READ_BINARY).read, binary_file_upload.read + + plain_file_upload = fixture_file_upload(path, content_type) + assert_equal File.open(path, READ_PLAIN).read, plain_file_upload.read + end + + def test_fixture_file_upload_should_be_able_access_to_tempfile + file = fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg") + assert_respond_to file, :tempfile + end + + def test_fixture_file_upload + post :test_file_upload, + params: { + file: fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg") + } + assert_equal "45142", @response.body + end + + def test_fixture_file_upload_relative_to_fixture_path + TestCaseTest.stub :fixture_path, FILES_DIR do + uploaded_file = fixture_file_upload("ruby_on_rails.jpg", "image/jpg") + assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read + end + end + + def test_fixture_file_upload_ignores_fixture_path_given_full_path + TestCaseTest.stub :fixture_path, __dir__ do + uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg") + assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read + end + end + + def test_fixture_file_upload_ignores_nil_fixture_path + uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg") + assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read + end + + def test_action_dispatch_uploaded_file_upload + filename = "ruby_on_rails.jpg" + path = "#{FILES_DIR}/#{filename}" + post :test_file_upload, params: { + file: Rack::Test::UploadedFile.new(path, "image/jpg", true) + } + assert_equal "45142", @response.body + end + + def test_test_uploaded_file_exception_when_file_doesnt_exist + assert_raise(RuntimeError) { Rack::Test::UploadedFile.new("non_existent_file") } + end + + def test_redirect_url_only_cares_about_location_header + get :create + assert_response :created + + # Redirect url doesn't care that it wasn't a :redirect response. + assert_equal "/resource", @response.redirect_url + assert_equal @response.redirect_url, redirect_to_url + + # Must be a :redirect response. + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_redirected_to "/resource" + end + end + + def test_exception_in_action_reaches_test + assert_raise(RuntimeError) do + process :boom, method: "GET" + end + end + + def test_request_state_is_cleared_after_exception + assert_raise(RuntimeError) do + process :boom, + method: "GET", + params: { q: "test1" } + end + + process :test_query_string, + method: "GET", + params: { q: "test2" } + + assert_equal "q=test2", @response.body + end + + def test_parsed_body_without_as_option + post :render_json, body: { foo: "heyo" } + assert_equal({ "foo" => "heyo" }, response.parsed_body) + end + + def test_parsed_body_with_as_option + post :render_json, body: { foo: "heyo" }.to_json, as: :json + assert_equal({ "foo" => "heyo" }, response.parsed_body) + end +end + +class ResponseDefaultHeadersTest < ActionController::TestCase + class TestController < ActionController::Base + def remove_header + headers.delete params[:header] + head :ok, "C" => "3" + end + + # Render a head response, but don't touch default headers + def leave_alone + head :ok + end + end + + def before_setup + @original = ActionDispatch::Response.default_headers + @defaults = { "A" => "1", "B" => "2" } + ActionDispatch::Response.default_headers = @defaults + super + end + + teardown do + ActionDispatch::Response.default_headers = @original + end + + def setup + super + @controller = TestController.new + @request.env["PATH_INFO"] = nil + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw do + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))" + end + end + end + end + + test "response contains default headers" do + get :leave_alone + + # Response headers start out with the defaults + assert_equal @defaults.merge("Content-Type" => "text/html"), response.headers + end + + test "response deletes a default header" do + get :remove_header, params: { header: "A" } + assert_response :ok + + # After a request, the response in the test case doesn't have the + # defaults merged on top again. + assert_not_includes response.headers, "A" + assert_includes response.headers, "B" + assert_includes response.headers, "C" + end +end + +module EngineControllerTests + class Engine < ::Rails::Engine + isolate_namespace EngineControllerTests + + routes.draw do + get "/" => "bar#index" + end + end + + class BarController < ActionController::Base + def index + render plain: "bar" + end + end + + class BarControllerTest < ActionController::TestCase + tests BarController + + def test_engine_controller_route + get :index + assert_equal @response.body, "bar" + end + end + + class BarControllerTestWithExplicitRouteSet < ActionController::TestCase + tests BarController + + def setup + @routes = Engine.routes + end + + def test_engine_controller_route + get :index + assert_equal @response.body, "bar" + end + end +end + +class InferringClassNameTest < ActionController::TestCase + def test_determine_controller_class + assert_equal ContentController, determine_class("ContentControllerTest") + end + + def test_determine_controller_class_with_nonsense_name + assert_nil determine_class("HelloGoodBye") + end + + def test_determine_controller_class_with_sensible_name_where_no_controller_exists + assert_nil determine_class("NoControllerWithThisNameTest") + end + + private + def determine_class(name) + ActionController::TestCase.determine_default_controller_class(name) + end +end + +class CrazyNameTest < ActionController::TestCase + tests ContentController + + def test_controller_class_can_be_set_manually_not_just_inferred + assert_equal ContentController, self.class.controller_class + end +end + +class CrazySymbolNameTest < ActionController::TestCase + tests :content + + def test_set_controller_class_using_symbol + assert_equal ContentController, self.class.controller_class + end +end + +class CrazyStringNameTest < ActionController::TestCase + tests "content" + + def test_set_controller_class_using_string + assert_equal ContentController, self.class.controller_class + end +end + +class NamedRoutesControllerTest < ActionController::TestCase + tests ContentController + + def test_should_be_able_to_use_named_routes_before_a_request_is_done + with_routing do |set| + set.draw { resources :contents } + assert_equal "http://test.host/contents/new", new_content_url + assert_equal "http://test.host/contents/1", content_url(id: 1) + end + end +end + +class AnonymousControllerTest < ActionController::TestCase + def setup + @controller = Class.new(ActionController::Base) do + def index + render plain: params[:controller] + end + end.new + + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw do + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))" + end + end + end + end + + def test_controller_name + get :index + assert_equal "anonymous", @response.body + end +end + +class RoutingDefaultsTest < ActionController::TestCase + def setup + @controller = Class.new(ActionController::Base) do + def post + render plain: request.fullpath + end + + def project + render plain: request.fullpath + end + end.new + + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw do + get "/posts/:id", to: "anonymous#post", bucket_type: "post" + get "/projects/:id", to: "anonymous#project", defaults: { bucket_type: "project" } + end + end + end + + def test_route_option_can_be_passed_via_process + get :post, params: { id: 1, bucket_type: "post" } + assert_equal "/posts/1", @response.body + end + + def test_route_default_is_not_required_for_building_request_uri + get :project, params: { id: 2 } + assert_equal "/projects/2", @response.body + end +end diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb new file mode 100644 index 0000000000..a1521da702 --- /dev/null +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_controllers" +require "active_support/core_ext/object/with_options" + +module ActionPack + class URLForIntegrationTest < ActiveSupport::TestCase + include RoutingTestHelpers + include ActionDispatch::RoutingVerbs + + Model = Struct.new(:to_param) + + Mapping = lambda { + namespace :admin do + resources :users, :posts + end + + namespace "api" do + root to: "users#index" + end + + get "/blog(/:year(/:month(/:day)))" => "posts#show_date", + :constraints => { + year: /(19|20)\d\d/, + month: /[01]?\d/, + day: /[0-3]?\d/ + }, + :day => nil, + :month => nil + + get "archive/:year", controller: "archive", action: "index", + defaults: { year: nil }, + constraints: { year: /\d{4}/ }, + as: "blog" + + resources :people + + get "symbols", controller: :symbols, action: :show, name: :as_symbol + get "id_default(/:id)" => "foo#id_default", :id => 1 + match "get_or_post" => "foo#get_or_post", :via => [:get, :post] + get "optional/:optional" => "posts#index" + get "projects/:project_id" => "project#index", :as => "project" + get "clients" => "projects#index" + + get "ignorecase/geocode/:postalcode" => "geocode#show", :postalcode => /hx\d\d-\d[a-z]{2}/i + get "extended/geocode/:postalcode" => "geocode#show", :constraints => { + postalcode: /# Postcode format + \d{5} #Prefix + (-\d{4})? #Suffix + /x + }, :as => "geocode" + + get "news(.:format)" => "news#index" + + ActiveSupport::Deprecation.silence { + get "comment/:id(/:action)" => "comments#show" + get "ws/:controller(/:action(/:id))", ws: true + get "account(/:action)" => "account#subscription" + get "pages/:page_id/:controller(/:action(/:id))" + get ":controller/ping", action: "ping" + get ":controller(/:action(/:id))(.:format)" + } + + root to: "news#index" + } + + attr_reader :routes + attr_accessor :controller + + def setup + @routes = make_set false + @routes.draw(&Mapping) + end + + [ + ["/admin/users", [ { use_route: "admin_users" }]], + ["/admin/users", [ { controller: "admin/users" }]], + ["/admin/users", [ { controller: "admin/users", action: "index" }]], + ["/admin/users", [ { action: "index" }, { controller: "admin/users", action: "index" }, "/admin/users"]], + ["/admin/users", [ { controller: "users", action: "index" }, { controller: "admin/accounts", action: "show", id: "1" }, "/admin/accounts/show/1"]], + ["/people", [ { controller: "/people", action: "index" }, { controller: "admin/accounts", action: "foo", id: "bar" }, "/admin/accounts/foo/bar"]], + + ["/admin/posts", [ { controller: "admin/posts" }]], + ["/admin/posts/new", [ { controller: "admin/posts", action: "new" }]], + + ["/blog/2009", [ { controller: "posts", action: "show_date", year: 2009 }]], + ["/blog/2009/1", [ { controller: "posts", action: "show_date", year: 2009, month: 1 }]], + ["/blog/2009/1/1", [ { controller: "posts", action: "show_date", year: 2009, month: 1, day: 1 }]], + + ["/archive/2010", [ { controller: "archive", action: "index", year: "2010" }]], + ["/archive", [ { controller: "archive", action: "index" }]], + ["/archive?year=january", [ { controller: "archive", action: "index", year: "january" }]], + + ["/people", [ { controller: "people", action: "index" }]], + ["/people", [ { action: "index" }, { controller: "people", action: "index" }, "/people"]], + ["/people", [ { action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people", [ { controller: "people", action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people", [ {}, { controller: "people", action: "index" }, "/people"]], + ["/people/1", [ { controller: "people", action: "show" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people/new", [ { use_route: "new_person" }]], + ["/people/new", [ { controller: "people", action: "new" }]], + ["/people/1", [ { use_route: "person", id: "1" }]], + ["/people/1", [ { controller: "people", action: "show", id: "1" }]], + ["/people/1.xml", [ { controller: "people", action: "show", id: "1", format: "xml" }]], + ["/people/1", [ { controller: "people", action: "show", id: 1 }]], + ["/people/1", [ { controller: "people", action: "show", id: Model.new("1") }]], + ["/people/1", [ { action: "show", id: "1" }, { controller: "people", action: "index" }, "/people"]], + ["/people/1", [ { action: "show", id: 1 }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people", [ { controller: "people", action: "index" }, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people/1", [ {}, { controller: "people", action: "show", id: "1" }, "/people/show/1"]], + ["/people/1", [ { controller: "people", action: "show" }, { controller: "people", action: "index", id: "1" }, "/people/index/1"]], + ["/people/1/edit", [ { controller: "people", action: "edit", id: "1" }]], + ["/people/1/edit.xml", [ { controller: "people", action: "edit", id: "1", format: "xml" }]], + ["/people/1/edit", [ { use_route: "edit_person", id: "1" }]], + ["/people/1?legacy=true", [ { controller: "people", action: "show", id: "1", legacy: "true" }]], + ["/people?legacy=true", [ { controller: "people", action: "index", legacy: "true" }]], + + ["/id_default/2", [ { controller: "foo", action: "id_default", id: "2" }]], + ["/id_default", [ { controller: "foo", action: "id_default", id: "1" }]], + ["/id_default", [ { controller: "foo", action: "id_default", id: 1 }]], + ["/id_default", [ { controller: "foo", action: "id_default" }]], + ["/optional/bar", [ { controller: "posts", action: "index", optional: "bar" }]], + ["/posts", [ { controller: "posts", action: "index" }]], + + ["/project", [ { controller: "project", action: "index" }]], + ["/projects/1", [ { controller: "project", action: "index", project_id: "1" }]], + ["/projects/1", [ { controller: "project", action: "index" }, { project_id: "1", controller: "project", action: "index" }, "/projects/1"]], + ["/projects/1", [ { use_route: "project", controller: "project", action: "index", project_id: "1" }]], + ["/projects/1", [ { use_route: "project", controller: "project", action: "index" }, { controller: "project", action: "index", project_id: "1" }, "/projects/1"]], + + ["/clients", [ { controller: "projects", action: "index" }]], + ["/clients?project_id=1", [ { controller: "projects", action: "index", project_id: "1" }]], + ["/clients", [ { controller: "projects", action: "index" }, { project_id: "1", controller: "project", action: "index" }, "/projects/1"]], + + ["/comment/20", [ { id: 20 }, { controller: "comments", action: "show" }, "/comments/show"]], + ["/comment/20", [ { controller: "comments", id: 20, action: "show" }]], + ["/comments/boo", [ { controller: "comments", action: "boo" }]], + + ["/ws/posts/show/1", [ { controller: "posts", action: "show", id: "1", ws: true }]], + ["/ws/posts", [ { controller: "posts", action: "index", ws: true }]], + + ["/account", [ { controller: "account", action: "subscription" }]], + ["/account/billing", [ { controller: "account", action: "billing" }]], + + ["/pages/1/notes/show/1", [ { page_id: "1", controller: "notes", action: "show", id: "1" }]], + ["/pages/1/notes/list", [ { page_id: "1", controller: "notes", action: "list" }]], + ["/pages/1/notes", [ { page_id: "1", controller: "notes", action: "index" }]], + ["/pages/1/notes", [ { page_id: "1", controller: "notes" }]], + ["/notes", [ { page_id: nil, controller: "notes" }]], + ["/notes", [ { controller: "notes" }]], + ["/notes/print", [ { controller: "notes", action: "print" }]], + ["/notes/print", [ {}, { controller: "notes", action: "print" }, "/notes/print"]], + + ["/notes/index/1", [ { controller: "notes" }, { controller: "notes", action: "index", id: "1" }, "/notes/index/1"]], + ["/notes/index/1", [ { controller: "notes" }, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]], + ["/notes/index/1", [ { action: "index" }, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]], + ["/notes/index/1", [ {}, { controller: "notes", id: "1", action: "index" }, "/notes/index/1"]], + ["/notes/show/1", [ {}, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]], + ["/posts", [ { controller: "posts" }, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]], + ["/notes/list", [ { action: "list" }, { controller: "notes", action: "show", id: "1" }, "/notes/show/1"]], + + ["/posts/ping", [ { controller: "posts", action: "ping" }]], + ["/posts/show/1", [ { controller: "posts", action: "show", id: "1" }]], + ["/posts/show/1", [ { controller: "posts", action: "show", id: "1", format: "" }]], + ["/posts", [ { controller: "posts" }]], + ["/posts", [ { controller: "posts", action: "index" }]], + ["/posts/create", [ { action: "create" }, { day: nil, month: nil, controller: "posts", action: "show_date" }, "/blog"]], + ["/posts?foo=bar", [ { controller: "posts", foo: "bar" }]], + ["/posts?foo%5B%5D=bar&foo%5B%5D=baz", [{ controller: "posts", foo: ["bar", "baz"] }]], + ["/posts?page=2", [{ controller: "posts", page: 2 }]], + ["/posts?q%5Bfoo%5D%5Ba%5D=b", [{ controller: "posts", q: { foo: { a: "b" } } }]], + + ["/news.rss", [{ controller: "news", action: "index", format: "rss" }]], + ].each_with_index do |(url, params), i| + if params.length > 1 + hash, path_params, route = *params + hash[:only_path] = true + + define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do + get URI("http://test.host" + route.to_s) + assert_equal path_params, controller.request.path_parameters + assert_equal url, controller.url_for(hash), params.inspect + end + else + define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do + assert_equal url, url_for(@routes, params.first), params.inspect + end + end + end + end +end diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb new file mode 100644 index 0000000000..e381abee36 --- /dev/null +++ b/actionpack/test/controller/url_for_test.rb @@ -0,0 +1,519 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module AbstractController + module Testing + class UrlForTest < ActionController::TestCase + class W + include ActionDispatch::Routing::RouteSet.new.tap { |r| + r.draw { + ActiveSupport::Deprecation.silence { + get ":controller(/:action(/:id(.:format)))" + } + } + }.url_helpers + end + + def teardown + W.default_url_options.clear + end + + def test_nested_optional + klass = Class.new { + include ActionDispatch::Routing::RouteSet.new.tap { |r| + r.draw { + get "/foo/(:bar/(:baz))/:zot", as: "fun", + controller: :articles, + action: :index + } + }.url_helpers + default_url_options[:host] = "example.com" + } + + path = klass.new.fun_path(controller: :articles, + baz: "baz", + zot: "zot") + # :bar key isn't provided + assert_equal "/foo/zot", path + end + + def add_host!(app = W) + app.default_url_options[:host] = "www.basecamphq.com" + end + + def add_port! + W.default_url_options[:port] = 3000 + end + + def add_numeric_host! + W.default_url_options[:host] = "127.0.0.1" + end + + def test_exception_is_thrown_without_host + assert_raise ArgumentError do + W.new.url_for controller: "c", action: "a", id: "i" + end + end + + def test_anchor + assert_equal("/c/a#anchor", + W.new.url_for(only_path: true, controller: "c", action: "a", anchor: "anchor") + ) + end + + def test_nil_anchor + assert_equal( + "/c/a", + W.new.url_for(only_path: true, controller: "c", action: "a", anchor: nil) + ) + end + + def test_false_anchor + assert_equal( + "/c/a", + W.new.url_for(only_path: true, controller: "c", action: "a", anchor: false) + ) + end + + def test_anchor_should_call_to_param + assert_equal("/c/a#anchor", + W.new.url_for(only_path: true, controller: "c", action: "a", anchor: Struct.new(:to_param).new("anchor")) + ) + end + + def test_anchor_should_escape_unsafe_pchar + assert_equal("/c/a#%23anchor", + W.new.url_for(only_path: true, controller: "c", action: "a", anchor: Struct.new(:to_param).new("#anchor")) + ) + end + + def test_anchor_should_not_escape_safe_pchar + assert_equal("/c/a#name=user&email=user@domain.com", + W.new.url_for(only_path: true, controller: "c", action: "a", anchor: Struct.new(:to_param).new("name=user&email=user@domain.com")) + ) + end + + def test_default_host + add_host! + assert_equal("http://www.basecamphq.com/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i") + ) + end + + def test_host_may_be_overridden + add_host! + assert_equal("http://37signals.basecamphq.com/c/a/i", + W.new.url_for(host: "37signals.basecamphq.com", controller: "c", action: "a", id: "i") + ) + end + + def test_subdomain_may_be_changed + add_host! + assert_equal("http://api.basecamphq.com/c/a/i", + W.new.url_for(subdomain: "api", controller: "c", action: "a", id: "i") + ) + end + + def test_subdomain_may_be_object + model = Class.new { def self.to_param; "api"; end } + add_host! + assert_equal("http://api.basecamphq.com/c/a/i", + W.new.url_for(subdomain: model, controller: "c", action: "a", id: "i") + ) + end + + def test_subdomain_may_be_removed + add_host! + assert_equal("http://basecamphq.com/c/a/i", + W.new.url_for(subdomain: false, controller: "c", action: "a", id: "i") + ) + end + + def test_subdomain_may_be_removed_with_blank_string + W.default_url_options[:host] = "api.basecamphq.com" + assert_equal("http://basecamphq.com/c/a/i", + W.new.url_for(subdomain: "", controller: "c", action: "a", id: "i") + ) + end + + def test_multiple_subdomains_may_be_removed + W.default_url_options[:host] = "mobile.www.api.basecamphq.com" + assert_equal("http://basecamphq.com/c/a/i", + W.new.url_for(subdomain: false, controller: "c", action: "a", id: "i") + ) + end + + def test_subdomain_may_be_accepted_with_numeric_host + add_numeric_host! + assert_equal("http://127.0.0.1/c/a/i", + W.new.url_for(subdomain: "api", controller: "c", action: "a", id: "i") + ) + end + + def test_domain_may_be_changed + add_host! + assert_equal("http://www.37signals.com/c/a/i", + W.new.url_for(domain: "37signals.com", controller: "c", action: "a", id: "i") + ) + end + + def test_tld_length_may_be_changed + add_host! + assert_equal("http://mobile.www.basecamphq.com/c/a/i", + W.new.url_for(subdomain: "mobile", tld_length: 2, controller: "c", action: "a", id: "i") + ) + end + + def test_port + add_host! + assert_equal("http://www.basecamphq.com:3000/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", port: 3000) + ) + end + + def test_default_port + add_host! + add_port! + assert_equal("http://www.basecamphq.com:3000/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i") + ) + end + + def test_protocol + add_host! + assert_equal("https://www.basecamphq.com/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https") + ) + end + + def test_protocol_with_and_without_separators + add_host! + assert_equal("https://www.basecamphq.com/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https") + ) + assert_equal("https://www.basecamphq.com/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https:") + ) + assert_equal("https://www.basecamphq.com/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https://") + ) + end + + def test_without_protocol + add_host! + assert_equal("//www.basecamphq.com/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", protocol: "//") + ) + assert_equal("//www.basecamphq.com/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", protocol: false) + ) + end + + def test_without_protocol_and_with_port + add_host! + add_port! + + assert_equal("//www.basecamphq.com:3000/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", protocol: "//") + ) + assert_equal("//www.basecamphq.com:3000/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", protocol: false) + ) + end + + def test_trailing_slash + add_host! + options = { controller: "foo", trailing_slash: true, action: "bar", id: "33" } + assert_equal("http://www.basecamphq.com/foo/bar/33/", W.new.url_for(options)) + end + + def test_trailing_slash_with_protocol + add_host! + options = { trailing_slash: true, protocol: "https", controller: "foo", action: "bar", id: "33" } + assert_equal("https://www.basecamphq.com/foo/bar/33/", W.new.url_for(options)) + assert_equal "https://www.basecamphq.com/foo/bar/33/?query=string", W.new.url_for(options.merge(query: "string")) + end + + def test_trailing_slash_with_only_path + options = { controller: "foo", trailing_slash: true } + assert_equal "/foo/", W.new.url_for(options.merge(only_path: true)) + options.update(action: "bar", id: "33") + assert_equal "/foo/bar/33/", W.new.url_for(options.merge(only_path: true)) + assert_equal "/foo/bar/33/?query=string", W.new.url_for(options.merge(query: "string", only_path: true)) + end + + def test_trailing_slash_with_anchor + options = { trailing_slash: true, controller: "foo", action: "bar", id: "33", only_path: true, anchor: "chapter7" } + assert_equal "/foo/bar/33/#chapter7", W.new.url_for(options) + assert_equal "/foo/bar/33/?query=string#chapter7", W.new.url_for(options.merge(query: "string")) + end + + def test_trailing_slash_with_params + url = W.new.url_for(trailing_slash: true, only_path: true, controller: "cont", action: "act", p1: "cafe", p2: "link") + params = extract_params(url) + assert_equal({ p1: "cafe" }.to_query, params[0]) + assert_equal({ p2: "link" }.to_query, params[1]) + end + + def test_relative_url_root_is_respected + add_host! + assert_equal("https://www.basecamphq.com/subdir/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https", script_name: "/subdir") + ) + end + + def test_relative_url_root_is_respected_with_environment_variable + # `config.relative_url_root` is set by ENV['RAILS_RELATIVE_URL_ROOT'] + w = Class.new { + config = ActionDispatch::Routing::RouteSet::Config.new "/subdir" + r = ActionDispatch::Routing::RouteSet.new(config) + r.draw { ActiveSupport::Deprecation.silence { get ":controller(/:action(/:id(.:format)))" } } + include r.url_helpers + } + add_host!(w) + assert_equal("https://www.basecamphq.com/subdir/c/a/i", + w.new.url_for(controller: "c", action: "a", id: "i", protocol: "https") + ) + end + + def test_named_routes + with_routing do |set| + set.draw do + get "this/is/verbose", to: "home#index", as: :no_args + get "home/sweet/home/:user", to: "home#index", as: :home + end + + # We need to create a new class in order to install the new named route. + kls = Class.new { include set.url_helpers } + + controller = kls.new + assert_respond_to controller, :home_url + assert_equal "http://www.basecamphq.com/home/sweet/home/again", + controller.send(:home_url, host: "www.basecamphq.com", user: "again") + + assert_equal("/home/sweet/home/alabama", controller.send(:home_path, user: "alabama", host: "unused")) + assert_equal("http://www.basecamphq.com/home/sweet/home/alabama", controller.send(:home_url, user: "alabama", host: "www.basecamphq.com")) + assert_equal("http://www.basecamphq.com/this/is/verbose", controller.send(:no_args_url, host: "www.basecamphq.com")) + end + end + + def test_relative_url_root_is_respected_for_named_routes + with_routing do |set| + set.draw do + get "/home/sweet/home/:user", to: "home#index", as: :home + end + + kls = Class.new { include set.url_helpers } + controller = kls.new + + assert_equal "http://www.basecamphq.com/subdir/home/sweet/home/again", + controller.send(:home_url, host: "www.basecamphq.com", user: "again", script_name: "/subdir") + end + end + + def test_using_nil_script_name_properly_concats_with_original_script_name + add_host! + assert_equal("https://www.basecamphq.com/subdir/c/a/i", + W.new.url_for(controller: "c", action: "a", id: "i", protocol: "https", script_name: nil, original_script_name: "/subdir") + ) + end + + def test_only_path + with_routing do |set| + set.draw do + get "home/sweet/home/:user", to: "home#index", as: :home + + ActiveSupport::Deprecation.silence do + get ":controller/:action/:id" + end + end + + # We need to create a new class in order to install the new named route. + kls = Class.new { include set.url_helpers } + controller = kls.new + assert_respond_to controller, :home_url + assert_equal "/brave/new/world", + controller.url_for(controller: "brave", action: "new", id: "world", only_path: true) + + assert_equal("/home/sweet/home/alabama", controller.home_path(user: "alabama", host: "unused")) + assert_equal("/home/sweet/home/alabama", controller.home_path("alabama")) + end + end + + def test_one_parameter + assert_equal("/c/a?param=val", + W.new.url_for(only_path: true, controller: "c", action: "a", param: "val") + ) + end + + def test_two_parameters + url = W.new.url_for(only_path: true, controller: "c", action: "a", p1: "X1", p2: "Y2") + params = extract_params(url) + assert_equal({ p1: "X1" }.to_query, params[0]) + assert_equal({ p2: "Y2" }.to_query, params[1]) + end + + def test_hash_parameter + url = W.new.url_for(only_path: true, controller: "c", action: "a", query: { name: "Bob", category: "prof" }) + params = extract_params(url) + assert_equal({ "query[category]" => "prof" }.to_query, params[0]) + assert_equal({ "query[name]" => "Bob" }.to_query, params[1]) + end + + def test_array_parameter + url = W.new.url_for(only_path: true, controller: "c", action: "a", query: ["Bob", "prof"]) + params = extract_params(url) + assert_equal({ "query[]" => "Bob" }.to_query, params[0]) + assert_equal({ "query[]" => "prof" }.to_query, params[1]) + end + + def test_hash_recursive_parameters + url = W.new.url_for(only_path: true, controller: "c", action: "a", query: { person: { name: "Bob", position: "prof" }, hobby: "piercing" }) + params = extract_params(url) + assert_equal({ "query[hobby]" => "piercing" }.to_query, params[0]) + assert_equal({ "query[person][name]" => "Bob" }.to_query, params[1]) + assert_equal({ "query[person][position]" => "prof" }.to_query, params[2]) + end + + def test_hash_recursive_and_array_parameters + url = W.new.url_for(only_path: true, controller: "c", action: "a", id: 101, query: { person: { name: "Bob", position: ["prof", "art director"] }, hobby: "piercing" }) + assert_match(%r(^/c/a/101), url) + params = extract_params(url) + assert_equal({ "query[hobby]" => "piercing" }.to_query, params[0]) + assert_equal({ "query[person][name]" => "Bob" }.to_query, params[1]) + assert_equal({ "query[person][position][]" => "art director" }.to_query, params[2]) + assert_equal({ "query[person][position][]" => "prof" }.to_query, params[3]) + end + + def test_url_action_controller_parameters + add_host! + assert_raise(ActionController::UnfilteredParameters) do + W.new.url_for(ActionController::Parameters.new(controller: "c", action: "a", protocol: "javascript", f: "%0Aeval(name)")) + end + end + + def test_path_generation_for_symbol_parameter_keys + assert_generates("/image", controller: :image) + end + + def test_named_routes_with_nil_keys + with_routing do |set| + set.draw do + get "posts.:format", to: "posts#index", as: :posts + get "/", to: "posts#index", as: :main + end + + # We need to create a new class in order to install the new named route. + kls = Class.new { include set.url_helpers } + kls.default_url_options[:host] = "www.basecamphq.com" + + controller = kls.new + params = { action: :index, controller: :posts, format: :xml } + assert_equal("http://www.basecamphq.com/posts.xml", controller.send(:url_for, params)) + params[:format] = nil + assert_equal("http://www.basecamphq.com/", controller.send(:url_for, params)) + end + end + + def test_multiple_includes_maintain_distinct_options + first_class = Class.new { include ActionController::UrlFor } + second_class = Class.new { include ActionController::UrlFor } + + first_host, second_host = "firsthost.com", "secondhost.com" + + first_class.default_url_options[:host] = first_host + second_class.default_url_options[:host] = second_host + + assert_equal first_host, first_class.default_url_options[:host] + assert_equal second_host, second_class.default_url_options[:host] + end + + def test_with_stringified_keys + assert_equal("/c", W.new.url_for("controller" => "c", "only_path" => true)) + assert_equal("/c/a", W.new.url_for("controller" => "c", "action" => "a", "only_path" => true)) + end + + def test_with_hash_with_indifferent_access + W.default_url_options[:controller] = "d" + W.default_url_options[:only_path] = false + assert_equal("/c", W.new.url_for(ActiveSupport::HashWithIndifferentAccess.new("controller" => "c", "only_path" => true))) + + W.default_url_options[:action] = "b" + assert_equal("/c/a", W.new.url_for(ActiveSupport::HashWithIndifferentAccess.new("controller" => "c", "action" => "a", "only_path" => true))) + end + + def test_url_params_with_nil_to_param_are_not_in_url + assert_equal("/c/a", W.new.url_for(only_path: true, controller: "c", action: "a", id: Struct.new(:to_param).new(nil))) + end + + def test_false_url_params_are_included_in_query + assert_equal("/c/a?show=false", W.new.url_for(only_path: true, controller: "c", action: "a", show: false)) + end + + def test_url_generation_with_array_and_hash + with_routing do |set| + set.draw do + namespace :admin do + resources :posts + end + end + + kls = Class.new { include set.url_helpers } + kls.default_url_options[:host] = "www.basecamphq.com" + + controller = kls.new + assert_equal("http://www.basecamphq.com/admin/posts/new?param=value", + controller.send(:url_for, [:new, :admin, :post, { param: "value" }]) + ) + end + end + + def test_url_for_with_array_is_unmodified + with_routing do |set| + set.draw do + namespace :admin do + resources :posts + end + end + + kls = Class.new { include set.url_helpers } + kls.default_url_options[:host] = "www.basecamphq.com" + + original_components = [:new, :admin, :post, { param: "value" }] + components = original_components.dup + + kls.new.url_for(components) + + assert_equal(original_components, components) + end + end + + def test_default_params_first_empty + with_routing do |set| + set.draw do + get "(:param1)/test(/:param2)" => "index#index", + defaults: { + param1: 1, + param2: 2 + }, + constraints: { + param1: /\d*/, + param2: /\d+/ + } + end + + kls = Class.new { include set.url_helpers } + kls.default_url_options[:host] = "www.basecamphq.com" + + assert_equal "http://www.basecamphq.com/test", kls.new.url_for(controller: "index", param1: "1") + end + end + + private + def extract_params(url) + url.split("?", 2).last.split("&").sort + end + end + end +end diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb new file mode 100644 index 0000000000..ca83b850d5 --- /dev/null +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "controller/fake_controllers" + +class UrlRewriterTests < ActionController::TestCase + class Rewriter + def initialize(request) + @options = { + host: request.host_with_port, + protocol: request.protocol + } + end + + def rewrite(routes, options) + routes.url_for(@options.merge(options)) + end + end + + def setup + @params = {} + @rewriter = Rewriter.new(@request) + @routes = ActionDispatch::Routing::RouteSet.new.tap do |r| + r.draw do + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))" + end + end + end + end + + def test_port + assert_equal("http://test.host:1271/c/a/i", + @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", port: 1271) + ) + end + + def test_protocol_with_and_without_separator + assert_equal("https://test.host/c/a/i", + @rewriter.rewrite(@routes, protocol: "https", controller: "c", action: "a", id: "i") + ) + + assert_equal("https://test.host/c/a/i", + @rewriter.rewrite(@routes, protocol: "https://", controller: "c", action: "a", id: "i") + ) + end + + def test_user_name_and_password + assert_equal( + "http://david:secret@test.host/c/a/i", + @rewriter.rewrite(@routes, user: "david", password: "secret", controller: "c", action: "a", id: "i") + ) + end + + def test_user_name_and_password_with_escape_codes + assert_equal( + "http://openid.aol.com%2Fnextangler:one+two%3F@test.host/c/a/i", + @rewriter.rewrite(@routes, user: "openid.aol.com/nextangler", password: "one two?", controller: "c", action: "a", id: "i") + ) + end + + def test_anchor + assert_equal( + "http://test.host/c/a/i#anchor", + @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", anchor: "anchor") + ) + end + + def test_anchor_should_call_to_param + assert_equal( + "http://test.host/c/a/i#anchor", + @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", anchor: Struct.new(:to_param).new("anchor")) + ) + end + + def test_anchor_should_be_uri_escaped + assert_equal( + "http://test.host/c/a/i#anc/hor", + @rewriter.rewrite(@routes, controller: "c", action: "a", id: "i", anchor: Struct.new(:to_param).new("anc/hor")) + ) + end + + def test_trailing_slash + options = { controller: "foo", action: "bar", id: "3", only_path: true } + assert_equal "/foo/bar/3", @rewriter.rewrite(@routes, options) + assert_equal "/foo/bar/3?query=string", @rewriter.rewrite(@routes, options.merge(query: "string")) + options.update(trailing_slash: true) + assert_equal "/foo/bar/3/", @rewriter.rewrite(@routes, options) + options.update(query: "string") + assert_equal "/foo/bar/3/?query=string", @rewriter.rewrite(@routes, options) + end +end diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb new file mode 100644 index 0000000000..4a10637b54 --- /dev/null +++ b/actionpack/test/controller/webservice_test.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/json/decoding" + +class WebServiceTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + def assign_parameters + if params[:full] + render plain: dump_params_keys + else + render plain: (params.keys - ["controller", "action"]).sort.join(", ") + end + end + + def dump_params_keys(hash = params) + hash.keys.sort.inject("") do |s, k| + value = hash[k] + + if value.is_a?(Hash) || value.is_a?(ActionController::Parameters) + value = "(#{dump_params_keys(value)})" + else + value = "" + end + + s += ", " unless s.empty? + s += "#{k}#{value}" + end + end + end + + def setup + @controller = TestController.new + @integration_session = nil + end + + def test_check_parameters + with_test_route_set do + get "/" + assert_equal "", @controller.response.body + end + end + + def test_post_json + with_test_route_set do + post "/", + params: '{"entry":{"summary":"content..."}}', + headers: { "CONTENT_TYPE" => "application/json" } + + assert_equal "entry", @controller.response.body + assert @controller.params.has_key?(:entry) + assert_equal "content...", @controller.params["entry"]["summary"] + end + end + + def test_put_json + with_test_route_set do + put "/", + params: '{"entry":{"summary":"content..."}}', + headers: { "CONTENT_TYPE" => "application/json" } + + assert_equal "entry", @controller.response.body + assert @controller.params.has_key?(:entry) + assert_equal "content...", @controller.params["entry"]["summary"] + end + end + + def test_register_and_use_json_simple + with_test_route_set do + with_params_parsers Mime[:json] => Proc.new { |data| ActiveSupport::JSON.decode(data)["request"].with_indifferent_access } do + post "/", + params: '{"request":{"summary":"content...","title":"JSON"}}', + headers: { "CONTENT_TYPE" => "application/json" } + + assert_equal "summary, title", @controller.response.body + assert @controller.params.has_key?(:summary) + assert @controller.params.has_key?(:title) + assert_equal "content...", @controller.params["summary"] + assert_equal "JSON", @controller.params["title"] + end + end + end + + def test_use_json_with_empty_request + with_test_route_set do + assert_nothing_raised { post "/", headers: { "CONTENT_TYPE" => "application/json" } } + assert_equal "", @controller.response.body + end + end + + def test_dasherized_keys_as_json + with_test_route_set do + post "/?full=1", + params: '{"first-key":{"sub-key":"..."}}', + headers: { "CONTENT_TYPE" => "application/json" } + assert_equal "action, controller, first-key(sub-key), full", @controller.response.body + assert_equal "...", @controller.params["first-key"]["sub-key"] + end + end + + def test_parsing_json_doesnot_rescue_exception + req = Class.new(ActionDispatch::Request) do + def params_parsers + { json: Proc.new { |data| raise Interrupt } } + end + + def content_length; get_header("rack.input").length; end + end.new("rack.input" => StringIO.new('{"title":"JSON"}}'), "CONTENT_TYPE" => "application/json") + + assert_raises(Interrupt) do + req.request_parameters + end + end + + private + def with_params_parsers(parsers = {}) + old_session = @integration_session + original_parsers = ActionDispatch::Request.parameter_parsers + ActionDispatch::Request.parameter_parsers = original_parsers.merge parsers + reset! + yield + ensure + ActionDispatch::Request.parameter_parsers = original_parsers + @integration_session = old_session + end + + def with_test_route_set + with_routing do |set| + set.draw do + match "/", to: "web_service_test/test#assign_parameters", via: :all + end + yield + end + end +end diff --git a/actionpack/test/dispatch/callbacks_test.rb b/actionpack/test/dispatch/callbacks_test.rb new file mode 100644 index 0000000000..fc80191c02 --- /dev/null +++ b/actionpack/test/dispatch/callbacks_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class DispatcherTest < ActiveSupport::TestCase + class Foo + cattr_accessor :a, :b + end + + class DummyApp + def call(env) + [200, {}, "response"] + end + end + + def setup + Foo.a, Foo.b = 0, 0 + ActionDispatch::Callbacks.reset_callbacks(:call) + end + + def test_before_and_after_callbacks + ActionDispatch::Callbacks.before { |*args| Foo.a += 1; Foo.b += 1 } + ActionDispatch::Callbacks.after { |*args| Foo.a += 1; Foo.b += 1 } + + dispatch + assert_equal 2, Foo.a + assert_equal 2, Foo.b + + dispatch + assert_equal 4, Foo.a + assert_equal 4, Foo.b + + dispatch do + raise "error" + end rescue nil + assert_equal 6, Foo.a + assert_equal 6, Foo.b + end + + private + + def dispatch(&block) + ActionDispatch::Callbacks.new(block || DummyApp.new).call( + "rack.input" => StringIO.new("") + ) + end +end diff --git a/actionpack/test/dispatch/content_disposition_test.rb b/actionpack/test/dispatch/content_disposition_test.rb new file mode 100644 index 0000000000..3f5959da6e --- /dev/null +++ b/actionpack/test/dispatch/content_disposition_test.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + class ContentDispositionTest < ActiveSupport::TestCase + test "encoding a Latin filename" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: "racecar.jpg") + + assert_equal %(filename="racecar.jpg"), disposition.ascii_filename + assert_equal "filename*=UTF-8''racecar.jpg", disposition.utf8_filename + assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s + end + + test "encoding a Latin filename with accented characters" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: "råcëçâr.jpg") + + assert_equal %(filename="racecar.jpg"), disposition.ascii_filename + assert_equal "filename*=UTF-8''r%C3%A5c%C3%AB%C3%A7%C3%A2r.jpg", disposition.utf8_filename + assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s + end + + test "encoding a non-Latin filename" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: "автомобиль.jpg") + + assert_equal %(filename="%3F%3F%3F%3F%3F%3F%3F%3F%3F%3F.jpg"), disposition.ascii_filename + assert_equal "filename*=UTF-8''%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%BE%D0%B1%D0%B8%D0%BB%D1%8C.jpg", disposition.utf8_filename + assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s + end + + test "without filename" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: nil) + + assert_equal "inline", disposition.to_s + end + end +end diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb new file mode 100644 index 0000000000..c8c885f35c --- /dev/null +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -0,0 +1,546 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ContentSecurityPolicyTest < ActiveSupport::TestCase + def setup + @policy = ActionDispatch::ContentSecurityPolicy.new + end + + def test_build + assert_equal "", @policy.build + + @policy.script_src :self + assert_equal "script-src 'self'", @policy.build + end + + def test_dup + @policy.img_src :self + @policy.block_all_mixed_content + @policy.upgrade_insecure_requests + @policy.sandbox + copied = @policy.dup + assert_equal copied.build, @policy.build + end + + def test_mappings + @policy.script_src :data + assert_equal "script-src data:", @policy.build + + @policy.script_src :mediastream + assert_equal "script-src mediastream:", @policy.build + + @policy.script_src :blob + assert_equal "script-src blob:", @policy.build + + @policy.script_src :filesystem + assert_equal "script-src filesystem:", @policy.build + + @policy.script_src :self + assert_equal "script-src 'self'", @policy.build + + @policy.script_src :unsafe_inline + assert_equal "script-src 'unsafe-inline'", @policy.build + + @policy.script_src :unsafe_eval + assert_equal "script-src 'unsafe-eval'", @policy.build + + @policy.script_src :none + assert_equal "script-src 'none'", @policy.build + + @policy.script_src :strict_dynamic + assert_equal "script-src 'strict-dynamic'", @policy.build + + @policy.script_src :ws + assert_equal "script-src ws:", @policy.build + + @policy.script_src :wss + assert_equal "script-src wss:", @policy.build + + @policy.script_src :none, :report_sample + assert_equal "script-src 'none' 'report-sample'", @policy.build + end + + def test_fetch_directives + @policy.child_src :self + assert_match %r{child-src 'self'}, @policy.build + + @policy.child_src false + assert_no_match %r{child-src}, @policy.build + + @policy.connect_src :self + assert_match %r{connect-src 'self'}, @policy.build + + @policy.connect_src false + assert_no_match %r{connect-src}, @policy.build + + @policy.default_src :self + assert_match %r{default-src 'self'}, @policy.build + + @policy.default_src false + assert_no_match %r{default-src}, @policy.build + + @policy.font_src :self + assert_match %r{font-src 'self'}, @policy.build + + @policy.font_src false + assert_no_match %r{font-src}, @policy.build + + @policy.frame_src :self + assert_match %r{frame-src 'self'}, @policy.build + + @policy.frame_src false + assert_no_match %r{frame-src}, @policy.build + + @policy.img_src :self + assert_match %r{img-src 'self'}, @policy.build + + @policy.img_src false + assert_no_match %r{img-src}, @policy.build + + @policy.manifest_src :self + assert_match %r{manifest-src 'self'}, @policy.build + + @policy.manifest_src false + assert_no_match %r{manifest-src}, @policy.build + + @policy.media_src :self + assert_match %r{media-src 'self'}, @policy.build + + @policy.media_src false + assert_no_match %r{media-src}, @policy.build + + @policy.object_src :self + assert_match %r{object-src 'self'}, @policy.build + + @policy.object_src false + assert_no_match %r{object-src}, @policy.build + + @policy.prefetch_src :self + assert_match %r{prefetch-src 'self'}, @policy.build + + @policy.prefetch_src false + assert_no_match %r{prefetch-src}, @policy.build + + @policy.script_src :self + assert_match %r{script-src 'self'}, @policy.build + + @policy.script_src false + assert_no_match %r{script-src}, @policy.build + + @policy.style_src :self + assert_match %r{style-src 'self'}, @policy.build + + @policy.style_src false + assert_no_match %r{style-src}, @policy.build + + @policy.worker_src :self + assert_match %r{worker-src 'self'}, @policy.build + + @policy.worker_src false + assert_no_match %r{worker-src}, @policy.build + end + + def test_document_directives + @policy.base_uri "https://example.com" + assert_match %r{base-uri https://example\.com}, @policy.build + + @policy.plugin_types "application/x-shockwave-flash" + assert_match %r{plugin-types application/x-shockwave-flash}, @policy.build + + @policy.sandbox + assert_match %r{sandbox}, @policy.build + + @policy.sandbox "allow-scripts", "allow-modals" + assert_match %r{sandbox allow-scripts allow-modals}, @policy.build + + @policy.sandbox false + assert_no_match %r{sandbox}, @policy.build + end + + def test_navigation_directives + @policy.form_action :self + assert_match %r{form-action 'self'}, @policy.build + + @policy.frame_ancestors :self + assert_match %r{frame-ancestors 'self'}, @policy.build + end + + def test_reporting_directives + @policy.report_uri "/violations" + assert_match %r{report-uri /violations}, @policy.build + end + + def test_other_directives + @policy.block_all_mixed_content + assert_match %r{block-all-mixed-content}, @policy.build + + @policy.block_all_mixed_content false + assert_no_match %r{block-all-mixed-content}, @policy.build + + @policy.require_sri_for :script, :style + assert_match %r{require-sri-for script style}, @policy.build + + @policy.require_sri_for "script", "style" + assert_match %r{require-sri-for script style}, @policy.build + + @policy.require_sri_for + assert_no_match %r{require-sri-for}, @policy.build + + @policy.upgrade_insecure_requests + assert_match %r{upgrade-insecure-requests}, @policy.build + + @policy.upgrade_insecure_requests false + assert_no_match %r{upgrade-insecure-requests}, @policy.build + end + + def test_multiple_sources + @policy.script_src :self, :https + assert_equal "script-src 'self' https:", @policy.build + end + + def test_multiple_directives + @policy.script_src :self, :https + @policy.style_src :self, :https + assert_equal "script-src 'self' https:; style-src 'self' https:", @policy.build + end + + def test_dynamic_directives + request = ActionDispatch::Request.new("HTTP_HOST" => "www.example.com") + controller = Struct.new(:request).new(request) + + @policy.script_src -> { request.host } + assert_equal "script-src www.example.com", @policy.build(controller) + end + + def test_mixed_static_and_dynamic_directives + @policy.script_src :self, -> { "foo.com" }, "bar.com" + request = ActionDispatch::Request.new({}) + controller = Struct.new(:request).new(request) + assert_equal "script-src 'self' foo.com bar.com", @policy.build(controller) + end + + def test_invalid_directive_source + exception = assert_raises(ArgumentError) do + @policy.script_src [:self] + end + + assert_equal "Invalid content security policy source: [:self]", exception.message + end + + def test_missing_context_for_dynamic_source + @policy.script_src -> { request.host } + + exception = assert_raises(RuntimeError) do + @policy.build + end + + assert_match %r{\AMissing context for the dynamic content security policy source:}, exception.message + end + + def test_raises_runtime_error_when_unexpected_source + @policy.plugin_types [:flash] + + exception = assert_raises(RuntimeError) do + @policy.build + end + + assert_match %r{\AUnexpected content security policy source:}, exception.message + end +end + +class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest + class PolicyController < ActionController::Base + def index + head :ok + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "default_content_security_policy_integration_test" do + get "/", to: "policy#index" + get "/redirect", to: redirect("/") + end + end + + POLICY = ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src -> { :self } + p.script_src -> { :https } + end + + class PolicyConfigMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.content_security_policy"] = POLICY + env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" } + env["action_dispatch.content_security_policy_report_only"] = false + env["action_dispatch.show_exceptions"] = false + + @app.call(env) + end + end + + APP = build_app(ROUTES) do |middleware| + middleware.use PolicyConfigMiddleware + middleware.use ActionDispatch::ContentSecurityPolicy::Middleware + end + + def app + APP + end + + def test_adds_nonce_to_script_src_content_security_policy_only_once + get "/" + get "/" + assert_response :success + assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='" + end + + def test_redirect_works_with_dynamic_sources + get "/redirect" + assert_response :redirect + assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='" + end + + private + + def assert_policy(expected, report_only: false) + if report_only + expected_header = "Content-Security-Policy-Report-Only" + unexpected_header = "Content-Security-Policy" + else + expected_header = "Content-Security-Policy" + unexpected_header = "Content-Security-Policy-Report-Only" + end + + assert_nil response.headers[unexpected_header] + assert_equal expected, response.headers[expected_header] + end +end + +class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest + class PolicyController < ActionController::Base + content_security_policy only: :inline do |p| + p.default_src "https://example.com" + end + + content_security_policy only: :conditional, if: :condition? do |p| + p.default_src "https://true.example.com" + end + + content_security_policy only: :conditional, unless: :condition? do |p| + p.default_src "https://false.example.com" + end + + content_security_policy only: :report_only do |p| + p.report_uri "/violations" + end + + content_security_policy only: :script_src do |p| + p.default_src false + p.script_src :self + end + + content_security_policy only: :style_src do |p| + p.default_src false + p.style_src :self + end + + content_security_policy(false, only: :no_policy) + + content_security_policy_report_only only: :report_only + + def index + head :ok + end + + def inline + head :ok + end + + def conditional + head :ok + end + + def report_only + head :ok + end + + def script_src + head :ok + end + + def style_src + head :ok + end + + def no_policy + head :ok + end + + private + def condition? + params[:condition] == "true" + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "content_security_policy_integration_test" do + get "/", to: "policy#index" + get "/inline", to: "policy#inline" + get "/conditional", to: "policy#conditional" + get "/report-only", to: "policy#report_only" + get "/script-src", to: "policy#script_src" + get "/style-src", to: "policy#style_src" + get "/no-policy", to: "policy#no_policy" + end + end + + POLICY = ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src :self + end + + class PolicyConfigMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.content_security_policy"] = POLICY + env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" } + env["action_dispatch.content_security_policy_report_only"] = false + env["action_dispatch.show_exceptions"] = false + + @app.call(env) + end + end + + APP = build_app(ROUTES) do |middleware| + middleware.use PolicyConfigMiddleware + middleware.use ActionDispatch::ContentSecurityPolicy::Middleware + end + + def app + APP + end + + def test_generates_content_security_policy_header + get "/" + assert_policy "default-src 'self'" + end + + def test_generates_inline_content_security_policy + get "/inline" + assert_policy "default-src https://example.com" + end + + def test_generates_conditional_content_security_policy + get "/conditional", params: { condition: "true" } + assert_policy "default-src https://true.example.com" + + get "/conditional", params: { condition: "false" } + assert_policy "default-src https://false.example.com" + end + + def test_generates_report_only_content_security_policy + get "/report-only" + assert_policy "default-src 'self'; report-uri /violations", report_only: true + end + + def test_adds_nonce_to_script_src_content_security_policy + get "/script-src" + assert_policy "script-src 'self' 'nonce-iyhD0Yc0W+c='" + end + + def test_adds_nonce_to_style_src_content_security_policy + get "/style-src" + assert_policy "style-src 'self' 'nonce-iyhD0Yc0W+c='" + end + + def test_generates_no_content_security_policy + get "/no-policy" + + assert_nil response.headers["Content-Security-Policy"] + assert_nil response.headers["Content-Security-Policy-Report-Only"] + end + + private + + def assert_policy(expected, report_only: false) + assert_response :success + + if report_only + expected_header = "Content-Security-Policy-Report-Only" + unexpected_header = "Content-Security-Policy" + else + expected_header = "Content-Security-Policy" + unexpected_header = "Content-Security-Policy-Report-Only" + end + + assert_nil response.headers[unexpected_header] + assert_equal expected, response.headers[expected_header] + end +end + +class DisabledContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest + class PolicyController < ActionController::Base + content_security_policy only: :inline do |p| + p.default_src "https://example.com" + end + + def index + head :ok + end + + def inline + head :ok + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "disabled_content_security_policy_integration_test" do + get "/", to: "policy#index" + get "/inline", to: "policy#inline" + end + end + + class PolicyConfigMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.content_security_policy"] = nil + env["action_dispatch.content_security_policy_nonce_generator"] = nil + env["action_dispatch.content_security_policy_report_only"] = false + env["action_dispatch.show_exceptions"] = false + + @app.call(env) + end + end + + APP = build_app(ROUTES) do |middleware| + middleware.use PolicyConfigMiddleware + middleware.use ActionDispatch::ContentSecurityPolicy::Middleware + end + + def app + APP + end + + def test_generates_no_content_security_policy_by_default + get "/" + assert_nil response.headers["Content-Security-Policy"] + end + + def test_generates_content_security_policy_header_when_globally_disabled + get "/inline" + assert_equal "default-src https://example.com", response.headers["Content-Security-Policy"] + end +end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb new file mode 100644 index 0000000000..6637c2cae9 --- /dev/null +++ b/actionpack/test/dispatch/cookies_test.rb @@ -0,0 +1,1483 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "openssl" +require "active_support/key_generator" +require "active_support/messages/rotation_configuration" + +class CookieJarTest < ActiveSupport::TestCase + attr_reader :request + + def setup + @request = ActionDispatch::Request.empty + end + + def test_fetch + x = Object.new + assert_not request.cookie_jar.key?("zzzzzz") + assert_equal x, request.cookie_jar.fetch("zzzzzz", x) + assert_not request.cookie_jar.key?("zzzzzz") + end + + def test_fetch_exists + x = Object.new + request.cookie_jar["foo"] = "bar" + assert_equal "bar", request.cookie_jar.fetch("foo", x) + end + + def test_fetch_block + x = Object.new + assert_not request.cookie_jar.key?("zzzzzz") + assert_equal x, request.cookie_jar.fetch("zzzzzz") { x } + end + + def test_key_is_to_s + request.cookie_jar["foo"] = "bar" + assert_equal "bar", request.cookie_jar.fetch(:foo) + end + + def test_to_hash + request.cookie_jar["foo"] = "bar" + assert_equal({ "foo" => "bar" }, request.cookie_jar.to_hash) + assert_equal({ "foo" => "bar" }, request.cookie_jar.to_h) + end + + def test_fetch_type_error + assert_raises(KeyError) do + request.cookie_jar.fetch(:omglolwut) + end + end + + def test_each + request.cookie_jar["foo"] = :bar + list = [] + request.cookie_jar.each do |k, v| + list << [k, v] + end + + assert_equal [["foo", :bar]], list + end + + def test_enumerable + request.cookie_jar["foo"] = :bar + actual = request.cookie_jar.map { |k, v| [k.to_s, v.to_s] } + assert_equal [["foo", "bar"]], actual + end + + def test_key_methods + assert_not request.cookie_jar.key?(:foo) + assert_not request.cookie_jar.has_key?("foo") + + request.cookie_jar[:foo] = :bar + assert request.cookie_jar.key?(:foo) + assert request.cookie_jar.has_key?("foo") + end + + def test_write_doesnt_set_a_nil_header + headers = {} + request.cookie_jar.write(headers) + assert_not_includes headers, "Set-Cookie" + end +end + +class CookiesTest < ActionController::TestCase + class CustomSerializer + def self.load(value) + value.to_s + " and loaded" + end + + def self.dump(value) + value.to_s + " was dumped" + end + end + + class TestController < ActionController::Base + def authenticate + cookies["user_name"] = "david" + head :ok + end + + def set_with_with_escapable_characters + cookies["that & guy"] = "foo & bar => baz" + head :ok + end + + def authenticate_for_fourteen_days + cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10, 5) } + head :ok + end + + def authenticate_for_fourteen_days_with_symbols + cookies[:user_name] = { value: "david", expires: Time.utc(2005, 10, 10, 5) } + head :ok + end + + def set_multiple_cookies + cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10, 5) } + cookies["login"] = "XJ-122" + head :ok + end + + def access_frozen_cookies + cookies["will"] = "work" + head :ok + end + + def logout + cookies.delete("user_name") + head :ok + end + + alias delete_cookie logout + + def delete_cookie_with_path + cookies.delete("user_name", path: "/beaten") + head :ok + end + + def authenticate_with_http_only + cookies["user_name"] = { value: "david", httponly: true } + head :ok + end + + def authenticate_with_secure + cookies["user_name"] = { value: "david", secure: true } + head :ok + end + + def set_permanent_cookie + cookies.permanent[:user_name] = "Jamie" + head :ok + end + + def set_signed_cookie + cookies.signed[:user_id] = 45 + head :ok + end + + def get_signed_cookie + cookies.signed[:user_id] + head :ok + end + + def set_encrypted_cookie + cookies.encrypted[:foo] = "bar" + head :ok + end + + class JSONWrapper + def initialize(obj) + @obj = obj + end + + def as_json(options = nil) + "wrapped: #{@obj.as_json(options)}" + end + end + + def set_wrapped_signed_cookie + cookies.signed[:user_id] = JSONWrapper.new(45) + head :ok + end + + def set_wrapped_encrypted_cookie + cookies.encrypted[:foo] = JSONWrapper.new("bar") + head :ok + end + + def get_encrypted_cookie + cookies.encrypted[:foo] + head :ok + end + + def set_invalid_encrypted_cookie + cookies[:invalid_cookie] = "invalid--9170e00a57cfc27083363b5c75b835e477bd90cf" + head :ok + end + + def raise_data_overflow + cookies.signed[:foo] = "bye!" * 1024 + head :ok + end + + def tampered_cookies + cookies[:tampered] = "BAh7BjoIZm9vIghiYXI%3D--123456780" + cookies.signed[:tampered] + head :ok + end + + def set_permanent_signed_cookie + cookies.permanent.signed[:remember_me] = 100 + head :ok + end + + def delete_and_set_cookie + cookies.delete :user_name + cookies[:user_name] = { value: "david", expires: Time.utc(2005, 10, 10, 5) } + head :ok + end + + def set_cookie_with_domain + cookies[:user_name] = { value: "rizwanreza", domain: :all } + head :ok + end + + def set_cookie_with_domain_all_as_string + cookies[:user_name] = { value: "rizwanreza", domain: "all" } + head :ok + end + + def delete_cookie_with_domain + cookies.delete(:user_name, domain: :all) + head :ok + end + + def delete_cookie_with_domain_all_as_string + cookies.delete(:user_name, domain: "all") + head :ok + end + + def set_cookie_with_domain_and_tld + cookies[:user_name] = { value: "rizwanreza", domain: :all, tld_length: 2 } + head :ok + end + + def delete_cookie_with_domain_and_tld + cookies.delete(:user_name, domain: :all, tld_length: 2) + head :ok + end + + def set_cookie_with_domains + cookies[:user_name] = { value: "rizwanreza", domain: %w(example1.com example2.com .example3.com) } + head :ok + end + + def delete_cookie_with_domains + cookies.delete(:user_name, domain: %w(example1.com example2.com .example3.com)) + head :ok + end + + def symbol_key + cookies[:user_name] = "david" + head :ok + end + + def string_key + cookies["user_name"] = "dhh" + head :ok + end + + def symbol_key_mock + cookies[:user_name] = "david" if cookies[:user_name] == "andrew" + head :ok + end + + def string_key_mock + cookies["user_name"] = "david" if cookies["user_name"] == "andrew" + head :ok + end + + def noop + head :ok + end + + def encrypted_cookie + cookies.encrypted["foo"] + end + + def cookie_expires_in_two_hours + cookies[:user_name] = { value: "assain", expires: 2.hours } + head :ok + end + + def encrypted_discount_and_user_id_cookie + cookies.encrypted[:user_id] = { value: 50, expires: 1.hour } + cookies.encrypted[:discount_percentage] = 10 + + head :ok + end + + def signed_discount_and_user_id_cookie + cookies.signed[:user_id] = { value: 50, expires: 1.hour } + cookies.signed[:discount_percentage] = 10 + + head :ok + end + + def rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on + # cookies.encrypted[:favorite] = { value: "5-2-Stable Chocolate Cookies", expires: 1000.years } + cookies[:favorite] = "KvH5lIHvX5vPQkLIK63r/NuIMwzWky8M0Zwk8SZ6DwUv8+srf36geR4nWq5KmhsZIYXA8NRdCZYIfxMKJsOFlz77Gf+Fq8vBBCWJTp95rx39A28TCUTJEyMhCNJO5eie7Skef76Qt5Jo/SCnIADAhzyGQkGBopKRcA==--qXZZFWGbCy6N8AGy--WswoH+xHrNh9MzSXDpB2fA==" + + head :ok + end + + def rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off + cookies[:favorite] = "Wmg4amgvcVVvWGcwK3c4WjJEbTdRQUgrWXhBdDliUTR0cVNidXpmVTMrc2RjcitwUzVsWWEwZGtuVGtFUjJwNi0tcVhVMTFMOTQ1d0hIVE1FK0pJc05SQT09--8b2a55c375049a50f7a959b9d42b31ef0b2bb594" + + head :ok + end + + def rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on + # cookies.signed[:favorite] = { value: "5-2-Stable Choco Chip Cookie", expires: 1000.years } + cookies[:favorite] = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJaUUxTFRJdFUzUmhZbXhsSUVOb2IyTnZJRU5vYVhBZ1EyOXZhMmxsQmpvR1JWUT0iLCJleHAiOiIzMDE4LTA3LTExVDE2OjExOjI2Ljc1M1oiLCJwdXIiOm51bGx9fQ==--7df5d885b78b70a501d6e82140ae91b24060ac00" + + head :ok + end + + def rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off + cookies[:favorite] = "BAhJIiE1LTItU3RhYmxlIENob2NvIENoaXAgQ29va2llBjoGRVQ=--50bbdbf8d64f5a3ec3e54878f54d4f55b6cb3aff" + + head :ok + end + end + + tests TestController + + SECRET_KEY_BASE = "b3c631c314c0bbca50c1b2843150fe33" + SIGNED_COOKIE_SALT = "signed cookie" + ENCRYPTED_COOKIE_SALT = "encrypted cookie" + ENCRYPTED_SIGNED_COOKIE_SALT = "sigend encrypted cookie" + AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "authenticated encrypted cookie" + + def setup + super + + @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE, iterations: 2) + @request.env["action_dispatch.cookies_rotations"] = ActiveSupport::Messages::RotationConfiguration.new + + @request.env["action_dispatch.secret_key_base"] = SECRET_KEY_BASE + @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true + + @request.env["action_dispatch.signed_cookie_salt"] = SIGNED_COOKIE_SALT + @request.env["action_dispatch.encrypted_cookie_salt"] = ENCRYPTED_COOKIE_SALT + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = ENCRYPTED_SIGNED_COOKIE_SALT + @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = AUTHENTICATED_ENCRYPTED_COOKIE_SALT + + @request.host = "www.nextangle.com" + end + + def test_setting_cookie + get :authenticate + assert_cookie_header "user_name=david; path=/" + assert_equal({ "user_name" => "david" }, @response.cookies) + end + + def test_setting_the_same_value_to_cookie + request.cookies[:user_name] = "david" + get :authenticate + assert_empty response.cookies + end + + def test_setting_the_same_value_to_permanent_cookie + request.cookies[:user_name] = "Jamie" + get :set_permanent_cookie + assert_equal({ "user_name" => "Jamie" }, response.cookies) + end + + def test_setting_with_escapable_characters + get :set_with_with_escapable_characters + assert_cookie_header "that+%26+guy=foo+%26+bar+%3D%3E+baz; path=/" + assert_equal({ "that & guy" => "foo & bar => baz" }, @response.cookies) + end + + def test_setting_cookie_for_fourteen_days + get :authenticate_for_fourteen_days + assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000" + assert_equal({ "user_name" => "david" }, @response.cookies) + end + + def test_setting_cookie_for_fourteen_days_with_symbols + get :authenticate_for_fourteen_days_with_symbols + assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000" + assert_equal({ "user_name" => "david" }, @response.cookies) + end + + def test_setting_cookie_with_http_only + get :authenticate_with_http_only + assert_cookie_header "user_name=david; path=/; HttpOnly" + assert_equal({ "user_name" => "david" }, @response.cookies) + end + + def test_setting_cookie_with_secure + @request.env["HTTPS"] = "on" + get :authenticate_with_secure + assert_cookie_header "user_name=david; path=/; secure" + assert_equal({ "user_name" => "david" }, @response.cookies) + end + + def test_setting_cookie_with_secure_when_always_write_cookie_is_true + old_cookie, @request.cookie_jar.always_write_cookie = @request.cookie_jar.always_write_cookie, true + get :authenticate_with_secure + assert_cookie_header "user_name=david; path=/; secure" + assert_equal({ "user_name" => "david" }, @response.cookies) + ensure + @request.cookie_jar.always_write_cookie = old_cookie + end + + def test_not_setting_cookie_with_secure + get :authenticate_with_secure + assert_not_cookie_header "user_name=david; path=/; secure" + assert_not_equal({ "user_name" => "david" }, @response.cookies) + end + + def test_multiple_cookies + get :set_multiple_cookies + assert_equal 2, @response.cookies.size + assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000\nlogin=XJ-122; path=/" + assert_equal({ "login" => "XJ-122", "user_name" => "david" }, @response.cookies) + end + + def test_setting_test_cookie + assert_nothing_raised { get :access_frozen_cookies } + end + + def test_expiring_cookie + request.cookies[:user_name] = "Joe" + get :logout + assert_cookie_header "user_name=; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000" + assert_equal({ "user_name" => nil }, @response.cookies) + end + + def test_delete_cookie_with_path + request.cookies[:user_name] = "Joe" + get :delete_cookie_with_path + assert_cookie_header "user_name=; path=/beaten; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000" + end + + def test_delete_unexisting_cookie + request.cookies.clear + get :delete_cookie + assert_empty @response.cookies + end + + def test_deleted_cookie_predicate + cookies[:user_name] = "Joe" + cookies.delete("user_name") + assert cookies.deleted?("user_name") + assert_equal false, cookies.deleted?("another") + end + + def test_deleted_cookie_predicate_with_mismatching_options + cookies[:user_name] = "Joe" + cookies.delete("user_name", path: "/path") + assert_equal false, cookies.deleted?("user_name", path: "/different") + end + + def test_cookies_persist_throughout_request + response = get :authenticate + assert_match(/user_name=david/, response.headers["Set-Cookie"]) + end + + def test_set_permanent_cookie + get :set_permanent_cookie + assert_match(/Jamie/, @response.headers["Set-Cookie"]) + assert_match(%r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"]) + end + + def test_read_permanent_cookie + get :set_permanent_cookie + assert_equal "Jamie", @controller.send(:cookies).permanent[:user_name] + end + + def test_signed_cookie_using_default_digest + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA1") + assert_equal verifier.generate(45), cookies[:user_id] + end + + def test_signed_cookie_using_custom_digest + @request.env["action_dispatch.signed_cookie_digest"] = "SHA256" + + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA256") + assert_equal verifier.generate(45), cookies[:user_id] + end + + def test_signed_cookie_rotating_secret_and_digest + secret = "b3c631c314c0bbca50c1b2843150fe33" + + @request.env["action_dispatch.signed_cookie_digest"] = "SHA256" + @request.env["action_dispatch.cookies_rotations"].rotate :signed, secret, digest: "SHA1" + + old_message = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal).generate(45) + @request.headers["Cookie"] = "user_id=#{old_message}" + + get :get_signed_cookie + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256", serializer: Marshal) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_signed_cookie_with_legacy_secret_scheme + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + old_message = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", digest: "SHA1", serializer: Marshal).generate(45) + + @request.headers["Cookie"] = "user_id=#{old_message}" + get :get_signed_cookie + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key("signed cookie") + verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_tampered_with_signed_cookie + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: "SHA1") + message = verifier.generate(45) + + @request.headers["Cookie"] = "user_id=#{Marshal.dump 45}--#{message.split("--").last}" + get :get_signed_cookie + assert_nil @controller.send(:cookies).signed[:user_id] + end + + def test_signed_cookie_using_default_serializer + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + end + + def test_signed_cookie_using_marshal_serializer + @request.env["action_dispatch.cookies_serializer"] = :marshal + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + end + + def test_signed_cookie_using_json_serializer + @request.env["action_dispatch.cookies_serializer"] = :json + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + end + + def test_wrapped_signed_cookie_using_json_serializer + @request.env["action_dispatch.cookies_serializer"] = :json + get :set_wrapped_signed_cookie + cookies = @controller.send :cookies + assert_not_equal "wrapped: 45", cookies[:user_id] + assert_equal "wrapped: 45", cookies.signed[:user_id] + end + + def test_signed_cookie_using_custom_serializer + @request.env["action_dispatch.cookies_serializer"] = CustomSerializer + get :set_signed_cookie + assert_not_equal 45, cookies[:user_id] + assert_equal "45 was dumped and loaded", cookies.signed[:user_id] + end + + def test_signed_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + + marshal_value = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal).generate(45) + @request.headers["Cookie"] = "user_id=#{marshal_value}" + + get :get_signed_cookie + + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_signed_cookie_using_hybrid_serializer_can_read_from_json_dumped_value + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + + json_value = ActiveSupport::MessageVerifier.new(secret, serializer: JSON).generate(45) + @request.headers["Cookie"] = "user_id=#{json_value}" + + get :get_signed_cookie + + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + assert_nil @response.cookies["user_id"] + end + + def test_accessing_nonexistent_signed_cookie_should_not_raise_an_invalid_signature + get :set_signed_cookie + assert_nil @controller.send(:cookies).signed[:non_existent_attribute] + end + + def test_encrypted_cookie_using_default_serializer + get :set_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_nil cookies.signed[:foo] + assert_equal "bar", cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_marshal_serializer + @request.env["action_dispatch.cookies_serializer"] = :marshal + get :set_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_nil cookies.signed[:foo] + assert_equal "bar", cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_json_serializer + @request.env["action_dispatch.cookies_serializer"] = :json + get :set_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_nil cookies.signed[:foo] + assert_equal "bar", cookies.encrypted[:foo] + end + + def test_wrapped_encrypted_cookie_using_json_serializer + @request.env["action_dispatch.cookies_serializer"] = :json + get :set_wrapped_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal "wrapped: bar", cookies[:foo] + assert_nil cookies.signed[:foo] + assert_equal "wrapped: bar", cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_custom_serializer + @request.env["action_dispatch.cookies_serializer"] = CustomSerializer + get :set_encrypted_cookie + assert_not_equal "bar", cookies.encrypted[:foo] + assert_equal "bar was dumped and loaded", cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32) + + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal) + marshal_value = encryptor.encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON) + assert_not_nil @response.cookies["foo"] + assert_equal "bar", json_encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32) + + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON) + json_value = encryptor.encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape json_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + assert_nil @response.cookies["foo"] + end + + def test_accessing_nonexistent_encrypted_cookie_should_not_raise_invalid_message + get :set_encrypted_cookie + assert_nil @controller.send(:cookies).encrypted[:non_existent_attribute] + end + + def test_setting_invalid_encrypted_cookie_should_return_nil_when_accessing_it + get :set_invalid_encrypted_cookie + assert_nil @controller.send(:cookies).encrypted[:invalid_cookie] + end + + def test_permanent_signed_cookie + get :set_permanent_signed_cookie + assert_match(%r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"]) + assert_equal 100, @controller.send(:cookies).signed[:remember_me] + end + + def test_delete_and_set_cookie + request.cookies[:user_name] = "Joe" + get :delete_and_set_cookie + assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000" + assert_equal({ "user_name" => "david" }, @response.cookies) + end + + def test_raise_data_overflow + assert_raise(ActionDispatch::Cookies::CookieOverflow) do + get :raise_data_overflow + end + end + + def test_tampered_cookies + assert_nothing_raised do + get :tampered_cookies + assert_response :success + end + end + + def test_cookie_jar_mutated_by_request_persists_on_future_requests + get :authenticate + cookie_jar = @request.cookie_jar + cookie_jar.signed[:user_id] = 123 + assert_equal ["user_name", "user_id"], @request.cookie_jar.instance_variable_get(:@cookies).keys + get :get_signed_cookie + assert_equal ["user_name", "user_id"], @request.cookie_jar.instance_variable_get(:@cookies).keys + end + + def test_raises_argument_error_if_missing_secret + assert_raise(ArgumentError, nil.inspect) { + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(nil) + get :set_signed_cookie + } + + assert_raise(ArgumentError, "".inspect) { + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("") + get :set_signed_cookie + } + end + + def test_raises_argument_error_if_secret_is_probably_insecure + assert_raise(ArgumentError, "password".inspect) { + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("password") + get :set_signed_cookie + } + + assert_raise(ArgumentError, "secret".inspect) { + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("secret") + get :set_signed_cookie + } + + assert_raise(ArgumentError, "12345678901234567890123456789".inspect) { + @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("12345678901234567890123456789") + get :set_signed_cookie + } + end + + def test_legacy_signed_cookie_is_read_and_transparently_upgraded_by_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45) + + @request.headers["Cookie"] = "user_id=#{legacy_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar") + + @request.headers["Cookie"] = "foo=#{legacy_value}" + get :get_encrypted_cookie + + assert_equal "bar", @controller.send(:cookies).encrypted[:foo] + + secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32) + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal) + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :json + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45) + + @request.headers["Cookie"] = "user_id=#{legacy_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :json + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") + + @request.headers["Cookie"] = "foo=#{legacy_value}" + get :get_encrypted_cookie + + assert_equal "bar", @controller.send(:cookies).encrypted[:foo] + + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :hybrid + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45) + + @request.headers["Cookie"] = "user_id=#{legacy_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :hybrid + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") + + @request.headers["Cookie"] = "foo=#{legacy_value}" + get :get_encrypted_cookie + + assert_equal "bar", @controller.send(:cookies).encrypted[:foo] + + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON) + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_marshal_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :hybrid + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45) + + @request.headers["Cookie"] = "user_id=#{legacy_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.cookies_serializer"] = :hybrid + + @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar") + + @request.headers["Cookie"] = "foo=#{legacy_value}" + get :get_encrypted_cookie + + assert_equal "bar", @controller.send(:cookies).encrypted[:foo] + + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON) + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + @request.headers["Cookie"] = "user_id=45" + get :get_signed_cookie + + assert_nil @controller.send(:cookies).signed[:user_id] + assert_nil @response.cookies["user_id"] + end + + def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + + @request.headers["Cookie"] = "foo=baz" + get :get_encrypted_cookie + + assert_nil @controller.send(:cookies).encrypted[:foo] + assert_nil @response.cookies["foo"] + end + + def test_use_authenticated_cookie_encryption_uses_legacy_hmac_aes_cbc_encryption_when_not_enabled + @request.env["action_dispatch.use_authenticated_cookie_encryption"] = nil + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", digest: "SHA1", serializer: Marshal) + + get :set_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_rotating_signed_cookies_digest + @request.env["action_dispatch.signed_cookie_digest"] = "SHA256" + @request.env["action_dispatch.cookies_rotations"].rotate :signed, digest: "SHA1" + + key_generator = @request.env["action_dispatch.key_generator"] + + old_secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + old_value = ActiveSupport::MessageVerifier.new(old_secret).generate(45) + + @request.headers["Cookie"] = "user_id=#{old_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256") + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: Marshal).encrypt_and_sign("bar") + + @request.headers["Cookie"] = "foo=#{marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + aead_secret = key_generator.generate_key(aead_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")) + aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: "aes-256-gcm", serializer: Marshal) + + assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_hmac_aes_cbc_encrypted_json_cookie_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.cookies_serializer"] = :json + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: JSON).encrypt_and_sign("bar") + + @request.headers["Cookie"] = "foo=#{marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")] + aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: "aes-256-gcm", serializer: JSON) + + assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_hmac_aes_cbc_encrypted_cookie_using_64_byte_key_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33" + + # Cookie generated with 64 bytes secret + message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*") + @request.headers["Cookie"] = "foo=#{message}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")) + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal) + + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_encrypted_cookie_rotating_secret + secret = "b3c631c314c0bbca50c1b2843150fe33" + + @request.env["action_dispatch.encrypted_cookie_cipher"] = "aes-256-gcm" + @request.env["action_dispatch.cookies_rotations"].rotate :encrypted, secret + + key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-gcm") + + old_message = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign(45) + + @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape old_message}" + + get :get_encrypted_cookie + assert_equal 45, @controller.send(:cookies).encrypted[:foo] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], key_len) + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal) + assert_equal 45, encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_cookie_with_all_domain_option + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.com; path=/" + end + + def test_cookie_with_all_domain_option_using_a_non_standard_tld + @request.host = "two.subdomains.nextangle.local" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.local; path=/" + end + + def test_cookie_with_all_domain_option_using_australian_style_tld + @request.host = "nextangle.com.au" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.com.au; path=/" + end + + def test_cookie_with_all_domain_option_using_uk_style_tld + @request.host = "nextangle.co.uk" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.co.uk; path=/" + end + + def test_cookie_with_all_domain_option_using_host_with_port + @request.host = "nextangle.local:3000" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.local; path=/" + end + + def test_cookie_with_all_domain_option_using_localhost + @request.host = "localhost" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; path=/" + end + + def test_cookie_with_all_domain_option_using_ipv4_address + @request.host = "192.168.1.1" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; path=/" + end + + def test_cookie_with_all_domain_option_using_ipv6_address + @request.host = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + get :set_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=rizwanreza; path=/" + end + + def test_deleting_cookie_with_all_domain_option + request.cookies[:user_name] = "Joe" + get :delete_cookie_with_domain + assert_response :success + assert_cookie_header "user_name=; domain=.nextangle.com; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000" + end + + def test_cookie_with_all_domain_option_and_tld_length + get :set_cookie_with_domain_and_tld + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.com; path=/" + end + + def test_cookie_with_all_domain_option_using_a_non_standard_tld_and_tld_length + @request.host = "two.subdomains.nextangle.local" + get :set_cookie_with_domain_and_tld + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.local; path=/" + end + + def test_cookie_with_all_domain_option_using_a_non_standard_2_letter_tld + @request.host = "admin.lvh.me" + get :set_cookie_with_domain_and_tld + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.lvh.me; path=/" + end + + def test_cookie_with_all_domain_option_using_host_with_port_and_tld_length + @request.host = "nextangle.local:3000" + get :set_cookie_with_domain_and_tld + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.nextangle.local; path=/" + end + + def test_deleting_cookie_with_all_domain_option_and_tld_length + request.cookies[:user_name] = "Joe" + get :delete_cookie_with_domain_and_tld + assert_response :success + assert_cookie_header "user_name=; domain=.nextangle.com; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000" + end + + def test_cookie_with_several_preset_domains_using_one_of_these_domains + @request.host = "example1.com" + get :set_cookie_with_domains + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=example1.com; path=/" + end + + def test_cookie_with_several_preset_domains_using_other_domain + @request.host = "other-domain.com" + get :set_cookie_with_domains + assert_response :success + assert_cookie_header "user_name=rizwanreza; path=/" + end + + def test_cookie_with_several_preset_domains_using_shared_domain + @request.host = "example3.com" + get :set_cookie_with_domains + assert_response :success + assert_cookie_header "user_name=rizwanreza; domain=.example3.com; path=/" + end + + def test_deletings_cookie_with_several_preset_domains_using_one_of_these_domains + @request.host = "example2.com" + request.cookies[:user_name] = "Joe" + get :delete_cookie_with_domains + assert_response :success + assert_cookie_header "user_name=; domain=example2.com; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000" + end + + def test_deletings_cookie_with_several_preset_domains_using_other_domain + @request.host = "other-domain.com" + request.cookies[:user_name] = "Joe" + get :delete_cookie_with_domains + assert_response :success + assert_cookie_header "user_name=; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000" + end + + def test_cookies_hash_is_indifferent_access + get :symbol_key + assert_equal "david", cookies[:user_name] + assert_equal "david", cookies["user_name"] + get :string_key + assert_equal "dhh", cookies[:user_name] + assert_equal "dhh", cookies["user_name"] + end + + def test_setting_request_cookies_is_indifferent_access + cookies.clear + cookies[:user_name] = "andrew" + get :string_key_mock + assert_equal "david", cookies["user_name"] + + cookies.clear + cookies["user_name"] = "andrew" + get :symbol_key_mock + assert_equal "david", cookies[:user_name] + end + + def test_cookies_retained_across_requests + get :symbol_key + assert_cookie_header "user_name=david; path=/" + assert_equal "david", cookies[:user_name] + + get :noop + assert_not_includes @response.headers, "Set-Cookie" + assert_equal "david", cookies[:user_name] + + get :noop + assert_not_includes @response.headers, "Set-Cookie" + assert_equal "david", cookies[:user_name] + end + + def test_cookies_can_be_cleared + get :symbol_key + assert_equal "david", cookies[:user_name] + + cookies.clear + get :noop + assert_nil cookies[:user_name] + + get :symbol_key + assert_equal "david", cookies[:user_name] + end + + def test_can_set_http_cookie_header + @request.env["HTTP_COOKIE"] = "user_name=david" + get :noop + assert_equal "david", cookies["user_name"] + assert_equal "david", cookies[:user_name] + + get :noop + assert_equal "david", cookies["user_name"] + assert_equal "david", cookies[:user_name] + + @request.env["HTTP_COOKIE"] = "user_name=andrew" + get :noop + assert_equal "andrew", cookies["user_name"] + assert_equal "andrew", cookies[:user_name] + end + + def test_can_set_request_cookies + @request.cookies["user_name"] = "david" + get :noop + assert_equal "david", cookies["user_name"] + assert_equal "david", cookies[:user_name] + + get :noop + assert_equal "david", cookies["user_name"] + assert_equal "david", cookies[:user_name] + + @request.cookies[:user_name] = "andrew" + get :noop + assert_equal "andrew", cookies["user_name"] + assert_equal "andrew", cookies[:user_name] + end + + def test_cookies_precedence_over_http_cookie + @request.env["HTTP_COOKIE"] = "user_name=andrew" + get :authenticate + assert_equal "david", cookies["user_name"] + assert_equal "david", cookies[:user_name] + + get :noop + assert_equal "david", cookies["user_name"] + assert_equal "david", cookies[:user_name] + end + + def test_cookies_precedence_over_request_cookies + @request.cookies["user_name"] = "andrew" + get :authenticate + assert_equal "david", cookies["user_name"] + assert_equal "david", cookies[:user_name] + + get :noop + assert_equal "david", cookies["user_name"] + assert_equal "david", cookies[:user_name] + end + + def test_cookies_are_not_cleared + cookies.encrypted["foo"] = "bar" + get :noop + assert_equal "bar", @controller.encrypted_cookie + end + + def test_signed_cookie_with_expires_set_relatively + request.env["action_dispatch.use_cookies_with_metadata"] = true + + cookies.signed[:user_name] = { value: "assain", expires: 2.hours } + + travel 1.hour + assert_equal "assain", cookies.signed[:user_name] + + travel 2.hours + assert_nil cookies.signed[:user_name] + end + + def test_encrypted_cookie_with_expires_set_relatively + request.env["action_dispatch.use_cookies_with_metadata"] = true + + cookies.encrypted[:user_name] = { value: "assain", expires: 2.hours } + + travel 1.hour + assert_equal "assain", cookies.encrypted[:user_name] + + travel 2.hours + assert_nil cookies.encrypted[:user_name] + end + + def test_vanilla_cookie_with_expires_set_relatively + travel_to Time.utc(2017, 8, 15) do + get :cookie_expires_in_two_hours + assert_cookie_header "user_name=assain; path=/; expires=Tue, 15 Aug 2017 02:00:00 -0000" + end + end + + def test_purpose_metadata_for_encrypted_cookies + get :encrypted_discount_and_user_id_cookie + + cookies[:discount_percentage] = cookies[:user_id] + assert_equal 50, cookies.encrypted[:discount_percentage] + + request.env["action_dispatch.use_cookies_with_metadata"] = true + + get :encrypted_discount_and_user_id_cookie + + cookies[:discount_percentage] = cookies[:user_id] + assert_nil cookies.encrypted[:discount_percentage] + end + + def test_purpose_metadata_for_signed_cookies + get :signed_discount_and_user_id_cookie + + cookies[:discount_percentage] = cookies[:user_id] + assert_equal 50, cookies.signed[:discount_percentage] + + request.env["action_dispatch.use_cookies_with_metadata"] = true + + get :signed_discount_and_user_id_cookie + + cookies[:discount_percentage] = cookies[:user_id] + assert_nil cookies.signed[:discount_percentage] + end + + def test_switch_off_metadata_for_encrypted_cookies_if_config_is_false + request.env["action_dispatch.use_cookies_with_metadata"] = false + + get :encrypted_discount_and_user_id_cookie + + travel 2.hours + assert_equal 50, cookies.encrypted[:user_id] + + cookies[:discount_percentage] = cookies[:user_id] + assert_not_equal 10, cookies.encrypted[:discount_percentage] + assert_equal 50, cookies.encrypted[:discount_percentage] + end + + def test_switch_off_metadata_for_signed_cookies_if_config_is_false + request.env["action_dispatch.use_cookies_with_metadata"] = false + + get :signed_discount_and_user_id_cookie + + travel 2.hours + assert_equal 50, cookies.signed[:user_id] + + cookies[:discount_percentage] = cookies[:user_id] + assert_not_equal 10, cookies.signed[:discount_percentage] + assert_equal 50, cookies.signed[:discount_percentage] + end + + def test_read_rails_5_2_stable_encrypted_cookies_if_config_is_false + request.env["action_dispatch.use_cookies_with_metadata"] = false + + get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on + + assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite] + + travel 1001.years do + assert_nil cookies.encrypted[:favorite] + end + + get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off + + assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite] + end + + def test_read_rails_5_2_stable_signed_cookies_if_config_is_false + request.env["action_dispatch.use_cookies_with_metadata"] = false + + get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on + + assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite] + + travel 1001.years do + assert_nil cookies.signed[:favorite] + end + + get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off + + assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite] + end + + def test_read_rails_5_2_stable_encrypted_cookies_if_use_metadata_config_is_true + request.env["action_dispatch.use_cookies_with_metadata"] = true + + get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_on + + assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite] + + travel 1001.years do + assert_nil cookies.encrypted[:favorite] + end + + get :rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off + + assert_equal "5-2-Stable Chocolate Cookies", cookies.encrypted[:favorite] + end + + def test_read_rails_5_2_stable_signed_cookies_if_use_metadata_config_is_true + request.env["action_dispatch.use_cookies_with_metadata"] = true + + get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_on + + assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite] + + travel 1001.years do + assert_nil cookies.signed[:favorite] + end + + get :rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off + + assert_equal "5-2-Stable Choco Chip Cookie", cookies.signed[:favorite] + end + + private + def assert_cookie_header(expected) + header = @response.headers["Set-Cookie"] + if header.respond_to?(:to_str) + assert_equal expected.split("\n").sort, header.split("\n").sort + else + assert_equal expected.split("\n"), header + end + end + + def assert_not_cookie_header(expected) + header = @response.headers["Set-Cookie"] + if header.respond_to?(:to_str) + assert_not_equal expected.split("\n").sort, header.split("\n").sort + else + assert_not_equal expected.split("\n"), header + end + end +end diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb new file mode 100644 index 0000000000..c326f276bf --- /dev/null +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -0,0 +1,571 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class DebugExceptionsTest < ActionDispatch::IntegrationTest + InterceptedErrorInstance = StandardError.new + + class Boomer + attr_accessor :closed + + def initialize(detailed = false) + @detailed = detailed + @closed = false + end + + # We're obliged to implement this (even though it doesn't actually + # get called here) to properly comply with the Rack SPEC + def each + end + + def close + @closed = true + end + + def method_that_raises + raise StandardError.new "error in framework" + end + + def raise_nested_exceptions + raise "First error" + rescue + begin + raise "Second error" + rescue + raise "Third error" + end + end + + def call(env) + env["action_dispatch.show_detailed_exceptions"] = @detailed + req = ActionDispatch::Request.new(env) + case req.path + when %r{/pass} + [404, { "X-Cascade" => "pass" }, self] + when %r{/not_found} + raise AbstractController::ActionNotFound + when %r{/runtime_error} + raise RuntimeError + when %r{/method_not_allowed} + raise ActionController::MethodNotAllowed + when %r{/intercepted_error} + raise InterceptedErrorInstance + when %r{/unknown_http_method} + raise ActionController::UnknownHttpMethod + when %r{/not_implemented} + raise ActionController::NotImplemented + when %r{/unprocessable_entity} + raise ActionController::InvalidAuthenticityToken + when %r{/not_found_original_exception} + begin + raise AbstractController::ActionNotFound.new + rescue + raise ActionView::Template::Error.new("template") + end + when %r{/missing_template} + raise ActionView::MissingTemplate.new(%w(foo), "foo/index", %w(foo), false, "mailer") + when %r{/bad_request} + raise ActionController::BadRequest + when %r{/missing_keys} + raise ActionController::UrlGenerationError, "No route matches" + when %r{/parameter_missing} + raise ActionController::ParameterMissing, :missing_param_key + when %r{/original_syntax_error} + eval "broke_syntax =" # `eval` need for raise native SyntaxError at runtime + when %r{/syntax_error_into_view} + begin + eval "broke_syntax =" + rescue Exception + template = ActionView::Template.new(File.read(__FILE__), + __FILE__, + ActionView::Template::Handlers::Raw.new, + {}) + raise ActionView::Template::Error.new(template) + end + when %r{/framework_raises} + method_that_raises + when %r{/nested_exceptions} + raise_nested_exceptions + else + raise "puke!" + end + end + end + + Interceptor = proc { |request, exception| request.set_header("int", exception) } + BadInterceptor = proc { |request, exception| raise "bad" } + RoutesApp = Struct.new(:routes).new(SharedTestRoutes) + ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp) + DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp) + InterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [Interceptor]) + BadInterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [BadInterceptor]) + + test "skip diagnosis if not showing detailed exceptions" do + @app = ProductionApp + assert_raise RuntimeError do + get "/", headers: { "action_dispatch.show_exceptions" => true } + end + end + + test "skip diagnosis if not showing exceptions" do + @app = DevelopmentApp + assert_raise RuntimeError do + get "/", headers: { "action_dispatch.show_exceptions" => false } + end + end + + test "raise an exception on cascade pass" do + @app = ProductionApp + assert_raise ActionController::RoutingError do + get "/pass", headers: { "action_dispatch.show_exceptions" => true } + end + end + + test "closes the response body on cascade pass" do + boomer = Boomer.new(false) + @app = ActionDispatch::DebugExceptions.new(boomer) + assert_raise ActionController::RoutingError do + get "/pass", headers: { "action_dispatch.show_exceptions" => true } + end + assert boomer.closed, "Expected to close the response body" + end + + test "displays routes in a table when a RoutingError occurs" do + @app = DevelopmentApp + get "/pass", headers: { "action_dispatch.show_exceptions" => true } + routing_table = body[/route_table.*<.table>/m] + assert_match "/:controller(/:action)(.:format)", routing_table + assert_match ":controller#:action", routing_table + assert_no_match "<|>", routing_table, "there should not be escaped html in the output" + end + + test "displays request and response info when a RoutingError occurs" do + @app = DevelopmentApp + + get "/pass", headers: { "action_dispatch.show_exceptions" => true } + + assert_select "h2", /Request/ + assert_select "h2", /Response/ + end + + test "rescue with diagnostics message" do + @app = DevelopmentApp + + get "/", headers: { "action_dispatch.show_exceptions" => true } + assert_response 500 + assert_match(/puke/, body) + + get "/not_found", headers: { "action_dispatch.show_exceptions" => true } + assert_response 404 + assert_match(/#{AbstractController::ActionNotFound.name}/, body) + + get "/method_not_allowed", headers: { "action_dispatch.show_exceptions" => true } + assert_response 405 + assert_match(/ActionController::MethodNotAllowed/, body) + + get "/unknown_http_method", headers: { "action_dispatch.show_exceptions" => true } + assert_response 405 + assert_match(/ActionController::UnknownHttpMethod/, body) + + get "/bad_request", headers: { "action_dispatch.show_exceptions" => true } + assert_response 400 + assert_match(/ActionController::BadRequest/, body) + + get "/parameter_missing", headers: { "action_dispatch.show_exceptions" => true } + assert_response 400 + assert_match(/ActionController::ParameterMissing/, body) + end + + test "rescue with text error for xhr request" do + @app = DevelopmentApp + xhr_request_env = { "action_dispatch.show_exceptions" => true, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" } + + get "/", headers: xhr_request_env + assert_response 500 + assert_no_match(/<header>/, body) + assert_no_match(/<body>/, body) + assert_equal "text/plain", response.content_type + assert_match(/RuntimeError\npuke/, body) + + Rails.stub :root, Pathname.new(".") do + get "/", headers: xhr_request_env + + assert_response 500 + assert_match "Extracted source (around line #", body + assert_select "pre", { count: 0 }, body + end + + get "/not_found", headers: xhr_request_env + assert_response 404 + assert_no_match(/<body>/, body) + assert_equal "text/plain", response.content_type + assert_match(/#{AbstractController::ActionNotFound.name}/, body) + + get "/method_not_allowed", headers: xhr_request_env + assert_response 405 + assert_no_match(/<body>/, body) + assert_equal "text/plain", response.content_type + assert_match(/ActionController::MethodNotAllowed/, body) + + get "/unknown_http_method", headers: xhr_request_env + assert_response 405 + assert_no_match(/<body>/, body) + assert_equal "text/plain", response.content_type + assert_match(/ActionController::UnknownHttpMethod/, body) + + get "/bad_request", headers: xhr_request_env + assert_response 400 + assert_no_match(/<body>/, body) + assert_equal "text/plain", response.content_type + assert_match(/ActionController::BadRequest/, body) + + get "/parameter_missing", headers: xhr_request_env + assert_response 400 + assert_no_match(/<body>/, body) + assert_equal "text/plain", response.content_type + assert_match(/ActionController::ParameterMissing/, body) + end + + test "rescue with JSON error for JSON API request" do + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/", headers: { "action_dispatch.show_exceptions" => true }, as: :json + assert_response 500 + assert_no_match(/<header>/, body) + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/RuntimeError: puke/, body) + + get "/not_found", headers: { "action_dispatch.show_exceptions" => true }, as: :json + assert_response 404 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/#{AbstractController::ActionNotFound.name}/, body) + + get "/method_not_allowed", headers: { "action_dispatch.show_exceptions" => true }, as: :json + assert_response 405 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::MethodNotAllowed/, body) + + get "/unknown_http_method", headers: { "action_dispatch.show_exceptions" => true }, as: :json + assert_response 405 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::UnknownHttpMethod/, body) + + get "/bad_request", headers: { "action_dispatch.show_exceptions" => true }, as: :json + assert_response 400 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::BadRequest/, body) + + get "/parameter_missing", headers: { "action_dispatch.show_exceptions" => true }, as: :json + assert_response 400 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::ParameterMissing/, body) + end + + test "rescue with HTML format for HTML API request" do + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/index.html", headers: { "action_dispatch.show_exceptions" => true } + assert_response 500 + assert_match(/<header>/, body) + assert_match(/<body>/, body) + assert_equal "text/html", response.content_type + assert_match(/puke/, body) + end + + test "rescue with XML format for XML API requests" do + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/index.xml", headers: { "action_dispatch.show_exceptions" => true } + assert_response 500 + assert_equal "application/xml", response.content_type + assert_match(/RuntimeError: puke/, body) + end + + test "rescue with JSON format as fallback if API request format is not supported" do + Mime::Type.register "text/wibble", :wibble + + ActionDispatch::IntegrationTest.register_encoder(:wibble, + param_encoder: -> params { params }) + + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/index", headers: { "action_dispatch.show_exceptions" => true }, as: :wibble + assert_response 500 + assert_equal "application/json", response.content_type + assert_match(/RuntimeError: puke/, body) + + ensure + Mime::Type.unregister :wibble + end + + test "does not show filtered parameters" do + @app = DevelopmentApp + + get "/", params: { "foo" => "bar" }, headers: { "action_dispatch.show_exceptions" => true, + "action_dispatch.parameter_filter" => [:foo] } + assert_response 500 + assert_match(""foo"=>"[FILTERED]"", body) + end + + test "show registered original exception for wrapped exceptions" do + @app = DevelopmentApp + + get "/not_found_original_exception", headers: { "action_dispatch.show_exceptions" => true } + assert_response 404 + assert_match(/AbstractController::ActionNotFound/, body) + end + + test "named urls missing keys raise 500 level error" do + @app = DevelopmentApp + + get "/missing_keys", headers: { "action_dispatch.show_exceptions" => true } + assert_response 500 + + assert_match(/ActionController::UrlGenerationError/, body) + end + + test "show the controller name in the diagnostics template when controller name is present" do + @app = DevelopmentApp + get("/runtime_error", headers: { + "action_dispatch.show_exceptions" => true, + "action_dispatch.request.parameters" => { + "action" => "show", + "id" => "unknown", + "controller" => "featured_tile" + } + }) + assert_response 500 + assert_match(/RuntimeError\n\s+in FeaturedTileController/, body) + end + + test "show formatted params" do + @app = DevelopmentApp + + params = { + "id" => "unknown", + "someparam" => { + "foo" => "bar", + "abc" => "goo" + } + } + + get("/runtime_error", headers: { + "action_dispatch.show_exceptions" => true, + "action_dispatch.request.parameters" => { + "action" => "show", + "controller" => "featured_tile" + }.merge(params) + }) + assert_response 500 + + assert_includes(body, CGI.escapeHTML(PP.pp(params, +"", 200))) + end + + test "sets the HTTP charset parameter" do + @app = DevelopmentApp + + get "/", headers: { "action_dispatch.show_exceptions" => true } + assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] + end + + test "uses logger from env" do + @app = DevelopmentApp + output = StringIO.new + get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => Logger.new(output) } + assert_match(/puke/, output.rewind && output.read) + end + + test "logs only what is necessary" do + @app = DevelopmentApp + io = StringIO.new + logger = ActiveSupport::Logger.new(io) + + _old, ActionView::Base.logger = ActionView::Base.logger, logger + begin + get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => logger } + ensure + ActionView::Base.logger = _old + end + + output = io.rewind && io.read + lines = output.lines + + # Other than the first three... + assert_equal([" \n", "RuntimeError (puke!):\n", " \n"], lines.slice!(0, 3)) + lines.each do |line| + # .. all the remaining lines should be from the backtrace + assert_match(/:\d+:in /, line) + end + end + + test "logs with non active support loggers" do + @app = DevelopmentApp + io = StringIO.new + logger = Logger.new(io) + + _old, ActionView::Base.logger = ActionView::Base.logger, logger + begin + assert_nothing_raised do + get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => logger } + end + ensure + ActionView::Base.logger = _old + end + + assert_match(/puke/, io.rewind && io.read) + end + + test "uses backtrace cleaner from env" do + @app = DevelopmentApp + backtrace_cleaner = ActiveSupport::BacktraceCleaner.new + + backtrace_cleaner.stub :clean, ["passed backtrace cleaner"] do + get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.backtrace_cleaner" => backtrace_cleaner } + assert_match(/passed backtrace cleaner/, body) + end + end + + test "logs exception backtrace when all lines silenced" do + output = StringIO.new + backtrace_cleaner = ActiveSupport::BacktraceCleaner.new + backtrace_cleaner.add_silencer { true } + + env = { "action_dispatch.show_exceptions" => true, + "action_dispatch.logger" => Logger.new(output), + "action_dispatch.backtrace_cleaner" => backtrace_cleaner } + + get "/", headers: env + assert_operator((output.rewind && output.read).lines.count, :>, 10) + end + + test "display backtrace when error type is SyntaxError" do + @app = DevelopmentApp + + get "/original_syntax_error", headers: { "action_dispatch.backtrace_cleaner" => ActiveSupport::BacktraceCleaner.new } + + assert_response 500 + assert_select "#Application-Trace-0" do + assert_select "code", /syntax error, unexpected/ + end + end + + test "display backtrace on template missing errors" do + @app = DevelopmentApp + + get "/missing_template" + + assert_select "header h1", /Template is missing/ + + assert_select "#container h2", /^Missing template/ + + assert_select "#Application-Trace-0" + assert_select "#Framework-Trace-0" + assert_select "#Full-Trace-0" + + assert_select "h2", /Request/ + end + + test "display backtrace when error type is SyntaxError wrapped by ActionView::Template::Error" do + @app = DevelopmentApp + + get "/syntax_error_into_view", headers: { "action_dispatch.backtrace_cleaner" => ActiveSupport::BacktraceCleaner.new } + + assert_response 500 + assert_select "#Application-Trace-0" do + assert_select "code", /syntax error, unexpected/ + end + end + + test "debug exceptions app shows user code that caused the error in source view" do + @app = DevelopmentApp + Rails.stub :root, Pathname.new(".") do + cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc| + bc.add_silencer { |line| line =~ /method_that_raises/ } + bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} } + end + + get "/framework_raises", headers: { "action_dispatch.backtrace_cleaner" => cleaner } + + # Assert correct error + assert_response 500 + assert_select "h2", /error in framework/ + + # assert source view line is the call to method_that_raises + assert_select "div.source:not(.hidden)" do + assert_select "pre .line.active", /method_that_raises/ + end + + # assert first source view (hidden) that throws the error + assert_select "div.source:first" do + assert_select "pre .line.active", /raise StandardError\.new/ + end + + # assert application trace refers to line that calls method_that_raises is first + assert_select "#Application-Trace-0" do + assert_select "code a:first", %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call} + end + + # assert framework trace that threw the error is first + assert_select "#Framework-Trace-0" do + assert_select "code a:first", /method_that_raises/ + end + end + end + + test "invoke interceptors before rendering" do + @app = InterceptedApp + get "/intercepted_error", headers: { "action_dispatch.show_exceptions" => true } + + assert_equal InterceptedErrorInstance, request.get_header("int") + end + + test "bad interceptors doesn't debug exceptions" do + @app = BadInterceptedApp + + get "/puke", headers: { "action_dispatch.show_exceptions" => true } + + assert_response 500 + assert_match(/puke/, body) + end + + test "debug exceptions app shows all the nested exceptions in source view" do + @app = DevelopmentApp + Rails.stub :root, Pathname.new(".") do + cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc| + bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} } + end + + get "/nested_exceptions", headers: { "action_dispatch.backtrace_cleaner" => cleaner } + + # Assert correct error + assert_response 500 + assert_select "h2", /Third error/ + + # assert source view line shows the last error + assert_select "div.source:not(.hidden)" do + assert_select "pre .line.active", /raise "Third error"/ + end + + # assert application trace refers to line that raises the last exception + assert_select "#Application-Trace-0" do + assert_select "code a:first", %r{in `rescue in rescue in raise_nested_exceptions'} + end + + # assert the second application trace refers to the line that raises the second exception + assert_select "#Application-Trace-1" do + assert_select "code a:first", %r{in `rescue in raise_nested_exceptions'} + end + + # assert the third application trace refers to the line that raises the first exception + assert_select "#Application-Trace-2" do + assert_select "code a:first", %r{in `raise_nested_exceptions'} + end + end + end +end diff --git a/actionpack/test/dispatch/debug_locks_test.rb b/actionpack/test/dispatch/debug_locks_test.rb new file mode 100644 index 0000000000..d69614bd79 --- /dev/null +++ b/actionpack/test/dispatch/debug_locks_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class DebugLocksTest < ActionDispatch::IntegrationTest + setup do + build_app + end + + def test_render_threads_status + thread_ready = Concurrent::CountDownLatch.new + test_terminated = Concurrent::CountDownLatch.new + + thread = Thread.new do + ActiveSupport::Dependencies.interlock.running do + thread_ready.count_down + test_terminated.wait + end + end + + thread_ready.wait + + get "/rails/locks" + + test_terminated.count_down + + assert_match(/Thread.*?Sharing/, @response.body) + ensure + thread.join + end + + private + def build_app + @app = self.class.build_app do |middleware| + middleware.use ActionDispatch::DebugLocks + end + end +end diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb new file mode 100644 index 0000000000..668469a01d --- /dev/null +++ b/actionpack/test/dispatch/exception_wrapper_test.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + class ExceptionWrapperTest < ActionDispatch::IntegrationTest + class TestError < StandardError + attr_reader :backtrace + + def initialize(*backtrace) + @backtrace = backtrace.flatten + end + end + + class BadlyDefinedError < StandardError + def backtrace + nil + end + end + + setup do + @cleaner = ActiveSupport::BacktraceCleaner.new + @cleaner.remove_filters! + @cleaner.add_silencer { |line| line !~ /^lib/ } + end + + test "#source_extracts fetches source fragments for every backtrace entry" do + exception = TestError.new("lib/file.rb:42:in `index'") + wrapper = ExceptionWrapper.new(nil, exception) + + assert_called_with(wrapper, :source_fragment, ["lib/file.rb", 42], returns: "foo") do + assert_equal [ code: "foo", line_number: 42 ], wrapper.source_extracts + end + end + + test "#source_extracts works with Windows paths" do + exc = TestError.new("c:/path/to/rails/app/controller.rb:27:in 'index':") + + wrapper = ExceptionWrapper.new(nil, exc) + + assert_called_with(wrapper, :source_fragment, ["c:/path/to/rails/app/controller.rb", 27], returns: "nothing") do + assert_equal [ code: "nothing", line_number: 27 ], wrapper.source_extracts + end + end + + test "#source_extracts works with non standard backtrace" do + exc = TestError.new("invalid") + + wrapper = ExceptionWrapper.new(nil, exc) + + assert_called_with(wrapper, :source_fragment, ["invalid", 0], returns: "nothing") do + assert_equal [ code: "nothing", line_number: 0 ], wrapper.source_extracts + end + end + + test "#application_trace returns traces only from the application" do + exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) + wrapper = ExceptionWrapper.new(@cleaner, exception) + + assert_equal [ "lib/file.rb:42:in `index'" ], wrapper.application_trace + end + + test "#status_code returns 400 for Rack::Utils::ParameterTypeError" do + exception = Rack::Utils::ParameterTypeError.new + wrapper = ExceptionWrapper.new(@cleaner, exception) + assert_equal 400, wrapper.status_code + end + + test "#application_trace cannot be nil" do + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) + + assert_equal [], nil_backtrace_wrapper.application_trace + assert_equal [], nil_cleaner_wrapper.application_trace + end + + test "#framework_trace returns traces outside the application" do + exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) + wrapper = ExceptionWrapper.new(@cleaner, exception) + + assert_equal caller, wrapper.framework_trace + end + + test "#framework_trace cannot be nil" do + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) + + assert_equal [], nil_backtrace_wrapper.framework_trace + assert_equal [], nil_cleaner_wrapper.framework_trace + end + + test "#full_trace returns application and framework traces" do + exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'")) + wrapper = ExceptionWrapper.new(@cleaner, exception) + + assert_equal exception.backtrace, wrapper.full_trace + end + + test "#full_trace cannot be nil" do + nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new) + nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new) + + assert_equal [], nil_backtrace_wrapper.full_trace + assert_equal [], nil_cleaner_wrapper.full_trace + end + + test "#traces returns every trace by category enumerated with an index" do + exception = TestError.new("lib/file.rb:42:in `index'", "/gems/rack.rb:43:in `index'") + wrapper = ExceptionWrapper.new(@cleaner, exception) + + assert_equal({ + "Application Trace" => [ + exception_object_id: exception.object_id, + id: 0, + trace: "lib/file.rb:42:in `index'" + ], + "Framework Trace" => [ + exception_object_id: exception.object_id, + id: 1, + trace: "/gems/rack.rb:43:in `index'" + ], + "Full Trace" => [ + { + exception_object_id: exception.object_id, + id: 0, + trace: "lib/file.rb:42:in `index'" + }, + { + exception_object_id: exception.object_id, + id: 1, + trace: "/gems/rack.rb:43:in `index'" + } + ] + }, wrapper.traces) + end + end +end diff --git a/actionpack/test/dispatch/executor_test.rb b/actionpack/test/dispatch/executor_test.rb new file mode 100644 index 0000000000..5b8be39b6d --- /dev/null +++ b/actionpack/test/dispatch/executor_test.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ExecutorTest < ActiveSupport::TestCase + class MyBody < Array + def initialize(&block) + @on_close = block + end + + def foo + "foo" + end + + def bar + "bar" + end + + def close + @on_close.call if @on_close + end + end + + def test_returned_body_object_always_responds_to_close + body = call_and_return_body + assert_respond_to body, :close + end + + def test_returned_body_object_always_responds_to_close_even_if_called_twice + body = call_and_return_body + assert_respond_to body, :close + body.close + + body = call_and_return_body + assert_respond_to body, :close + body.close + end + + def test_returned_body_object_behaves_like_underlying_object + body = call_and_return_body do + b = MyBody.new + b << "hello" + b << "world" + [200, { "Content-Type" => "text/html" }, b] + end + assert_equal 2, body.size + assert_equal "hello", body[0] + assert_equal "world", body[1] + assert_equal "foo", body.foo + assert_equal "bar", body.bar + end + + def test_it_calls_close_on_underlying_object_when_close_is_called_on_body + close_called = false + body = call_and_return_body do + b = MyBody.new do + close_called = true + end + [200, { "Content-Type" => "text/html" }, b] + end + body.close + assert close_called + end + + def test_returned_body_object_responds_to_all_methods_supported_by_underlying_object + body = call_and_return_body do + [200, { "Content-Type" => "text/html" }, MyBody.new] + end + assert_respond_to body, :size + assert_respond_to body, :each + assert_respond_to body, :foo + assert_respond_to body, :bar + end + + def test_run_callbacks_are_called_before_close + running = false + executor.to_run { running = true } + + body = call_and_return_body + assert running + + running = false + body.close + assert_not running + end + + def test_complete_callbacks_are_called_on_close + completed = false + executor.to_complete { completed = true } + + body = call_and_return_body + assert_not completed + + body.close + assert completed + end + + def test_complete_callbacks_are_called_on_exceptions + completed = false + executor.to_complete { completed = true } + + begin + call_and_return_body do + raise "error" + end + rescue + end + + assert completed + end + + def test_callbacks_execute_in_shared_context + result = false + executor.to_run { @in_shared_context = true } + executor.to_complete { result = @in_shared_context } + + call_and_return_body.close + assert result + assert_not defined?(@in_shared_context) # it's not in the test itself + end + + private + def call_and_return_body(&block) + app = middleware(block || proc { [200, {}, "response"] }) + _, _, body = app.call("rack.input" => StringIO.new("")) + body + end + + def middleware(inner_app) + ActionDispatch::Executor.new(inner_app, executor) + end + + def executor + @executor ||= Class.new(ActiveSupport::Executor) + end +end diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb new file mode 100644 index 0000000000..bd2a5b35fb --- /dev/null +++ b/actionpack/test/dispatch/header_test.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class HeaderTest < ActiveSupport::TestCase + def make_headers(hash) + ActionDispatch::Http::Headers.new ActionDispatch::Request.new hash + end + + setup do + @headers = make_headers( + "CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page" + ) + end + + test "#new does not normalize the data" do + headers = make_headers( + "Content-Type" => "application/json", + "HTTP_REFERER" => "/some/page", + "Host" => "http://test.com") + + assert_equal({ "Content-Type" => "application/json", + "HTTP_REFERER" => "/some/page", + "Host" => "http://test.com" }, headers.env) + end + + test "#env returns the headers as env variables" do + assert_equal({ "CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page" }, @headers.env) + end + + test "#each iterates through the env variables" do + headers = [] + @headers.each { |pair| headers << pair } + assert_equal [["CONTENT_TYPE", "text/plain"], + ["HTTP_REFERER", "/some/page"]], headers + end + + test "set new headers" do + @headers["Host"] = "127.0.0.1" + + assert_equal "127.0.0.1", @headers["Host"] + assert_equal "127.0.0.1", @headers["HTTP_HOST"] + end + + test "add to multivalued headers" do + # Sets header when not present + @headers.add "Foo", "1" + assert_equal "1", @headers["Foo"] + + # Ignores nil values + @headers.add "Foo", nil + assert_equal "1", @headers["Foo"] + + # Converts value to string + @headers.add "Foo", 1 + assert_equal "1,1", @headers["Foo"] + + # Case-insensitive + @headers.add "fOo", 2 + assert_equal "1,1,2", @headers["foO"] + end + + test "headers can contain numbers" do + @headers["Content-MD5"] = "Q2hlY2sgSW50ZWdyaXR5IQ==" + + assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["Content-MD5"] + assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["HTTP_CONTENT_MD5"] + end + + test "set new env variables" do + @headers["HTTP_HOST"] = "127.0.0.1" + + assert_equal "127.0.0.1", @headers["Host"] + assert_equal "127.0.0.1", @headers["HTTP_HOST"] + end + + test "key?" do + assert @headers.key?("CONTENT_TYPE") + assert_includes @headers, "CONTENT_TYPE" + assert @headers.key?("Content-Type") + assert_includes @headers, "Content-Type" + end + + test "fetch with block" do + assert_equal "omg", @headers.fetch("notthere") { "omg" } + end + + test "accessing http header" do + assert_equal "/some/page", @headers["Referer"] + assert_equal "/some/page", @headers["referer"] + assert_equal "/some/page", @headers["HTTP_REFERER"] + end + + test "accessing special header" do + assert_equal "text/plain", @headers["Content-Type"] + assert_equal "text/plain", @headers["content-type"] + assert_equal "text/plain", @headers["CONTENT_TYPE"] + end + + test "fetch" do + assert_equal "text/plain", @headers.fetch("content-type", nil) + assert_equal "not found", @headers.fetch("not-found", "not found") + end + + test "#merge! headers with mutation" do + @headers.merge!("Host" => "http://example.test", + "Content-Type" => "text/html") + assert_equal({ "HTTP_HOST" => "http://example.test", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page" }, @headers.env) + end + + test "#merge! env with mutation" do + @headers.merge!("HTTP_HOST" => "http://first.com", + "CONTENT_TYPE" => "text/html") + assert_equal({ "HTTP_HOST" => "http://first.com", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page" }, @headers.env) + end + + test "merge without mutation" do + combined = @headers.merge("HTTP_HOST" => "http://example.com", + "CONTENT_TYPE" => "text/html") + assert_equal({ "HTTP_HOST" => "http://example.com", + "CONTENT_TYPE" => "text/html", + "HTTP_REFERER" => "/some/page" }, combined.env) + + assert_equal({ "CONTENT_TYPE" => "text/plain", + "HTTP_REFERER" => "/some/page" }, @headers.env) + end + + test "env variables with . are not modified" do + headers = make_headers({}) + headers.merge! "rack.input" => "", + "rack.request.cookie_hash" => "", + "action_dispatch.logger" => "" + + assert_equal(["action_dispatch.logger", + "rack.input", + "rack.request.cookie_hash"], headers.env.keys.sort) + end + + test "symbols are treated as strings" do + headers = make_headers({}) + headers.merge!(:SERVER_NAME => "example.com", + "HTTP_REFERER" => "/", + :Host => "test.com") + assert_equal "example.com", headers["SERVER_NAME"] + assert_equal "/", headers[:HTTP_REFERER] + assert_equal "test.com", headers["HTTP_HOST"] + end + + test "headers directly modifies the passed environment" do + env = { "HTTP_REFERER" => "/" } + headers = make_headers(env) + headers["Referer"] = "http://example.com/" + headers["CONTENT_TYPE"] = "text/plain" + assert_equal({ "HTTP_REFERER" => "http://example.com/", + "CONTENT_TYPE" => "text/plain" }, env) + end + + test "fetch exception" do + assert_raises KeyError do + @headers.fetch(:some_key_that_doesnt_exist) + end + end +end diff --git a/actionpack/test/dispatch/host_authorization_test.rb b/actionpack/test/dispatch/host_authorization_test.rb new file mode 100644 index 0000000000..dae7b08ec1 --- /dev/null +++ b/actionpack/test/dispatch/host_authorization_test.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "ipaddr" + +class HostAuthorizationTest < ActionDispatch::IntegrationTest + App = -> env { [200, {}, %w(Success)] } + + test "blocks requests to unallowed host" do + @app = ActionDispatch::HostAuthorization.new(App, %w(only.com)) + + get "/" + + assert_response :forbidden + assert_match "Blocked host: www.example.com", response.body + end + + test "passes all requests to if the whitelist is empty" do + @app = ActionDispatch::HostAuthorization.new(App, nil) + + get "/" + + assert_response :ok + assert_equal "Success", body + end + + test "passes requests to allowed host" do + @app = ActionDispatch::HostAuthorization.new(App, %w(www.example.com)) + + get "/" + + assert_response :ok + assert_equal "Success", body + end + + test "the whitelist could be a single element" do + @app = ActionDispatch::HostAuthorization.new(App, "www.example.com") + + get "/" + + assert_response :ok + assert_equal "Success", body + end + + test "passes requests to allowed hosts with domain name notation" do + @app = ActionDispatch::HostAuthorization.new(App, ".example.com") + + get "/" + + assert_response :ok + assert_equal "Success", body + end + + test "does not allow domain name notation in the HOST header itself" do + @app = ActionDispatch::HostAuthorization.new(App, ".example.com") + + get "/", env: { + "HOST" => ".example.com", + } + + assert_response :forbidden + assert_match "Blocked host: .example.com", response.body + end + + test "checks for requests with #=== to support wider range of host checks" do + @app = ActionDispatch::HostAuthorization.new(App, [-> input { input == "www.example.com" }]) + + get "/" + + assert_response :ok + assert_equal "Success", body + end + + test "mark the host when authorized" do + @app = ActionDispatch::HostAuthorization.new(App, ".example.com") + + get "/" + + assert_equal "www.example.com", request.get_header("action_dispatch.authorized_host") + end + + test "sanitizes regular expressions to prevent accidental matches" do + @app = ActionDispatch::HostAuthorization.new(App, [/w.example.co/]) + + get "/" + + assert_response :forbidden + assert_match "Blocked host: www.example.com", response.body + end + + test "blocks requests to unallowed host supporting custom responses" do + @app = ActionDispatch::HostAuthorization.new(App, ["w.example.co"], -> env do + [401, {}, %w(Custom)] + end) + + get "/" + + assert_response :unauthorized + assert_equal "Custom", body + end + + test "blocks requests with spoofed X-FORWARDED-HOST" do + @app = ActionDispatch::HostAuthorization.new(App, [IPAddr.new("127.0.0.1")]) + + get "/", env: { + "HTTP_X_FORWARDED_HOST" => "127.0.0.1", + "HOST" => "www.example.com", + } + + assert_response :forbidden + assert_match "Blocked host: 127.0.0.1", response.body + end + + test "does not consider IP addresses in X-FORWARDED-HOST spoofed when disabled" do + @app = ActionDispatch::HostAuthorization.new(App, nil) + + get "/", env: { + "HTTP_X_FORWARDED_HOST" => "127.0.0.1", + "HOST" => "www.example.com", + } + + assert_response :ok + assert_equal "Success", body + end + + test "detects localhost domain spoofing" do + @app = ActionDispatch::HostAuthorization.new(App, "localhost") + + get "/", env: { + "HTTP_X_FORWARDED_HOST" => "localhost", + "HOST" => "www.example.com", + } + + assert_response :forbidden + assert_match "Blocked host: localhost", response.body + end + + test "forwarded hosts should be permitted" do + @app = ActionDispatch::HostAuthorization.new(App, "domain.com") + + get "/", env: { + "HTTP_X_FORWARDED_HOST" => "sub.domain.com", + "HOST" => "domain.com", + } + + assert_response :forbidden + assert_match "Blocked host: sub.domain.com", response.body + end + + test "forwarded hosts are allowed when permitted" do + @app = ActionDispatch::HostAuthorization.new(App, ".domain.com") + + get "/", env: { + "HTTP_X_FORWARDED_HOST" => "sub.domain.com", + "HOST" => "domain.com", + } + + assert_response :ok + assert_equal "Success", body + end +end diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb new file mode 100644 index 0000000000..a9a56f205f --- /dev/null +++ b/actionpack/test/dispatch/live_response_test.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "concurrent/atomic/count_down_latch" + +module ActionController + module Live + class ResponseTest < ActiveSupport::TestCase + def setup + @response = Live::Response.new + @response.request = ActionDispatch::Request.empty + end + + def test_header_merge + header = @response.header.merge("Foo" => "Bar") + assert_kind_of(ActionController::Live::Response::Header, header) + assert_not_equal header, @response.header + end + + def test_initialize_with_default_headers + r = Class.new(Live::Response) do + def self.default_headers + { "omg" => "g" } + end + end + + header = r.new.header + assert_kind_of(ActionController::Live::Response::Header, header) + end + + def test_parallel + latch = Concurrent::CountDownLatch.new + + t = Thread.new { + @response.stream.write "foo" + latch.wait + @response.stream.close + } + + @response.await_commit + @response.each do |part| + assert_equal "foo", part + latch.count_down + end + assert t.join + end + + def test_setting_body_populates_buffer + @response.body = "omg" + @response.close + assert_equal ["omg"], @response.body_parts + end + + def test_cache_control_is_set + @response.stream.write "omg" + assert_equal "no-cache", @response.headers["Cache-Control"] + end + + def test_content_length_is_removed + @response.headers["Content-Length"] = "1234" + @response.stream.write "omg" + assert_nil @response.headers["Content-Length"] + end + + def test_headers_cannot_be_written_after_webserver_reads + @response.stream.write "omg" + latch = Concurrent::CountDownLatch.new + + t = Thread.new { + @response.each do + latch.count_down + end + } + + latch.wait + assert_predicate @response.headers, :frozen? + e = assert_raises(ActionDispatch::IllegalStateError) do + @response.headers["Content-Length"] = "zomg" + end + + assert_equal "header already sent", e.message + @response.stream.close + t.join + end + + def test_headers_cannot_be_written_after_close + @response.stream.close + # we can add data until it's actually written, which happens on `each` + @response.each { |x| } + + e = assert_raises(ActionDispatch::IllegalStateError) do + @response.headers["Content-Length"] = "zomg" + end + assert_equal "header already sent", e.message + end + end + end +end diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb new file mode 100644 index 0000000000..969a08efed --- /dev/null +++ b/actionpack/test/dispatch/mapper_test.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Routing + class MapperTest < ActiveSupport::TestCase + class FakeSet < ActionDispatch::Routing::RouteSet + def resources_path_names + {} + end + + def request_class + ActionDispatch::Request + end + + def dispatcher_class + RouteSet::Dispatcher + end + + def defaults + routes.map(&:defaults) + end + + def conditions + routes.map(&:constraints) + end + + def requirements + routes.map(&:path).map(&:requirements) + end + + def asts + routes.map(&:path).map(&:spec) + end + end + + def test_initialize + Mapper.new FakeSet.new + end + + def test_scope_raises_on_anchor + fakeset = FakeSet.new + mapper = Mapper.new fakeset + assert_raises(ArgumentError) do + mapper.scope(anchor: false) do + end + end + end + + def test_blows_up_without_via + fakeset = FakeSet.new + mapper = Mapper.new fakeset + assert_raises(ArgumentError) do + mapper.match "/", to: "posts#index", as: :main + end + end + + def test_unscoped_formatted + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.get "/foo", to: "posts#index", as: :main, format: true + assert_equal({ controller: "posts", action: "index" }, + fakeset.defaults.first) + assert_equal "/foo.:format", fakeset.asts.first.to_s + end + + def test_scoped_formatted + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(format: true) do + mapper.get "/foo", to: "posts#index", as: :main + end + assert_equal({ controller: "posts", action: "index" }, + fakeset.defaults.first) + assert_equal "/foo.:format", fakeset.asts.first.to_s + end + + def test_random_keys + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(omg: :awesome) do + mapper.get "/", to: "posts#index", as: :main + end + assert_equal({ omg: :awesome, controller: "posts", action: "index" }, + fakeset.defaults.first) + assert_equal("GET", fakeset.routes.first.verb) + end + + def test_mapping_requirements + options = {} + scope = Mapper::Scope.new({}) + ast = Journey::Parser.parse "/store/:name(*rest)" + m = Mapper::Mapping.build(scope, FakeSet.new, ast, "foo", "bar", nil, [:get], nil, {}, true, options) + assert_equal(/.+?/, m.requirements[:rest]) + end + + def test_via_scope + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(via: :put) do + mapper.match "/", to: "posts#index", as: :main + end + assert_equal("PUT", fakeset.routes.first.verb) + end + + def test_to_scope + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(to: "posts#index") do + mapper.get :all + mapper.post :most + end + + assert_equal "posts#index", fakeset.routes.to_a[0].defaults[:to] + assert_equal "posts#index", fakeset.routes.to_a[1].defaults[:to] + end + + def test_map_slash + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.get "/", to: "posts#index", as: :main + assert_equal "/", fakeset.asts.first.to_s + end + + def test_map_more_slashes + fakeset = FakeSet.new + mapper = Mapper.new fakeset + + # FIXME: is this a desired behavior? + mapper.get "/one/two/", to: "posts#index", as: :main + assert_equal "/one/two(.:format)", fakeset.asts.first.to_s + end + + def test_map_wildcard + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.get "/*path", to: "pages#show" + assert_equal "/*path(.:format)", fakeset.asts.first.to_s + assert_equal(/.+?/, fakeset.requirements.first[:path]) + end + + def test_map_wildcard_with_other_element + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.get "/*path/foo/:bar", to: "pages#show" + assert_equal "/*path/foo/:bar(.:format)", fakeset.asts.first.to_s + assert_equal(/.+?/, fakeset.requirements.first[:path]) + end + + def test_map_wildcard_with_multiple_wildcard + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.get "/*foo/*bar", to: "pages#show" + assert_equal "/*foo/*bar(.:format)", fakeset.asts.first.to_s + assert_equal(/.+?/, fakeset.requirements.first[:foo]) + assert_equal(/.+?/, fakeset.requirements.first[:bar]) + end + + def test_map_wildcard_with_format_false + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.get "/*path", to: "pages#show", format: false + assert_equal "/*path", fakeset.asts.first.to_s + assert_nil fakeset.requirements.first[:path] + end + + def test_map_wildcard_with_format_true + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.get "/*path", to: "pages#show", format: true + assert_equal "/*path.:format", fakeset.asts.first.to_s + end + + def test_raising_error_when_path_is_not_passed + fakeset = FakeSet.new + mapper = Mapper.new fakeset + app = lambda { |env| [200, {}, [""]] } + assert_raises ArgumentError do + mapper.mount app + end + end + + def test_raising_error_when_rack_app_is_not_passed + fakeset = FakeSet.new + mapper = Mapper.new fakeset + assert_raises ArgumentError do + mapper.mount 10, as: "exciting" + end + + assert_raises ArgumentError do + mapper.mount as: "exciting" + end + end + + def test_scope_does_not_destructively_mutate_default_options + fakeset = FakeSet.new + mapper = Mapper.new fakeset + + frozen = { foo: :bar }.freeze + + assert_nothing_raised do + mapper.scope(defaults: frozen) do + # pass + end + end + end + end + end +end diff --git a/actionpack/test/dispatch/middleware_stack_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb new file mode 100644 index 0000000000..5f43e5a3c5 --- /dev/null +++ b/actionpack/test/dispatch/middleware_stack_test.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class MiddlewareStackTest < ActiveSupport::TestCase + class FooMiddleware; end + class BarMiddleware; end + class BazMiddleware; end + class HiyaMiddleware; end + class BlockMiddleware + attr_reader :block + def initialize(&block) + @block = block + end + end + + def setup + @stack = ActionDispatch::MiddlewareStack.new + @stack.use FooMiddleware + @stack.use BarMiddleware + end + + def test_delete_works + assert_difference "@stack.size", -1 do + @stack.delete FooMiddleware + end + end + + test "use should push middleware as class onto the stack" do + assert_difference "@stack.size" do + @stack.use BazMiddleware + end + assert_equal BazMiddleware, @stack.last.klass + end + + test "use should push middleware class with arguments onto the stack" do + assert_difference "@stack.size" do + @stack.use BazMiddleware, true, foo: "bar" + end + assert_equal BazMiddleware, @stack.last.klass + assert_equal([true, { foo: "bar" }], @stack.last.args) + end + + test "use should push middleware class with block arguments onto the stack" do + proc = Proc.new { } + assert_difference "@stack.size" do + @stack.use(BlockMiddleware, &proc) + end + assert_equal BlockMiddleware, @stack.last.klass + assert_equal proc, @stack.last.block + end + + test "insert inserts middleware at the integer index" do + @stack.insert(1, BazMiddleware) + assert_equal BazMiddleware, @stack[1].klass + end + + test "insert_after inserts middleware after the integer index" do + @stack.insert_after(1, BazMiddleware) + assert_equal BazMiddleware, @stack[2].klass + end + + test "insert_before inserts middleware before another middleware class" do + @stack.insert_before(BarMiddleware, BazMiddleware) + assert_equal BazMiddleware, @stack[1].klass + end + + test "insert_after inserts middleware after another middleware class" do + @stack.insert_after(BarMiddleware, BazMiddleware) + assert_equal BazMiddleware, @stack[2].klass + end + + test "swaps one middleware out for another" do + assert_equal FooMiddleware, @stack[0].klass + @stack.swap(FooMiddleware, BazMiddleware) + assert_equal BazMiddleware, @stack[0].klass + end + + test "swaps one middleware out for same middleware class" do + assert_equal FooMiddleware, @stack[0].klass + @stack.swap(FooMiddleware, FooMiddleware, Proc.new { |env| [500, {}, ["error!"]] }) + assert_equal FooMiddleware, @stack[0].klass + end + + test "unshift adds a new middleware at the beginning of the stack" do + @stack.unshift MiddlewareStackTest::BazMiddleware + assert_equal BazMiddleware, @stack.first.klass + end + + test "raise an error on invalid index" do + assert_raise RuntimeError do + @stack.insert(HiyaMiddleware, BazMiddleware) + end + + assert_raise RuntimeError do + @stack.insert_after(HiyaMiddleware, BazMiddleware) + end + end + + test "can check if Middleware are equal - Class" do + assert_equal @stack.last, BarMiddleware + end + + test "includes a class" do + assert_equal true, @stack.include?(BarMiddleware) + end + + test "can check if Middleware are equal - Middleware" do + assert_equal @stack.last, @stack.last + end + + test "includes a middleware" do + assert_equal true, @stack.include?(ActionDispatch::MiddlewareStack::Middleware.new(BarMiddleware, nil, nil)) + end +end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb new file mode 100644 index 0000000000..45d91883c0 --- /dev/null +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class MimeTypeTest < ActiveSupport::TestCase + test "parse single" do + Mime::LOOKUP.each_key do |mime_type| + unless mime_type == "image/*" + assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type) + end + end + end + + test "unregister" do + assert_nil Mime[:mobile] + + begin + mime = Mime::Type.register("text/x-mobile", :mobile) + assert_equal mime, Mime[:mobile] + assert_equal mime, Mime::Type.lookup("text/x-mobile") + assert_equal mime, Mime::Type.lookup_by_extension(:mobile) + + Mime::Type.unregister(:mobile) + assert_nil Mime[:mobile], "Mime[:mobile] should be nil after unregistering :mobile" + assert_nil Mime::Type.lookup_by_extension(:mobile), "Should be missing MIME extension lookup for :mobile" + ensure + Mime::Type.unregister :mobile + end + end + + test "parse text with trailing star at the beginning" do + accept = "text/*, text/html, application/json, multipart/form-data" + expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]] + parsed = Mime::Type.parse(accept) + assert_equal expect.map(&:to_s), parsed.map(&:to_s) + end + + test "parse text with trailing star in the end" do + accept = "text/html, application/json, multipart/form-data, text/*" + expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml]] + parsed = Mime::Type.parse(accept) + assert_equal expect.map(&:to_s), parsed.map(&:to_s) + end + + test "parse text with trailing star" do + accept = "text/*" + expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json]] + parsed = Mime::Type.parse(accept) + assert_equal expect.map(&:to_s).sort!, parsed.map(&:to_s).sort! + end + + test "parse application with trailing star" do + accept = "application/*" + expect = [Mime[:html], Mime[:js], Mime[:xml], Mime[:rss], Mime[:atom], Mime[:yaml], Mime[:url_encoded_form], Mime[:json], Mime[:pdf], Mime[:zip], Mime[:gzip]] + parsed = Mime::Type.parse(accept) + assert_equal expect.map(&:to_s).sort!, parsed.map(&:to_s).sort! + end + + test "parse without q" do + accept = "text/xml,application/xhtml+xml,text/yaml,application/xml,text/html,image/png,text/plain,application/pdf,*/*" + expect = [Mime[:html], Mime[:xml], Mime[:yaml], Mime[:png], Mime[:text], Mime[:pdf], "*/*"] + assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s) + end + + test "parse with q" do + accept = "text/xml,application/xhtml+xml,text/yaml; q=0.3,application/xml,text/html; q=0.8,image/png,text/plain; q=0.5,application/pdf,*/*; q=0.2" + expect = [Mime[:html], Mime[:xml], Mime[:png], Mime[:pdf], Mime[:text], Mime[:yaml], "*/*"] + assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s) + end + + test "parse single media range with q" do + accept = "text/html;q=0.9" + expect = [Mime[:html]] + assert_equal expect, Mime::Type.parse(accept) + end + + test "parse arbitrary media type parameters" do + accept = 'multipart/form-data; boundary="simple boundary"' + expect = [Mime[:multipart_form]] + assert_equal expect, Mime::Type.parse(accept) + end + + # Accept header send with user HTTP_USER_AGENT: Sunrise/0.42j (Windows XP) + test "parse broken acceptlines" do + accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/*,,*/*;q=0.5" + expect = [Mime[:html], Mime[:xml], "image/*", Mime[:text], "*/*"] + assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s) + end + + # Accept header send with user HTTP_USER_AGENT: Mozilla/4.0 + # (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; InfoPath.1) + test "parse other broken acceptlines" do + accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, , pronto/1.00.00, sslvpn/1.00.00.00, */*" + expect = ["image/gif", "image/x-xbitmap", "image/jpeg", "image/pjpeg", "application/x-shockwave-flash", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/msword", "pronto/1.00.00", "sslvpn/1.00.00.00", "*/*"] + assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s) + end + + test "custom type" do + type = Mime::Type.register("image/foo", :foo) + assert_equal type, Mime[:foo] + ensure + Mime::Type.unregister(:foo) + end + + test "custom type with type aliases" do + Mime::Type.register "text/foobar", :foobar, ["text/foo", "text/bar"] + %w[text/foobar text/foo text/bar].each do |type| + assert_equal Mime[:foobar], type + end + ensure + Mime::Type.unregister(:foobar) + end + + test "register callbacks" do + registered_mimes = [] + Mime::Type.register_callback do |mime| + registered_mimes << mime + end + + mime = Mime::Type.register("text/foo", :foo) + assert_equal [mime], registered_mimes + ensure + Mime::Type.unregister(:foo) + end + + test "custom type with extension aliases" do + Mime::Type.register "text/foobar", :foobar, [], [:foo, "bar"] + %w[foobar foo bar].each do |extension| + assert_equal Mime[:foobar], Mime::EXTENSION_LOOKUP[extension] + end + ensure + Mime::Type.unregister(:foobar) + end + + test "register alias" do + Mime::Type.register_alias "application/xhtml+xml", :foobar + assert_equal Mime[:html], Mime::EXTENSION_LOOKUP["foobar"] + ensure + Mime::Type.unregister(:foobar) + end + + test "type should be equal to symbol" do + assert_equal Mime[:html], "application/xhtml+xml" + assert_equal Mime[:html], :html + end + + test "type convenience methods" do + types = Mime::SET.symbols.uniq - [:iphone] + + types.each do |type| + mime = Mime[type] + assert_respond_to mime, "#{type}?" + assert_equal type, mime.symbol, "#{mime.inspect} is not #{type}?" + invalid_types = types - [type] + invalid_types.delete(:html) + invalid_types.each { |other_type| + assert_not_equal mime.symbol, other_type, "#{mime.inspect} is #{other_type}?" + } + end + end + + test "references gives preference to symbols before strings" do + assert_equal :html, Mime[:html].ref + another = Mime::Type.lookup("foo/bar") + assert_nil another.to_sym + assert_equal "foo/bar", another.ref + end + + test "regexp matcher" do + assert Mime[:js] =~ "text/javascript" + assert Mime[:js] =~ "application/javascript" + assert Mime[:js] !~ "text/html" + assert_not (Mime[:js] !~ "text/javascript") + assert_not (Mime[:js] !~ "application/javascript") + assert Mime[:html] =~ "application/xhtml+xml" + end +end diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb new file mode 100644 index 0000000000..f6cf653980 --- /dev/null +++ b/actionpack/test/dispatch/mount_test.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "rails/engine" + +class TestRoutingMount < ActionDispatch::IntegrationTest + Router = ActionDispatch::Routing::RouteSet.new + + class AppWithRoutes < Rails::Engine + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + end + + # Test for mounting apps that respond to routes, but aren't Rails-like apps. + class SinatraLikeApp + def self.routes; Object.new; end + + def self.call(env) + [200, { "Content-Type" => "text/html" }, ["OK"]] + end + end + + Router.draw do + SprocketsApp = lambda { |env| + [200, { "Content-Type" => "text/html" }, ["#{env["SCRIPT_NAME"]} -- #{env["PATH_INFO"]}"]] + } + + mount SprocketsApp, at: "/sprockets" + mount SprocketsApp => "/shorthand" + + mount SinatraLikeApp, at: "/fakeengine", as: :fake + mount SinatraLikeApp, at: "/getfake", via: :get + + scope "/its_a" do + mount SprocketsApp, at: "/sprocket" + end + + resources :users do + mount AppWithRoutes, at: "/fakeengine", as: :fake_mounted_at_resource + end + + mount SprocketsApp, at: "/", via: :get + end + + APP = RoutedRackApp.new Router + def app + APP + end + + def test_app_name_is_properly_generated_when_engine_is_mounted_in_resources + assert Router.mounted_helpers.method_defined?(:user_fake_mounted_at_resource), + "A mounted helper should be defined with a parent's prefix" + assert Router.named_routes.key?(:user_fake_mounted_at_resource), + "A named route should be defined with a parent's prefix" + end + + def test_mounting_at_root_path + get "/omg" + assert_equal " -- /omg", response.body + end + + def test_mounting_sets_script_name + get "/sprockets/omg" + assert_equal "/sprockets -- /omg", response.body + end + + def test_mounting_works_with_nested_script_name + get "/foo/sprockets/omg", headers: { "SCRIPT_NAME" => "/foo", "PATH_INFO" => "/sprockets/omg" } + assert_equal "/foo/sprockets -- /omg", response.body + end + + def test_mounting_works_with_scope + get "/its_a/sprocket/omg" + assert_equal "/its_a/sprocket -- /omg", response.body + end + + def test_mounting_with_shorthand + get "/shorthand/omg" + assert_equal "/shorthand -- /omg", response.body + end + + def test_mounting_works_with_via + get "/getfake" + assert_equal "OK", response.body + + post "/getfake" + assert_response :not_found + end + + def test_with_fake_engine_does_not_call_invalid_method + get "/fakeengine" + assert_equal "OK", response.body + end +end diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb new file mode 100644 index 0000000000..7a7a201b11 --- /dev/null +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -0,0 +1,463 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "rack/test" +require "rails/engine" + +module TestGenerationPrefix + class Post + extend ActiveModel::Naming + + def to_param + "1" + end + + def self.model_name + klass = +"Post" + def klass.name; self end + + ActiveModel::Name.new(klass) + end + + def to_model; self; end + def persisted?; true; end + end + + class WithMountedEngine < ActionDispatch::IntegrationTest + class BlogEngine < Rails::Engine + routes.draw do + get "/posts/:id", to: "inside_engine_generating#show", as: :post + get "/posts", to: "inside_engine_generating#index", as: :posts + get "/url_to_application", to: "inside_engine_generating#url_to_application" + get "/polymorphic_path_for_engine", to: "inside_engine_generating#polymorphic_path_for_engine" + get "/conflicting_url", to: "inside_engine_generating#conflicting" + get "/foo", to: "never#invoked", as: :named_helper_that_should_be_invoked_only_in_respond_to_test + + get "/relative_path_root", to: redirect("") + get "/relative_path_redirect", to: redirect("foo") + get "/relative_option_root", to: redirect(path: "") + get "/relative_option_redirect", to: redirect(path: "foo") + get "/relative_custom_root", to: redirect { |params, request| "" } + get "/relative_custom_redirect", to: redirect { |params, request| "foo" } + + get "/absolute_path_root", to: redirect("/") + get "/absolute_path_redirect", to: redirect("/foo") + get "/absolute_option_root", to: redirect(path: "/") + get "/absolute_option_redirect", to: redirect(path: "/foo") + get "/absolute_custom_root", to: redirect { |params, request| "/" } + get "/absolute_custom_redirect", to: redirect { |params, request| "/foo" } + end + end + + class RailsApplication < Rails::Engine + routes.draw do + scope "/:omg", omg: "awesome" do + mount BlogEngine => "/blog", :as => "blog_engine" + end + get "/posts/:id", to: "outside_engine_generating#post", as: :post + get "/generate", to: "outside_engine_generating#index" + get "/polymorphic_path_for_app", to: "outside_engine_generating#polymorphic_path_for_app" + get "/polymorphic_path_for_engine", to: "outside_engine_generating#polymorphic_path_for_engine" + get "/polymorphic_with_url_for", to: "outside_engine_generating#polymorphic_with_url_for" + get "/conflicting_url", to: "outside_engine_generating#conflicting" + get "/ivar_usage", to: "outside_engine_generating#ivar_usage" + root to: "outside_engine_generating#index" + end + end + + # force draw + RailsApplication.routes.define_mounted_helper(:main_app) + + class ::InsideEngineGeneratingController < ActionController::Base + include BlogEngine.routes.url_helpers + include RailsApplication.routes.mounted_helpers + + def index + render plain: posts_path + end + + def show + render plain: post_path(id: params[:id]) + end + + def url_to_application + path = main_app.url_for(controller: "outside_engine_generating", + action: "index", + only_path: true) + render plain: path + end + + def polymorphic_path_for_engine + render plain: polymorphic_path(Post.new) + end + + def conflicting + render plain: "engine" + end + end + + class ::OutsideEngineGeneratingController < ActionController::Base + include BlogEngine.routes.mounted_helpers + include RailsApplication.routes.url_helpers + + def index + render plain: blog_engine.post_path(id: 1) + end + + def polymorphic_path_for_engine + render plain: blog_engine.polymorphic_path(Post.new) + end + + def polymorphic_path_for_app + render plain: polymorphic_path(Post.new) + end + + def polymorphic_with_url_for + render plain: blog_engine.url_for(Post.new) + end + + def conflicting + render plain: "application" + end + + def ivar_usage + @blog_engine = "Not the engine route helper" + render plain: blog_engine.post_path(id: 1) + end + end + + class EngineObject + include ActionDispatch::Routing::UrlFor + include BlogEngine.routes.url_helpers + end + + class AppObject + include ActionDispatch::Routing::UrlFor + include RailsApplication.routes.url_helpers + end + + def app + RailsApplication.instance + end + + attr_reader :engine_object, :app_object + + def setup + RailsApplication.routes.default_url_options = {} + @engine_object = EngineObject.new + @app_object = AppObject.new + end + + include BlogEngine.routes.mounted_helpers + + # Inside Engine + test "[ENGINE] generating engine's url use SCRIPT_NAME from request" do + get "/pure-awesomeness/blog/posts/1" + assert_equal "/pure-awesomeness/blog/posts/1", response.body + end + + test "[ENGINE] generating application's url never uses SCRIPT_NAME from request" do + get "/pure-awesomeness/blog/url_to_application" + assert_equal "/generate", response.body + end + + test "[ENGINE] generating engine's url with polymorphic path" do + get "/pure-awesomeness/blog/polymorphic_path_for_engine" + assert_equal "/pure-awesomeness/blog/posts/1", response.body + end + + test "[ENGINE] url_helpers from engine have higher priority than application's url_helpers" do + get "/awesome/blog/conflicting_url" + assert_equal "engine", response.body + end + + test "[ENGINE] relative path root uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_path_root" + verify_redirect "http://www.example.com/awesome/blog" + end + + test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_path_redirect" + verify_redirect "http://www.example.com/awesome/blog/foo" + end + + test "[ENGINE] relative option root uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_option_root" + verify_redirect "http://www.example.com/awesome/blog" + end + + test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_option_redirect" + verify_redirect "http://www.example.com/awesome/blog/foo" + end + + test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_custom_root" + verify_redirect "http://www.example.com/awesome/blog" + end + + test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do + get "/awesome/blog/relative_custom_redirect" + verify_redirect "http://www.example.com/awesome/blog/foo" + end + + test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_path_root" + verify_redirect "http://www.example.com/" + end + + test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_path_redirect" + verify_redirect "http://www.example.com/foo" + end + + test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_option_root" + verify_redirect "http://www.example.com/" + end + + test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_option_redirect" + verify_redirect "http://www.example.com/foo" + end + + test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_custom_root" + verify_redirect "http://www.example.com/" + end + + test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do + get "/awesome/blog/absolute_custom_redirect" + verify_redirect "http://www.example.com/foo" + end + + # Inside Application + test "[APP] generating engine's route includes prefix" do + get "/generate" + assert_equal "/awesome/blog/posts/1", response.body + end + + test "[APP] generating engine's route includes default_url_options[:script_name]" do + RailsApplication.routes.default_url_options = { script_name: "/something" } + get "/generate" + assert_equal "/something/awesome/blog/posts/1", response.body + end + + test "[APP] generating engine's url with polymorphic path" do + get "/polymorphic_path_for_engine" + assert_equal "/awesome/blog/posts/1", response.body + end + + test "polymorphic_path_for_app" do + get "/polymorphic_path_for_app" + assert_equal "/posts/1", response.body + end + + test "[APP] generating engine's url with url_for(@post)" do + get "/polymorphic_with_url_for" + assert_equal "http://www.example.com/awesome/blog/posts/1", response.body + end + + test "[APP] instance variable with same name as engine" do + get "/ivar_usage" + assert_equal "/awesome/blog/posts/1", response.body + end + + # Inside any Object + test "[OBJECT] proxy route should override respond_to?() as expected" do + assert_respond_to blog_engine, :named_helper_that_should_be_invoked_only_in_respond_to_test_path + end + + test "[OBJECT] generating engine's route includes prefix" do + assert_equal "/awesome/blog/posts/1", engine_object.post_path(id: 1) + end + + test "[OBJECT] generating engine's route includes dynamic prefix" do + assert_equal "/pure-awesomeness/blog/posts/3", engine_object.post_path(id: 3, omg: "pure-awesomeness") + end + + test "[OBJECT] generating engine's route includes default_url_options[:script_name]" do + RailsApplication.routes.default_url_options = { script_name: "/something" } + assert_equal "/something/pure-awesomeness/blog/posts/3", engine_object.post_path(id: 3, omg: "pure-awesomeness") + end + + test "[OBJECT] generating application's route" do + assert_equal "/", app_object.root_path + end + + test "[OBJECT] generating application's route includes default_url_options[:script_name]" do + RailsApplication.routes.default_url_options = { script_name: "/something" } + assert_equal "/something/", app_object.root_path + end + + test "[OBJECT] generating application's route includes default_url_options[:trailing_slash]" do + RailsApplication.routes.default_url_options[:trailing_slash] = true + assert_equal "/awesome/blog/posts", engine_object.posts_path + end + + test "[OBJECT] generating engine's route with url_for" do + path = engine_object.url_for(controller: "inside_engine_generating", + action: "show", + only_path: true, + omg: "omg", + id: 1) + assert_equal "/omg/blog/posts/1", path + end + + test "[OBJECT] generating engine's route with named helpers" do + path = engine_object.posts_path + assert_equal "/awesome/blog/posts", path + + path = engine_object.posts_url(host: "example.com") + assert_equal "http://example.com/awesome/blog/posts", path + end + + test "[OBJECT] generating engine's route with polymorphic_url" do + path = engine_object.polymorphic_path(Post.new) + assert_equal "/awesome/blog/posts/1", path + + path = engine_object.polymorphic_url(Post.new, host: "www.example.com") + assert_equal "http://www.example.com/awesome/blog/posts/1", path + end + + private + def verify_redirect(url, status = 301) + assert_equal status, response.status + assert_equal url, response.headers["Location"] + assert_equal expected_redirect_body(url), response.body + end + + def expected_redirect_body(url) + %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>) + end + end + + class EngineMountedAtRoot < ActionDispatch::IntegrationTest + class BlogEngine + def self.routes + @routes ||= begin + routes = ActionDispatch::Routing::RouteSet.new + routes.draw do + get "/posts/:id", to: "posts#show", as: :post + + get "/relative_path_root", to: redirect("") + get "/relative_path_redirect", to: redirect("foo") + get "/relative_option_root", to: redirect(path: "") + get "/relative_option_redirect", to: redirect(path: "foo") + get "/relative_custom_root", to: redirect { |params, request| "" } + get "/relative_custom_redirect", to: redirect { |params, request| "foo" } + + get "/absolute_path_root", to: redirect("/") + get "/absolute_path_redirect", to: redirect("/foo") + get "/absolute_option_root", to: redirect(path: "/") + get "/absolute_option_redirect", to: redirect(path: "/foo") + get "/absolute_custom_root", to: redirect { |params, request| "/" } + get "/absolute_custom_redirect", to: redirect { |params, request| "/foo" } + end + + routes + end + end + + def self.call(env) + env["action_dispatch.routes"] = routes + routes.call(env) + end + end + + class RailsApplication < Rails::Engine + routes.draw do + mount BlogEngine => "/" + end + end + + class ::PostsController < ActionController::Base + include BlogEngine.routes.url_helpers + include RailsApplication.routes.mounted_helpers + + def show + render plain: post_path(id: params[:id]) + end + end + + def app + RailsApplication.instance + end + + test "generating path inside engine" do + get "/posts/1" + assert_equal "/posts/1", response.body + end + + test "[ENGINE] relative path root uses SCRIPT_NAME from request" do + get "/relative_path_root" + verify_redirect "http://www.example.com/" + end + + test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do + get "/relative_path_redirect" + verify_redirect "http://www.example.com/foo" + end + + test "[ENGINE] relative option root uses SCRIPT_NAME from request" do + get "/relative_option_root" + verify_redirect "http://www.example.com/" + end + + test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do + get "/relative_option_redirect" + verify_redirect "http://www.example.com/foo" + end + + test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do + get "/relative_custom_root" + verify_redirect "http://www.example.com/" + end + + test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do + get "/relative_custom_redirect" + verify_redirect "http://www.example.com/foo" + end + + test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do + get "/absolute_path_root" + verify_redirect "http://www.example.com/" + end + + test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do + get "/absolute_path_redirect" + verify_redirect "http://www.example.com/foo" + end + + test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do + get "/absolute_option_root" + verify_redirect "http://www.example.com/" + end + + test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do + get "/absolute_option_redirect" + verify_redirect "http://www.example.com/foo" + end + + test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do + get "/absolute_custom_root" + verify_redirect "http://www.example.com/" + end + + test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do + get "/absolute_custom_redirect" + verify_redirect "http://www.example.com/foo" + end + + private + def verify_redirect(url, status = 301) + assert_equal status, response.status + assert_equal url, response.headers["Location"] + assert_equal expected_redirect_body(url), response.body + end + + def expected_redirect_body(url) + %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>) + end + end +end diff --git a/actionpack/test/dispatch/rack_cache_test.rb b/actionpack/test/dispatch/rack_cache_test.rb new file mode 100644 index 0000000000..86b375a2a8 --- /dev/null +++ b/actionpack/test/dispatch/rack_cache_test.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_dispatch/http/rack_cache" + +class RackCacheMetaStoreTest < ActiveSupport::TestCase + class ReadWriteHash < ::Hash + alias :read :[] + alias :write :[]= + end + + setup do + @store = ActionDispatch::RailsMetaStore.new(ReadWriteHash.new) + end + + test "stuff is deep duped" do + @store.write(:foo, bar: :original) + hash = @store.read(:foo) + hash[:bar] = :changed + hash = @store.read(:foo) + assert_equal :original, hash[:bar] + end +end diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb new file mode 100644 index 0000000000..edc4cd62a3 --- /dev/null +++ b/actionpack/test/dispatch/reloader_test.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ReloaderTest < ActiveSupport::TestCase + teardown do + ActiveSupport::Reloader.reset_callbacks :prepare + ActiveSupport::Reloader.reset_callbacks :complete + end + + class MyBody < Array + def initialize(&block) + @on_close = block + end + + def foo + "foo" + end + + def bar + "bar" + end + + def close + @on_close.call if @on_close + end + end + + def test_prepare_callbacks + a = b = c = nil + reloader.to_prepare { |*args| a = b = c = 1 } + reloader.to_prepare { |*args| b = c = 2 } + reloader.to_prepare { |*args| c = 3 } + + # Ensure to_prepare callbacks are not run when defined + assert_nil a || b || c + + # Run callbacks + call_and_return_body + + assert_equal 1, a + assert_equal 2, b + assert_equal 3, c + end + + def test_returned_body_object_always_responds_to_close + body = call_and_return_body + assert_respond_to body, :close + end + + def test_returned_body_object_always_responds_to_close_even_if_called_twice + body = call_and_return_body + assert_respond_to body, :close + body.close + + body = call_and_return_body + assert_respond_to body, :close + body.close + end + + def test_condition_specifies_when_to_reload + i, j = 0, 0, 0, 0 + + reloader = reloader(lambda { i < 3 }) + reloader.to_prepare { |*args| i += 1 } + reloader.to_complete { |*args| j += 1 } + + app = middleware(lambda { |env| [200, {}, []] }, reloader) + 5.times do + resp = app.call({}) + resp[2].close + end + assert_equal 3, i + assert_equal 3, j + end + + def test_returned_body_object_behaves_like_underlying_object + body = call_and_return_body do + b = MyBody.new + b << "hello" + b << "world" + [200, { "Content-Type" => "text/html" }, b] + end + assert_equal 2, body.size + assert_equal "hello", body[0] + assert_equal "world", body[1] + assert_equal "foo", body.foo + assert_equal "bar", body.bar + end + + def test_it_calls_close_on_underlying_object_when_close_is_called_on_body + close_called = false + body = call_and_return_body do + b = MyBody.new do + close_called = true + end + [200, { "Content-Type" => "text/html" }, b] + end + body.close + assert close_called + end + + def test_returned_body_object_responds_to_all_methods_supported_by_underlying_object + body = call_and_return_body do + [200, { "Content-Type" => "text/html" }, MyBody.new] + end + assert_respond_to body, :size + assert_respond_to body, :each + assert_respond_to body, :foo + assert_respond_to body, :bar + end + + def test_complete_callbacks_are_called_when_body_is_closed + completed = false + reloader.to_complete { completed = true } + + body = call_and_return_body + assert_not completed + + body.close + assert completed + end + + def test_prepare_callbacks_arent_called_when_body_is_closed + prepared = false + reloader.to_prepare { prepared = true } + + body = call_and_return_body + prepared = false + + body.close + assert_not prepared + end + + def test_complete_callbacks_are_called_on_exceptions + completed = false + reloader.to_complete { completed = true } + + begin + call_and_return_body do + raise "error" + end + rescue + end + + assert completed + end + + private + def call_and_return_body(&block) + app = middleware(block || proc { [200, {}, "response"] }) + _, _, body = app.call("rack.input" => StringIO.new("")) + body + end + + def middleware(inner_app, reloader = reloader()) + ActionDispatch::Reloader.new(inner_app, reloader) + end + + def reloader(check = lambda { true }) + @reloader ||= begin + reloader = Class.new(ActiveSupport::Reloader) + reloader.check = check + reloader + end + end +end diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb new file mode 100644 index 0000000000..2a48a12497 --- /dev/null +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class JsonParamsParsingTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + class << self + attr_accessor :last_request_parameters + end + + def parse + self.class.last_request_parameters = request.request_parameters + head :ok + end + end + + def teardown + TestController.last_request_parameters = nil + end + + test "parses json params for application json" do + assert_parses( + { "person" => { "name" => "David" } }, + "{\"person\": {\"name\": \"David\"}}", "CONTENT_TYPE" => "application/json" + ) + end + + test "parses boolean and number json params for application json" do + assert_parses( + { "item" => { "enabled" => false, "count" => 10 } }, + "{\"item\": {\"enabled\": false, \"count\": 10}}", "CONTENT_TYPE" => "application/json" + ) + end + + test "parses json params for application jsonrequest" do + assert_parses( + { "person" => { "name" => "David" } }, + "{\"person\": {\"name\": \"David\"}}", "CONTENT_TYPE" => "application/jsonrequest" + ) + end + + test "does not parse unregistered media types such as application/vnd.api+json" do + assert_parses( + {}, + "{\"person\": {\"name\": \"David\"}}", "CONTENT_TYPE" => "application/vnd.api+json" + ) + end + + test "nils are stripped from collections" do + assert_parses( + { "person" => [] }, + "{\"person\":[null]}", "CONTENT_TYPE" => "application/json" + ) + assert_parses( + { "person" => ["foo"] }, + "{\"person\":[\"foo\",null]}", "CONTENT_TYPE" => "application/json" + ) + assert_parses( + { "person" => [] }, + "{\"person\":[null, null]}", "CONTENT_TYPE" => "application/json" + ) + end + + test "logs error if parsing unsuccessful" do + with_test_routing do + output = StringIO.new + json = "[\"person]\": {\"name\": \"David\"}}" + post "/parse", params: json, headers: { "CONTENT_TYPE" => "application/json", "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => ActiveSupport::Logger.new(output) } + assert_response :bad_request + output.rewind && err = output.read + assert err =~ /Error occurred while parsing request parameters/ + end + end + + test "occurring a parse error if parsing unsuccessful" do + with_test_routing do + $stderr = StringIO.new # suppress the log + json = "[\"person]\": {\"name\": \"David\"}}" + exception = assert_raise(ActionDispatch::Http::Parameters::ParseError) do + post "/parse", params: json, headers: { "CONTENT_TYPE" => "application/json", "action_dispatch.show_exceptions" => false } + end + assert_equal JSON::ParserError, exception.cause.class + assert_equal exception.cause.message, exception.message + ensure + $stderr = STDERR + end + end + + test "raw_post is not empty for JSON request" do + with_test_routing do + post "/parse", params: '{"posts": [{"title": "Post Title"}]}', headers: { "CONTENT_TYPE" => "application/json" } + assert_equal '{"posts": [{"title": "Post Title"}]}', request.raw_post + end + end + + private + def assert_parses(expected, actual, headers = {}) + with_test_routing do + post "/parse", params: actual, headers: headers + assert_response :ok + assert_equal(expected, TestController.last_request_parameters) + end + end + + def with_test_routing + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + post ":action", to: ::JsonParamsParsingTest::TestController + end + end + yield + end + end +end + +class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest + class UsersController < ActionController::Base + wrap_parameters format: :json + + class << self + attr_accessor :last_request_parameters, :last_parameters + end + + def parse + self.class.last_request_parameters = request.request_parameters + self.class.last_parameters = params.to_unsafe_h + head :ok + end + end + + def teardown + UsersController.last_request_parameters = nil + end + + test "parses json params for application json" do + assert_parses( + { "user" => { "username" => "sikachu" }, "username" => "sikachu" }, + "{\"username\": \"sikachu\"}", "CONTENT_TYPE" => "application/json" + ) + end + + test "parses json params for application jsonrequest" do + assert_parses( + { "user" => { "username" => "sikachu" }, "username" => "sikachu" }, + "{\"username\": \"sikachu\"}", "CONTENT_TYPE" => "application/jsonrequest" + ) + end + + test "parses json with non-object JSON content" do + assert_parses( + { "user" => { "_json" => "string content" }, "_json" => "string content" }, + "\"string content\"", "CONTENT_TYPE" => "application/json" + ) + end + + test "parses json params after custom json mime type registered" do + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w(application/vnd.rails+json) + assert_parses( + { "user" => { "username" => "meinac" }, "username" => "meinac" }, + "{\"username\": \"meinac\"}", "CONTENT_TYPE" => "application/json" + ) + ensure + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) + end + + test "parses json params after custom json mime type registered with synonym" do + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w(application/vnd.rails+json) + assert_parses( + { "user" => { "username" => "meinac" }, "username" => "meinac" }, + "{\"username\": \"meinac\"}", "CONTENT_TYPE" => "application/vnd.rails+json" + ) + ensure + Mime::Type.unregister :json + Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) + end + + private + def assert_parses(expected, actual, headers = {}) + with_test_routing(UsersController) do + post "/parse", params: actual, headers: headers + assert_response :ok + assert_equal(expected, UsersController.last_request_parameters) + assert_equal(expected.merge("action" => "parse"), UsersController.last_parameters) + end + end + + def with_test_routing(controller) + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + post ":action", to: controller + end + end + yield + end + end +end diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb new file mode 100644 index 0000000000..da8233c074 --- /dev/null +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class MultipartParamsParsingTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + class << self + attr_accessor :last_request_parameters, :last_parameters + end + + def parse + self.class.last_request_parameters = begin + request.request_parameters + rescue EOFError + {} + end + self.class.last_parameters = request.parameters + head :ok + end + + def read + render plain: "File: #{params[:uploaded_data].read}" + end + end + + FIXTURE_PATH = File.expand_path("../../fixtures/multipart", __dir__) + + def teardown + TestController.last_request_parameters = nil + end + + test "parses single parameter" do + assert_equal({ "foo" => "bar" }, parse_multipart("single_parameter")) + end + + test "parses bracketed parameters" do + assert_equal({ "foo" => { "baz" => "bar" } }, parse_multipart("bracketed_param")) + end + + test "parse single utf8 parameter" do + assert_equal({ "Iñtërnâtiônàlizætiøn_name" => "Iñtërnâtiônàlizætiøn_value" }, + parse_multipart("single_utf8_param"), "request.request_parameters") + assert_equal( + "Iñtërnâtiônàlizætiøn_value", + TestController.last_parameters["Iñtërnâtiônàlizætiøn_name"], "request.parameters") + end + + test "parse bracketed utf8 parameter" do + assert_equal({ "Iñtërnâtiônàlizætiøn_name" => { + "Iñtërnâtiônàlizætiøn_nested_name" => "Iñtërnâtiônàlizætiøn_value" } }, + parse_multipart("bracketed_utf8_param"), "request.request_parameters") + assert_equal( + { "Iñtërnâtiônàlizætiøn_nested_name" => "Iñtërnâtiônàlizætiøn_value" }, + TestController.last_parameters["Iñtërnâtiônàlizætiøn_name"], "request.parameters") + end + + test "parses text file" do + params = parse_multipart("text_file") + assert_equal %w(file foo), params.keys.sort + assert_equal "bar", params["foo"] + + file = params["file"] + assert_equal "file.txt", file.original_filename + assert_equal "text/plain", file.content_type + assert_equal "contents", file.read + end + + test "parses utf8 filename with percent character" do + params = parse_multipart("utf8_filename") + assert_equal %w(file foo), params.keys.sort + assert_equal "bar", params["foo"] + + file = params["file"] + assert_equal "ファイル%名.txt", file.original_filename + assert_equal "text/plain", file.content_type + assert_equal "contents", file.read + end + + test "parses boundary problem file" do + params = parse_multipart("boundary_problem_file") + assert_equal %w(file foo), params.keys.sort + + file = params["file"] + foo = params["foo"] + + assert_equal "file.txt", file.original_filename + assert_equal "text/plain", file.content_type + + assert_equal "bar", foo + end + + test "parses large text file" do + params = parse_multipart("large_text_file") + assert_equal %w(file foo), params.keys.sort + assert_equal "bar", params["foo"] + + file = params["file"] + + assert_equal "file.txt", file.original_filename + assert_equal "text/plain", file.content_type + assert_equal(("a" * 20480), file.read) + end + + test "parses binary file" do + params = parse_multipart("binary_file") + assert_equal %w(file flowers foo), params.keys.sort + assert_equal "bar", params["foo"] + + file = params["file"] + assert_equal "file.csv", file.original_filename + assert_nil file.content_type + assert_equal "contents", file.read + + file = params["flowers"] + assert_equal "flowers.jpg", file.original_filename + assert_equal "image/jpeg", file.content_type + assert_equal 19512, file.size + end + + test "parses mixed files" do + params = parse_multipart("mixed_files") + assert_equal %w(files foo), params.keys.sort + assert_equal "bar", params["foo"] + + # Rack doesn't handle multipart/mixed for us. + files = params["files"] + assert_equal 19756, files.bytesize + end + + test "does not create tempfile if no file has been selected" do + params = parse_multipart("none") + assert_equal %w(submit-name), params.keys.sort + assert_equal "Larry", params["submit-name"] + assert_nil params["files"] + end + + test "parses empty upload file" do + params = parse_multipart("empty") + assert_equal %w(files submit-name), params.keys.sort + assert_equal "Larry", params["submit-name"] + assert params["files"] + assert_equal "", params["files"].read + end + + test "uploads and reads binary file" do + with_test_routing do + fixture = FIXTURE_PATH + "/ruby_on_rails.jpg" + params = { uploaded_data: fixture_file_upload(fixture, "image/jpg") } + post "/read", params: params + end + end + + test "uploads and reads file" do + with_test_routing do + post "/read", params: { uploaded_data: fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain") } + assert_equal "File: Hello", response.body + end + end + + # This can happen in Internet Explorer when redirecting after multipart form submit. + test "does not raise EOFError on GET request with multipart content-type" do + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + get ":action", controller: "multipart_params_parsing_test/test" + end + end + headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" } + get "/parse", headers: headers + assert_response :ok + end + end + + private + def fixture(name) + File.open(File.join(FIXTURE_PATH, name), "rb") do |file| + { "rack.input" => file.read, + "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", + "CONTENT_LENGTH" => file.stat.size.to_s } + end + end + + def parse_multipart(name) + with_test_routing do + headers = fixture(name) + post "/parse", params: headers.delete("rack.input"), headers: headers + assert_response :ok + TestController.last_request_parameters + end + end + + def with_test_routing + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + post ":action", controller: "multipart_params_parsing_test/test" + end + end + yield + end + end +end diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb new file mode 100644 index 0000000000..f9ae5ef4e8 --- /dev/null +++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class QueryStringParsingTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + class << self + attr_accessor :last_query_parameters + end + + def parse + self.class.last_query_parameters = request.query_parameters + head :ok + end + end + class EarlyParse + def initialize(app) + @app = app + end + + def call(env) + # Trigger a Rack parse so that env caches the query params + Rack::Request.new(env).params + @app.call(env) + end + end + + def teardown + TestController.last_query_parameters = nil + end + + test "query string" do + assert_parses( + { "action" => "create_customer", "full_name" => "David Heinemeier Hansson", "customerId" => "1" }, + "action=create_customer&full_name=David%20Heinemeier%20Hansson&customerId=1" + ) + end + + test "deep query string" do + assert_parses( + { "x" => { "y" => { "z" => "10" } } }, + "x[y][z]=10" + ) + end + + test "deep query string with array" do + assert_parses({ "x" => { "y" => { "z" => ["10"] } } }, "x[y][z][]=10") + assert_parses({ "x" => { "y" => { "z" => ["10", "5"] } } }, "x[y][z][]=10&x[y][z][]=5") + end + + test "deep query string with array of hash" do + assert_parses({ "x" => { "y" => [{ "z" => "10" }] } }, "x[y][][z]=10") + assert_parses({ "x" => { "y" => [{ "z" => "10", "w" => "10" }] } }, "x[y][][z]=10&x[y][][w]=10") + assert_parses({ "x" => { "y" => [{ "z" => "10", "v" => { "w" => "10" } }] } }, "x[y][][z]=10&x[y][][v][w]=10") + end + + test "deep query string with array of hashes with one pair" do + assert_parses({ "x" => { "y" => [{ "z" => "10" }, { "z" => "20" }] } }, "x[y][][z]=10&x[y][][z]=20") + end + + test "deep query string with array of hashes with multiple pairs" do + assert_parses( + { "x" => { "y" => [{ "z" => "10", "w" => "a" }, { "z" => "20", "w" => "b" }] } }, + "x[y][][z]=10&x[y][][w]=a&x[y][][z]=20&x[y][][w]=b" + ) + end + + test "query string with nil" do + assert_parses( + { "action" => "create_customer", "full_name" => "" }, + "action=create_customer&full_name=" + ) + end + + test "query string with array" do + assert_parses( + { "action" => "create_customer", "selected" => ["1", "2", "3"] }, + "action=create_customer&selected[]=1&selected[]=2&selected[]=3" + ) + end + + test "query string with amps" do + assert_parses( + { "action" => "create_customer", "name" => "Don't & Does" }, + "action=create_customer&name=Don%27t+%26+Does" + ) + end + + test "query string with many equal" do + assert_parses( + { "action" => "create_customer", "full_name" => "abc=def=ghi" }, + "action=create_customer&full_name=abc=def=ghi" + ) + end + + test "query string without equal" do + assert_parses({ "action" => nil }, "action") + assert_parses({ "action" => { "foo" => nil } }, "action[foo]") + assert_parses({ "action" => { "foo" => { "bar" => nil } } }, "action[foo][bar]") + assert_parses({ "action" => { "foo" => { "bar" => [] } } }, "action[foo][bar][]") + assert_parses({ "action" => { "foo" => [] } }, "action[foo][]") + assert_parses({ "action" => { "foo" => [{ "bar" => nil }] } }, "action[foo][][bar]") + end + + def test_array_parses_without_nil + assert_parses({ "action" => ["1"] }, "action[]=1&action[]") + end + + test "perform_deep_munge" do + old_perform_deep_munge = ActionDispatch::Request::Utils.perform_deep_munge + ActionDispatch::Request::Utils.perform_deep_munge = false + begin + assert_parses({ "action" => nil }, "action") + assert_parses({ "action" => { "foo" => nil } }, "action[foo]") + assert_parses({ "action" => { "foo" => { "bar" => nil } } }, "action[foo][bar]") + assert_parses({ "action" => { "foo" => { "bar" => [nil] } } }, "action[foo][bar][]") + assert_parses({ "action" => { "foo" => [nil] } }, "action[foo][]") + assert_parses({ "action" => { "foo" => [{ "bar" => nil }] } }, "action[foo][][bar]") + assert_parses({ "action" => ["1", nil] }, "action[]=1&action[]") + ensure + ActionDispatch::Request::Utils.perform_deep_munge = old_perform_deep_munge + end + end + + test "query string with empty key" do + assert_parses( + { "action" => "create_customer", "full_name" => "David Heinemeier Hansson" }, + "action=create_customer&full_name=David%20Heinemeier%20Hansson&=Save" + ) + end + + test "query string with many ampersands" do + assert_parses( + { "action" => "create_customer", "full_name" => "David Heinemeier Hansson" }, + "&action=create_customer&&&full_name=David%20Heinemeier%20Hansson" + ) + end + + test "unbalanced query string with array" do + assert_parses( + { "location" => ["1", "2"], "age_group" => ["2"] }, + "location[]=1&location[]=2&age_group[]=2" + ) + end + + test "ambiguous query string returns a bad request" do + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + get ":action", to: ::QueryStringParsingTest::TestController + end + end + + get "/parse", headers: { "QUERY_STRING" => "foo[]=bar&foo[4]=bar" } + assert_response :bad_request + end + end + + private + def assert_parses(expected, actual) + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + get ":action", to: ::QueryStringParsingTest::TestController + end + end + @app = self.class.build_app(set) do |middleware| + middleware.use(EarlyParse) + end + + get "/parse", params: actual + assert_response :ok + assert_equal(expected, ::QueryStringParsingTest::TestController.last_query_parameters) + end + end +end diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb new file mode 100644 index 0000000000..74da2fe7d3 --- /dev/null +++ b/actionpack/test/dispatch/request/session_test.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_dispatch/middleware/session/abstract_store" + +module ActionDispatch + class Request + class SessionTest < ActiveSupport::TestCase + attr_reader :req + + def setup + @req = ActionDispatch::Request.empty + end + + def test_create_adds_itself_to_env + s = Session.create(store, req, {}) + assert_equal s, req.env[Rack::RACK_SESSION] + end + + def test_to_hash + s = Session.create(store, req, {}) + s["foo"] = "bar" + assert_equal "bar", s["foo"] + assert_equal({ "foo" => "bar" }, s.to_hash) + assert_equal({ "foo" => "bar" }, s.to_h) + end + + def test_create_merges_old + s = Session.create(store, req, {}) + s["foo"] = "bar" + + s1 = Session.create(store, req, {}) + assert_not_equal s, s1 + assert_equal "bar", s1["foo"] + end + + def test_find + assert_nil Session.find(req) + + s = Session.create(store, req, {}) + assert_equal s, Session.find(req) + end + + def test_destroy + s = Session.create(store, req, {}) + s["rails"] = "ftw" + + s.destroy + + assert_empty s + end + + def test_keys + s = Session.create(store, req, {}) + s["rails"] = "ftw" + s["adequate"] = "awesome" + assert_equal %w[rails adequate], s.keys + end + + def test_keys_with_deferred_loading + s = Session.create(store_with_data, req, {}) + assert_equal %w[sample_key], s.keys + end + + def test_values + s = Session.create(store, req, {}) + s["rails"] = "ftw" + s["adequate"] = "awesome" + assert_equal %w[ftw awesome], s.values + end + + def test_values_with_deferred_loading + s = Session.create(store_with_data, req, {}) + assert_equal %w[sample_value], s.values + end + + def test_clear + s = Session.create(store, req, {}) + s["rails"] = "ftw" + s["adequate"] = "awesome" + + s.clear + assert_empty(s.values) + end + + def test_update + s = Session.create(store, req, {}) + s["rails"] = "ftw" + + s.update(rails: "awesome") + + assert_equal(["rails"], s.keys) + assert_equal("awesome", s["rails"]) + end + + def test_delete + s = Session.create(store, req, {}) + s["rails"] = "ftw" + + s.delete("rails") + + assert_empty(s.keys) + end + + def test_fetch + session = Session.create(store, req, {}) + + session["one"] = "1" + assert_equal "1", session.fetch(:one) + + assert_equal "2", session.fetch(:two, "2") + assert_nil session.fetch(:two, nil) + + assert_equal "three", session.fetch(:three) { |el| el.to_s } + + assert_raise KeyError do + session.fetch(:three) + end + end + + def test_dig + session = Session.create(store, req, {}) + session["one"] = { "two" => "3" } + + assert_equal "3", session.dig("one", "two") + assert_equal "3", session.dig(:one, "two") + + assert_nil session.dig("three", "two") + assert_nil session.dig("one", "three") + assert_nil session.dig("one", :two) + end + + private + def store + Class.new { + def load_session(env); [1, {}]; end + def session_exists?(env); true; end + def delete_session(env, id, options); 123; end + }.new + end + + def store_with_data + Class.new { + def load_session(env); [1, { "sample_key" => "sample_value" }]; end + def session_exists?(env); true; end + def delete_session(env, id, options); 123; end + }.new + end + end + + class SessionIntegrationTest < ActionDispatch::IntegrationTest + class MySessionApp + def call(env) + request = Rack::Request.new(env) + request.session["hello"] = "Hello from MySessionApp!" + [ 200, {}, ["Hello from MySessionApp!"] ] + end + end + + Router = ActionDispatch::Routing::RouteSet.new + Router.draw do + get "/mysessionapp" => MySessionApp.new + end + + def app + @app ||= RoutedRackApp.new(Router) + end + + def test_session_follows_rack_api_contract_1 + get "/mysessionapp" + assert_response :ok + assert_equal "Hello from MySessionApp!", @response.body + assert_equal "Hello from MySessionApp!", session["hello"] + end + end + end +end diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb new file mode 100644 index 0000000000..9e55a7242e --- /dev/null +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -0,0 +1,181 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + class << self + attr_accessor :last_request_parameters, :last_request_type + end + + def parse + self.class.last_request_parameters = request.request_parameters + head :ok + end + end + + def teardown + TestController.last_request_parameters = nil + end + + test "parses unbalanced query string with array" do + query = "location[]=1&location[]=2&age_group[]=2" + expected = { "location" => ["1", "2"], "age_group" => ["2"] } + assert_parses expected, query + end + + test "parses nested hash" do + query = [ + "note[viewers][viewer][][type]=User", + "note[viewers][viewer][][id]=1", + "note[viewers][viewer][][type]=Group", + "note[viewers][viewer][][id]=2" + ].join("&") + expected = { + "note" => { + "viewers" => { + "viewer" => [ + { "id" => "1", "type" => "User" }, + { "type" => "Group", "id" => "2" } + ] + } + } + } + assert_parses expected, query + end + + test "parses more complex nesting" do + query = [ + "customers[boston][first][name]=David", + "customers[boston][first][url]=http://David", + "customers[boston][second][name]=Allan", + "customers[boston][second][url]=http://Allan", + "something_else=blah", + "something_nil=", + "something_empty=", + "products[first]=Apple Computer", + "products[second]=Pc", + "=Save" + ].join("&") + expected = { + "customers" => { + "boston" => { + "first" => { + "name" => "David", + "url" => "http://David" + }, + "second" => { + "name" => "Allan", + "url" => "http://Allan" + } + } + }, + "something_else" => "blah", + "something_empty" => "", + "something_nil" => "", + "products" => { + "first" => "Apple Computer", + "second" => "Pc" + } + } + assert_parses expected, query + end + + test "parses params with array" do + query = "selected[]=1&selected[]=2&selected[]=3" + expected = { "selected" => ["1", "2", "3"] } + assert_parses expected, query + end + + test "parses params with nil key" do + query = "=&test2=value1" + expected = { "test2" => "value1" } + assert_parses expected, query + end + + test "parses params with array prefix and hashes" do + query = "a[][b][c]=d" + expected = { "a" => [{ "b" => { "c" => "d" } }] } + assert_parses expected, query + end + + test "parses params with complex nesting" do + query = "a[][b][c][][d][]=e" + expected = { "a" => [{ "b" => { "c" => [{ "d" => ["e"] }] } }] } + assert_parses expected, query + end + + test "parses params with file path" do + query = [ + "customers[boston][first][name]=David", + "something_else=blah", + "logo=#{__FILE__}" + ].join("&") + expected = { + "customers" => { + "boston" => { + "first" => { + "name" => "David" + } + } + }, + "something_else" => "blah", + "logo" => __FILE__, + } + assert_parses expected, query + end + + test "parses params with Safari 2 trailing null character" do + query = "selected[]=1&selected[]=2&selected[]=3\0" + expected = { "selected" => ["1", "2", "3"] } + assert_parses expected, query + end + + test "ambiguous params returns a bad request" do + with_test_routing do + post "/parse", params: "foo[]=bar&foo[4]=bar" + assert_response :bad_request + end + end + + private + def with_test_routing + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + post ":action", to: ::UrlEncodedParamsParsingTest::TestController + end + end + yield + end + end + + def assert_parses(expected, actual) + with_test_routing do + post "/parse", params: actual + assert_response :ok + assert_equal expected, TestController.last_request_parameters + assert_utf8 TestController.last_request_parameters + end + end + + def assert_utf8(object) + correct_encoding = Encoding.default_internal + + unless object.is_a?(Hash) + assert_equal correct_encoding, object.encoding, "#{object.inspect} should have been UTF-8" + return + end + + object.each_value do |v| + case v + when Hash + assert_utf8 v + when Array + v.each { |el| assert_utf8 el } + else + assert_utf8 v + end + end + end +end diff --git a/actionpack/test/dispatch/request_id_test.rb b/actionpack/test/dispatch/request_id_test.rb new file mode 100644 index 0000000000..9df4712dab --- /dev/null +++ b/actionpack/test/dispatch/request_id_test.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class RequestIdTest < ActiveSupport::TestCase + test "passing on the request id from the outside" do + assert_equal "external-uu-rid", stub_request("HTTP_X_REQUEST_ID" => "external-uu-rid").request_id + end + + test "ensure that only alphanumeric uurids are accepted" do + assert_equal "X-Hacked-HeaderStuff", stub_request("HTTP_X_REQUEST_ID" => "; X-Hacked-Header: Stuff").request_id + end + + test "accept Apache mod_unique_id format" do + mod_unique_id = "abcxyz@ABCXYZ-0123456789" + assert_equal mod_unique_id, stub_request("HTTP_X_REQUEST_ID" => mod_unique_id).request_id + end + + test "ensure that 255 char limit on the request id is being enforced" do + assert_equal "X" * 255, stub_request("HTTP_X_REQUEST_ID" => "X" * 500).request_id + end + + test "generating a request id when none is supplied" do + assert_match(/\w+-\w+-\w+-\w+-\w+/, stub_request.request_id) + end + + test "uuid alias" do + assert_equal "external-uu-rid", stub_request("HTTP_X_REQUEST_ID" => "external-uu-rid").uuid + end + + private + + def stub_request(env = {}) + ActionDispatch::RequestId.new(lambda { |environment| [ 200, environment, [] ] }).call(env) + ActionDispatch::Request.new(env) + end +end + +class RequestIdResponseTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + def index + head :ok + end + end + + test "request id is passed all the way to the response" do + with_test_route_set do + get "/" + assert_match(/\w+/, @response.headers["X-Request-Id"]) + end + end + + test "request id given on request is passed all the way to the response" do + with_test_route_set do + get "/", headers: { "HTTP_X_REQUEST_ID" => "X" * 500 } + assert_equal "X" * 255, @response.headers["X-Request-Id"] + end + end + + private + + def with_test_route_set + with_routing do |set| + set.draw do + get "/", to: ::RequestIdResponseTest::TestController.action(:index) + end + + @app = self.class.build_app(set) do |middleware| + middleware.use ActionDispatch::RequestId + end + + yield + end + end +end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb new file mode 100644 index 0000000000..9d1246b3a4 --- /dev/null +++ b/actionpack/test/dispatch/request_test.rb @@ -0,0 +1,1272 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class BaseRequestTest < ActiveSupport::TestCase + def setup + @env = { + :ip_spoofing_check => true, + "rack.input" => "foo" + } + @original_tld_length = ActionDispatch::Http::URL.tld_length + end + + def teardown + ActionDispatch::Http::URL.tld_length = @original_tld_length + end + + def url_for(options = {}) + options = { host: "www.example.com" }.merge!(options) + ActionDispatch::Http::URL.url_for(options) + end + + private + def stub_request(env = {}) + ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true + @trusted_proxies ||= nil + ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies) + ActionDispatch::Http::URL.tld_length = env.delete(:tld_length) if env.key?(:tld_length) + + ip_app.call(env) + + env = @env.merge(env) + ActionDispatch::Request.new(env) + end +end + +class RequestUrlFor < BaseRequestTest + test "url_for class method" do + e = assert_raise(ArgumentError) { url_for(host: nil) } + assert_match(/Please provide the :host parameter/, e.message) + + assert_equal "/books", url_for(only_path: true, path: "/books") + + assert_equal "http://www.example.com/books/?q=code", url_for(trailing_slash: true, path: "/books?q=code") + assert_equal "http://www.example.com/books/?spareslashes=////", url_for(trailing_slash: true, path: "/books?spareslashes=////") + + assert_equal "http://www.example.com", url_for + assert_equal "http://api.example.com", url_for(subdomain: "api") + assert_equal "http://example.com", url_for(subdomain: false) + assert_equal "http://www.ror.com", url_for(domain: "ror.com") + assert_equal "http://api.ror.co.uk", url_for(host: "www.ror.co.uk", subdomain: "api", tld_length: 2) + assert_equal "http://www.example.com:8080", url_for(port: 8080) + assert_equal "https://www.example.com", url_for(protocol: "https") + assert_equal "http://www.example.com/docs", url_for(path: "/docs") + assert_equal "http://www.example.com#signup", url_for(anchor: "signup") + assert_equal "http://www.example.com/", url_for(trailing_slash: true) + assert_equal "http://dhh:supersecret@www.example.com", url_for(user: "dhh", password: "supersecret") + assert_equal "http://www.example.com?search=books", url_for(params: { search: "books" }) + assert_equal "http://www.example.com?params=", url_for(params: "") + assert_equal "http://www.example.com?params=1", url_for(params: 1) + end +end + +class RequestIP < BaseRequestTest + test "remote ip" do + request = stub_request "REMOTE_ADDR" => "1.2.3.4" + assert_equal "1.2.3.4", request.remote_ip + + request = stub_request "REMOTE_ADDR" => "1.2.3.4,3.4.5.6" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "REMOTE_ADDR" => "1.2.3.4", + "HTTP_X_FORWARDED_FOR" => "3.4.5.6" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "REMOTE_ADDR" => "127.0.0.1", + "HTTP_X_FORWARDED_FOR" => "3.4.5.6" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6,unknown" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6,172.16.0.1" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6,192.168.0.1" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6,10.0.0.1" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6, 10.0.0.1, 10.0.0.1" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "3.4.5.6,127.0.0.1" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,192.168.0.1" + assert_nil request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "9.9.9.9, 3.4.5.6, 172.31.4.4, 10.0.0.1" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "not_ip_address" + assert_nil request.remote_ip + end + + test "remote ip spoof detection" do + request = stub_request "HTTP_X_FORWARDED_FOR" => "1.1.1.1", + "HTTP_CLIENT_IP" => "2.2.2.2" + e = assert_raise(ActionDispatch::RemoteIp::IpSpoofAttackError) { + request.remote_ip + } + assert_match(/IP spoofing attack/, e.message) + assert_match(/HTTP_X_FORWARDED_FOR="1\.1\.1\.1"/, e.message) + assert_match(/HTTP_CLIENT_IP="2\.2\.2\.2"/, e.message) + end + + test "remote ip with spoof detection disabled" do + request = stub_request "HTTP_X_FORWARDED_FOR" => "1.1.1.1", + "HTTP_CLIENT_IP" => "2.2.2.2", + :ip_spoofing_check => false + assert_equal "1.1.1.1", request.remote_ip + end + + test "remote ip spoof protection ignores private addresses" do + request = stub_request "HTTP_X_FORWARDED_FOR" => "172.17.19.51", + "HTTP_CLIENT_IP" => "172.17.19.51", + "REMOTE_ADDR" => "1.1.1.1", + "HTTP_X_BLUECOAT_VIA" => "de462e07a2db325e" + assert_equal "1.1.1.1", request.remote_ip + end + + test "remote ip v6" do + request = stub_request "REMOTE_ADDR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip + + request = stub_request "REMOTE_ADDR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334" + assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip + + request = stub_request "REMOTE_ADDR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329" + assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip + + request = stub_request "REMOTE_ADDR" => "::1", + "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329" + assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,unknown" + assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,::1" + assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, ::1" + assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,::1" + assert_nil request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334, fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, fc00::, fc01::, fdff" + assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "FE00::, FDFF::" + assert_equal "FE00::", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "not_ip_address" + assert_nil request.remote_ip + end + + test "remote ip v6 spoof detection" do + request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", + "HTTP_CLIENT_IP" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + e = assert_raise(ActionDispatch::RemoteIp::IpSpoofAttackError) { + request.remote_ip + } + assert_match(/IP spoofing attack/, e.message) + assert_match(/HTTP_X_FORWARDED_FOR="fe80:0000:0000:0000:0202:b3ff:fe1e:8329"/, e.message) + assert_match(/HTTP_CLIENT_IP="2001:0db8:85a3:0000:0000:8a2e:0370:7334"/, e.message) + end + + test "remote ip v6 spoof detection disabled" do + request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", + "HTTP_CLIENT_IP" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + :ip_spoofing_check => false + assert_equal "fe80:0000:0000:0000:0202:b3ff:fe1e:8329", request.remote_ip + end + + test "remote ip with user specified trusted proxies String" do + @trusted_proxies = "67.205.106.73" + + request = stub_request "REMOTE_ADDR" => "3.4.5.6", + "HTTP_X_FORWARDED_FOR" => "67.205.106.73" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "REMOTE_ADDR" => "172.16.0.1,67.205.106.73", + "HTTP_X_FORWARDED_FOR" => "67.205.106.73" + assert_equal "67.205.106.73", request.remote_ip + + request = stub_request "REMOTE_ADDR" => "67.205.106.73,3.4.5.6", + "HTTP_X_FORWARDED_FOR" => "67.205.106.73" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "67.205.106.73,unknown" + assert_nil request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "9.9.9.9, 3.4.5.6, 10.0.0.1, 67.205.106.73" + assert_equal "3.4.5.6", request.remote_ip + end + + test "remote ip v6 with user specified trusted proxies String" do + @trusted_proxies = "fe80:0000:0000:0000:0202:b3ff:fe1e:8329" + + request = stub_request "REMOTE_ADDR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329" + assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip + + request = stub_request "REMOTE_ADDR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329" + assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip + + request = stub_request "REMOTE_ADDR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,::1", + "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329" + assert_equal "::1", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "unknown,fe80:0000:0000:0000:0202:b3ff:fe1e:8329" + assert_nil request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334" + assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip + end + + test "remote ip with user specified trusted proxies Regexp" do + @trusted_proxies = /^67\.205\.106\.73$/i + + request = stub_request "REMOTE_ADDR" => "67.205.106.73", + "HTTP_X_FORWARDED_FOR" => "3.4.5.6" + assert_equal "3.4.5.6", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "10.0.0.1, 9.9.9.9, 3.4.5.6, 67.205.106.73" + assert_equal "3.4.5.6", request.remote_ip + end + + test "remote ip v6 with user specified trusted proxies Regexp" do + @trusted_proxies = /^fe80:0000:0000:0000:0202:b3ff:fe1e:8329$/i + + request = stub_request "REMOTE_ADDR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "HTTP_X_FORWARDED_FOR" => "fe80:0000:0000:0000:0202:b3ff:fe1e:8329" + assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip + + request = stub_request "HTTP_X_FORWARDED_FOR" => "2001:0db8:85a3:0000:0000:8a2e:0370:7334, fe80:0000:0000:0000:0202:b3ff:fe1e:8329" + assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip + end + + test "remote ip middleware not present still returns an IP" do + request = stub_request("REMOTE_ADDR" => "127.0.0.1") + assert_equal "127.0.0.1", request.remote_ip + end +end + +class RequestDomain < BaseRequestTest + test "domains" do + request = stub_request "HTTP_HOST" => "192.168.1.200" + assert_nil request.domain + + request = stub_request "HTTP_HOST" => "foo.192.168.1.200" + assert_nil request.domain + + request = stub_request "HTTP_HOST" => "192.168.1.200.com" + assert_equal "200.com", request.domain + + request = stub_request "HTTP_HOST" => "www.rubyonrails.org" + assert_equal "rubyonrails.org", request.domain + + request = stub_request "HTTP_HOST" => "www.rubyonrails.co.uk" + assert_equal "rubyonrails.co.uk", request.domain(2) + + request = stub_request "HTTP_HOST" => "www.rubyonrails.co.uk", :tld_length => 2 + assert_equal "rubyonrails.co.uk", request.domain + end + + test "subdomains" do + request = stub_request "HTTP_HOST" => "foobar.foobar.com" + assert_equal %w( foobar ), request.subdomains + assert_equal "foobar", request.subdomain + + request = stub_request "HTTP_HOST" => "192.168.1.200" + assert_equal [], request.subdomains + assert_equal "", request.subdomain + + request = stub_request "HTTP_HOST" => "foo.192.168.1.200" + assert_equal [], request.subdomains + assert_equal "", request.subdomain + + request = stub_request "HTTP_HOST" => "192.168.1.200.com" + assert_equal %w( 192 168 1 ), request.subdomains + assert_equal "192.168.1", request.subdomain + + request = stub_request "HTTP_HOST" => nil + assert_equal [], request.subdomains + assert_equal "", request.subdomain + + request = stub_request "HTTP_HOST" => "www.rubyonrails.org" + assert_equal %w( www ), request.subdomains + assert_equal "www", request.subdomain + + request = stub_request "HTTP_HOST" => "www.rubyonrails.co.uk" + assert_equal %w( www ), request.subdomains(2) + assert_equal "www", request.subdomain(2) + + request = stub_request "HTTP_HOST" => "dev.www.rubyonrails.co.uk" + assert_equal %w( dev www ), request.subdomains(2) + assert_equal "dev.www", request.subdomain(2) + + request = stub_request "HTTP_HOST" => "dev.www.rubyonrails.co.uk", :tld_length => 2 + assert_equal %w( dev www ), request.subdomains + assert_equal "dev.www", request.subdomain + end +end + +class RequestPort < BaseRequestTest + test "standard_port" do + request = stub_request + assert_equal 80, request.standard_port + + request = stub_request "HTTPS" => "on" + assert_equal 443, request.standard_port + end + + test "standard_port?" do + request = stub_request + assert_not_predicate request, :ssl? + assert_predicate request, :standard_port? + + request = stub_request "HTTPS" => "on" + assert_predicate request, :ssl? + assert_predicate request, :standard_port? + + request = stub_request "HTTP_HOST" => "www.example.org:8080" + assert_not_predicate request, :ssl? + assert_not_predicate request, :standard_port? + + request = stub_request "HTTP_HOST" => "www.example.org:8443", "HTTPS" => "on" + assert_predicate request, :ssl? + assert_not_predicate request, :standard_port? + end + + test "optional port" do + request = stub_request "HTTP_HOST" => "www.example.org:80" + assert_nil request.optional_port + + request = stub_request "HTTP_HOST" => "www.example.org:8080" + assert_equal 8080, request.optional_port + end + + test "port string" do + request = stub_request "HTTP_HOST" => "www.example.org:80" + assert_equal "", request.port_string + + request = stub_request "HTTP_HOST" => "www.example.org:8080" + assert_equal ":8080", request.port_string + end + + test "server port" do + request = stub_request "SERVER_PORT" => "8080" + assert_equal 8080, request.server_port + + request = stub_request "SERVER_PORT" => "80" + assert_equal 80, request.server_port + + request = stub_request "SERVER_PORT" => "" + assert_equal 0, request.server_port + end +end + +class RequestPath < BaseRequestTest + test "full path" do + request = stub_request "SCRIPT_NAME" => "", "PATH_INFO" => "/path/of/some/uri", "QUERY_STRING" => "mapped=1" + assert_equal "/path/of/some/uri?mapped=1", request.fullpath + assert_equal "/path/of/some/uri", request.path_info + + request = stub_request "SCRIPT_NAME" => "", "PATH_INFO" => "/path/of/some/uri" + assert_equal "/path/of/some/uri", request.fullpath + assert_equal "/path/of/some/uri", request.path_info + + request = stub_request "SCRIPT_NAME" => "", "PATH_INFO" => "/" + assert_equal "/", request.fullpath + assert_equal "/", request.path_info + + request = stub_request "SCRIPT_NAME" => "", "PATH_INFO" => "/", "QUERY_STRING" => "m=b" + assert_equal "/?m=b", request.fullpath + assert_equal "/", request.path_info + + request = stub_request "SCRIPT_NAME" => "/hieraki", "PATH_INFO" => "/" + assert_equal "/hieraki/", request.fullpath + assert_equal "/", request.path_info + + request = stub_request "SCRIPT_NAME" => "/collaboration/hieraki", "PATH_INFO" => "/books/edit/2" + assert_equal "/collaboration/hieraki/books/edit/2", request.fullpath + assert_equal "/books/edit/2", request.path_info + + request = stub_request "SCRIPT_NAME" => "/path", "PATH_INFO" => "/of/some/uri", "QUERY_STRING" => "mapped=1" + assert_equal "/path/of/some/uri?mapped=1", request.fullpath + assert_equal "/of/some/uri", request.path_info + end + + test "original_fullpath returns ORIGINAL_FULLPATH" do + request = stub_request("ORIGINAL_FULLPATH" => "/foo?bar") + + path = request.original_fullpath + assert_equal "/foo?bar", path + end + + test "original_url returns url built using ORIGINAL_FULLPATH" do + request = stub_request("ORIGINAL_FULLPATH" => "/foo?bar", + "HTTP_HOST" => "example.org", + "rack.url_scheme" => "http") + + url = request.original_url + assert_equal "http://example.org/foo?bar", url + end + + test "original_fullpath returns fullpath if ORIGINAL_FULLPATH is not present" do + request = stub_request("PATH_INFO" => "/foo", + "QUERY_STRING" => "bar") + + path = request.original_fullpath + assert_equal "/foo?bar", path + end +end + +class RequestHost < BaseRequestTest + test "host without specifying port" do + request = stub_request "HTTP_HOST" => "rubyonrails.org" + assert_equal "rubyonrails.org", request.host_with_port + end + + test "host with default port" do + request = stub_request "HTTP_HOST" => "rubyonrails.org:80" + assert_equal "rubyonrails.org", request.host_with_port + end + + test "host with non default port" do + request = stub_request "HTTP_HOST" => "rubyonrails.org:81" + assert_equal "rubyonrails.org:81", request.host_with_port + end + + test "raw without specifying port" do + request = stub_request "HTTP_HOST" => "rubyonrails.org" + assert_equal "rubyonrails.org", request.raw_host_with_port + end + + test "raw host with default port" do + request = stub_request "HTTP_HOST" => "rubyonrails.org:80" + assert_equal "rubyonrails.org:80", request.raw_host_with_port + end + + test "raw host with non default port" do + request = stub_request "HTTP_HOST" => "rubyonrails.org:81" + assert_equal "rubyonrails.org:81", request.raw_host_with_port + end + + test "proxy request" do + request = stub_request "HTTP_HOST" => "glu.ttono.us:80" + assert_equal "glu.ttono.us", request.host_with_port + end + + test "http host" do + request = stub_request "HTTP_HOST" => "rubyonrails.org:8080" + assert_equal "rubyonrails.org", request.host + assert_equal "rubyonrails.org:8080", request.host_with_port + + request = stub_request "HTTP_X_FORWARDED_HOST" => "www.firsthost.org, www.secondhost.org" + assert_equal "www.secondhost.org", request.host + + request = stub_request "HTTP_X_FORWARDED_HOST" => "", "HTTP_HOST" => "rubyonrails.org" + assert_equal "rubyonrails.org", request.host + end + + test "http host with default port overrides server port" do + request = stub_request "HTTP_HOST" => "rubyonrails.org" + assert_equal "rubyonrails.org", request.host_with_port + end + + test "host with port if http standard port is specified" do + request = stub_request "HTTP_X_FORWARDED_HOST" => "glu.ttono.us:80" + assert_equal "glu.ttono.us", request.host_with_port + end + + test "host with port if https standard port is specified" do + request = stub_request( + "HTTP_X_FORWARDED_PROTO" => "https", + "HTTP_X_FORWARDED_HOST" => "glu.ttono.us:443" + ) + assert_equal "glu.ttono.us", request.host_with_port + end + + test "host if ipv6 reference" do + request = stub_request "HTTP_HOST" => "[2001:1234:5678:9abc:def0::dead:beef]" + assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", request.host + end + + test "host if ipv6 reference with port" do + request = stub_request "HTTP_HOST" => "[2001:1234:5678:9abc:def0::dead:beef]:8008" + assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", request.host + end +end + +class RequestCGI < BaseRequestTest + test "CGI environment variables" do + request = stub_request( + "AUTH_TYPE" => "Basic", + "GATEWAY_INTERFACE" => "CGI/1.1", + "HTTP_ACCEPT" => "*/*", + "HTTP_ACCEPT_CHARSET" => "UTF-8", + "HTTP_ACCEPT_ENCODING" => "gzip, deflate", + "HTTP_ACCEPT_LANGUAGE" => "en", + "HTTP_CACHE_CONTROL" => "no-cache, max-age=0", + "HTTP_FROM" => "googlebot", + "HTTP_HOST" => "glu.ttono.us:8007", + "HTTP_NEGOTIATE" => "trans", + "HTTP_PRAGMA" => "no-cache", + "HTTP_REFERER" => "http://www.google.com/search?q=glu.ttono.us", + "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)", + "PATH_INFO" => "/homepage/", + "PATH_TRANSLATED" => "/home/kevinc/sites/typo/public/homepage/", + "QUERY_STRING" => "", + "REMOTE_ADDR" => "207.7.108.53", + "REMOTE_HOST" => "google.com", + "REMOTE_IDENT" => "kevin", + "REMOTE_USER" => "kevin", + "REQUEST_METHOD" => "GET", + "SCRIPT_NAME" => "/dispatch.fcgi", + "SERVER_NAME" => "glu.ttono.us", + "SERVER_PORT" => "8007", + "SERVER_PROTOCOL" => "HTTP/1.1", + "SERVER_SOFTWARE" => "lighttpd/1.4.5", + ) + + assert_equal "Basic", request.auth_type + assert_equal 0, request.content_length + assert_nil request.content_mime_type + assert_equal "CGI/1.1", request.gateway_interface + assert_equal "*/*", request.accept + assert_equal "UTF-8", request.accept_charset + assert_equal "gzip, deflate", request.accept_encoding + assert_equal "en", request.accept_language + assert_equal "no-cache, max-age=0", request.cache_control + assert_equal "googlebot", request.from + assert_equal "glu.ttono.us", request.host + assert_equal "trans", request.negotiate + assert_equal "no-cache", request.pragma + assert_equal "http://www.google.com/search?q=glu.ttono.us", request.referer + assert_equal "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)", request.user_agent + assert_equal "/homepage/", request.path_info + assert_equal "/home/kevinc/sites/typo/public/homepage/", request.path_translated + assert_equal "", request.query_string + assert_equal "207.7.108.53", request.remote_addr + assert_equal "google.com", request.remote_host + assert_equal "kevin", request.remote_ident + assert_equal "kevin", request.remote_user + assert_equal "GET", request.request_method + assert_equal "/dispatch.fcgi", request.script_name + assert_equal "glu.ttono.us", request.server_name + assert_equal 8007, request.server_port + assert_equal "HTTP/1.1", request.server_protocol + assert_equal "lighttpd", request.server_software + end +end + +class LocalhostTest < BaseRequestTest + test "IPs that match localhost" do + request = stub_request("REMOTE_IP" => "127.1.1.1", "REMOTE_ADDR" => "127.1.1.1") + assert_predicate request, :local? + end +end + +class RequestCookie < BaseRequestTest + test "cookie syntax resilience" do + request = stub_request("HTTP_COOKIE" => "_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes") + assert_equal "c84ace84796670c052c6ceb2451fb0f2", request.cookies["_session_id"], request.cookies.inspect + assert_equal "yes", request.cookies["is_admin"], request.cookies.inspect + + # some Nokia phone browsers omit the space after the semicolon separator. + # some developers have grown accustomed to using comma in cookie values. + request = stub_request("HTTP_COOKIE" => "_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes") + assert_equal "c84ace847", request.cookies["_session_id"], request.cookies.inspect + assert_equal "yes", request.cookies["is_admin"], request.cookies.inspect + end +end + +class RequestParamsParsing < BaseRequestTest + test "doesnt break when content type has charset" do + request = stub_request( + "REQUEST_METHOD" => "POST", + "CONTENT_LENGTH" => "flamenco=love".length, + "CONTENT_TYPE" => "application/x-www-form-urlencoded; charset=utf-8", + "rack.input" => StringIO.new("flamenco=love") + ) + + assert_equal({ "flamenco" => "love" }, request.request_parameters) + end + + test "doesnt interpret request uri as query string when missing" do + request = stub_request("REQUEST_URI" => "foo") + assert_equal({}, request.query_parameters) + end +end + +class RequestRewind < BaseRequestTest + test "body should be rewound" do + data = "rewind" + env = { + "rack.input" => StringIO.new(data), + "CONTENT_LENGTH" => data.length, + "CONTENT_TYPE" => "application/x-www-form-urlencoded; charset=utf-8" + } + + # Read the request body by parsing params. + request = stub_request(env) + request.request_parameters + + # Should have rewound the body. + assert_equal 0, request.body.pos + end + + test "raw_post rewinds rack.input if RAW_POST_DATA is nil" do + request = stub_request( + "rack.input" => StringIO.new("raw"), + "CONTENT_LENGTH" => 3 + ) + assert_equal "raw", request.raw_post + assert_equal "raw", request.env["rack.input"].read + end +end + +class RequestProtocol < BaseRequestTest + test "server software" do + assert_equal "lighttpd", stub_request("SERVER_SOFTWARE" => "lighttpd/1.4.5").server_software + assert_equal "apache", stub_request("SERVER_SOFTWARE" => "Apache3.422").server_software + end + + test "xml http request" do + request = stub_request + + assert_not_predicate request, :xml_http_request? + assert_not_predicate request, :xhr? + + request = stub_request "HTTP_X_REQUESTED_WITH" => "DefinitelyNotAjax1.0" + assert_not_predicate request, :xml_http_request? + assert_not_predicate request, :xhr? + + request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" + assert_predicate request, :xml_http_request? + assert_predicate request, :xhr? + end + + test "reports ssl" do + assert_not_predicate stub_request, :ssl? + assert_predicate stub_request("HTTPS" => "on"), :ssl? + end + + test "reports ssl when proxied via lighttpd" do + assert_predicate stub_request("HTTP_X_FORWARDED_PROTO" => "https"), :ssl? + end + + test "scheme returns https when proxied" do + request = stub_request "rack.url_scheme" => "http" + assert_not_predicate request, :ssl? + assert_equal "http", request.scheme + + request = stub_request( + "rack.url_scheme" => "http", + "HTTP_X_FORWARDED_PROTO" => "https" + ) + assert_predicate request, :ssl? + assert_equal "https", request.scheme + end +end + +class RequestMethod < BaseRequestTest + test "method returns environment's request method when it has not been + overridden by middleware".squish do + + ActionDispatch::Request::HTTP_METHODS.each do |method| + request = stub_request("REQUEST_METHOD" => method) + + assert_equal method, request.method + assert_equal method.underscore.to_sym, request.method_symbol + end + end + + test "allow request method hacking" do + request = stub_request("REQUEST_METHOD" => "POST") + + assert_equal "POST", request.request_method + assert_equal "POST", request.env["REQUEST_METHOD"] + + request.request_method = "GET" + + assert_equal "GET", request.request_method + assert_equal "GET", request.env["REQUEST_METHOD"] + assert_predicate request, :get? + end + + test "invalid http method raises exception" do + assert_raise(ActionController::UnknownHttpMethod) do + stub_request("REQUEST_METHOD" => "RANDOM_METHOD").request_method + end + end + + test "method returns original value of environment request method on POST" do + request = stub_request("rack.methodoverride.original_method" => "POST") + assert_equal "POST", request.method + end + + test "method raises exception on invalid HTTP method" do + assert_raise(ActionController::UnknownHttpMethod) do + stub_request("rack.methodoverride.original_method" => "_RANDOM_METHOD").method + end + + assert_raise(ActionController::UnknownHttpMethod) do + stub_request("REQUEST_METHOD" => "_RANDOM_METHOD").method + end + end + + test "exception on invalid HTTP method unaffected by I18n settings" do + old_locales = I18n.available_locales + old_enforce = I18n.config.enforce_available_locales + + begin + I18n.available_locales = [:nl] + I18n.config.enforce_available_locales = true + assert_raise(ActionController::UnknownHttpMethod) do + stub_request("REQUEST_METHOD" => "_RANDOM_METHOD").method + end + ensure + I18n.available_locales = old_locales + I18n.config.enforce_available_locales = old_enforce + end + end + + test "post masquerading as patch" do + request = stub_request( + "REQUEST_METHOD" => "PATCH", + "rack.methodoverride.original_method" => "POST" + ) + + assert_equal "POST", request.method + assert_equal "PATCH", request.request_method + assert_predicate request, :patch? + end + + test "post masquerading as put" do + request = stub_request( + "REQUEST_METHOD" => "PUT", + "rack.methodoverride.original_method" => "POST" + ) + assert_equal "POST", request.method + assert_equal "PUT", request.request_method + assert_predicate request, :put? + end + + test "post uneffected by local inflections" do + existing_acronyms = ActiveSupport::Inflector.inflections.acronyms.dup + assert_deprecated { ActiveSupport::Inflector.inflections.acronym_regex.dup } + begin + ActiveSupport::Inflector.inflections do |inflect| + inflect.acronym "POS" + end + assert_equal "pos_t", "POST".underscore + request = stub_request "REQUEST_METHOD" => "POST" + assert_equal :post, ActionDispatch::Request::HTTP_METHOD_LOOKUP["POST"] + assert_equal :post, request.method_symbol + assert_predicate request, :post? + ensure + # Reset original acronym set + ActiveSupport::Inflector.inflections do |inflect| + inflect.send(:instance_variable_set, "@acronyms", existing_acronyms) + inflect.send(:define_acronym_regex_patterns) + end + end + end +end + +class RequestFormat < BaseRequestTest + test "xml format" do + request = stub_request "QUERY_STRING" => "format=xml" + + assert_equal Mime[:xml], request.format + end + + test "xhtml format" do + request = stub_request "QUERY_STRING" => "format=xhtml" + + assert_equal Mime[:html], request.format + end + + test "txt format" do + request = stub_request "QUERY_STRING" => "format=txt" + + assert_equal Mime[:text], request.format + end + + test "XMLHttpRequest" do + request = stub_request( + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "HTTP_ACCEPT" => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(","), + "QUERY_STRING" => "" + ) + + assert_predicate request, :xhr? + assert_equal Mime[:js], request.format + end + + test "can override format with parameter negative" do + request = stub_request("QUERY_STRING" => "format=txt") + + assert_not_predicate request.format, :xml? + end + + test "can override format with parameter positive" do + request = stub_request("QUERY_STRING" => "format=xml") + + assert_predicate request.format, :xml? + end + + test "formats text/html with accept header" do + request = stub_request "HTTP_ACCEPT" => "text/html" + assert_equal [Mime[:html]], request.formats + end + + test "formats blank with accept header" do + request = stub_request "HTTP_ACCEPT" => "" + assert_equal [Mime[:html]], request.formats + end + + test "formats XMLHttpRequest with accept header" do + request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" + assert_equal [Mime[:js]], request.formats + end + + test "formats application/xml with accept header" do + request = stub_request("CONTENT_TYPE" => "application/xml; charset=UTF-8", + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest") + assert_equal [Mime[:xml]], request.formats + end + + test "formats format:text with accept header" do + request = stub_request("QUERY_STRING" => "format=txt") + + assert_equal [Mime[:text]], request.formats + end + + test "formats format:unknown with accept header" do + request = stub_request("QUERY_STRING" => "format=unknown") + + assert_instance_of Mime::NullType, request.format + end + + test "format is not nil with unknown format" do + request = stub_request("QUERY_STRING" => "format=hello") + + assert_nil request.format + assert_not_predicate request.format, :html? + assert_not_predicate request.format, :xml? + assert_not_predicate request.format, :json? + end + + test "format does not throw exceptions when malformed parameters" do + request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") + assert request.formats + assert_predicate request.format, :html? + end + + test "formats with xhr request" do + request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "QUERY_STRING" => "" + + assert_equal [Mime[:js]], request.formats + end + + test "ignore_accept_header" do + old_ignore_accept_header = ActionDispatch::Request.ignore_accept_header + ActionDispatch::Request.ignore_accept_header = true + + begin + request = stub_request "HTTP_ACCEPT" => "application/xml", + "QUERY_STRING" => "" + + assert_equal [ Mime[:html] ], request.formats + + request = stub_request "HTTP_ACCEPT" => "koz-asked/something-crazy", + "QUERY_STRING" => "" + + assert_equal [ Mime[:html] ], request.formats + + request = stub_request "HTTP_ACCEPT" => "*/*;q=0.1", + "QUERY_STRING" => "" + + assert_equal [ Mime[:html] ], request.formats + + request = stub_request "HTTP_ACCEPT" => "application/jxw", + "QUERY_STRING" => "" + + assert_equal [ Mime[:html] ], request.formats + + request = stub_request "HTTP_ACCEPT" => "application/xml", + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "QUERY_STRING" => "" + + assert_equal [ Mime[:js] ], request.formats + + request = stub_request "HTTP_ACCEPT" => "application/xml", + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "QUERY_STRING" => "format=json" + + assert_equal [ Mime[:json] ], request.formats + ensure + ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header + end + end + + test "format taken from the path extension" do + request = stub_request "PATH_INFO" => "/foo.xml", "QUERY_STRING" => "" + + assert_equal [Mime[:xml]], request.formats + + request = stub_request "PATH_INFO" => "/foo.123", "QUERY_STRING" => "" + + assert_equal [Mime[:html]], request.formats + end + + test "formats from accept headers have higher precedence than path extension" do + request = stub_request "HTTP_ACCEPT" => "application/json", + "PATH_INFO" => "/foo.xml", + "QUERY_STRING" => "" + + assert_equal [Mime[:json]], request.formats + end +end + +class RequestMimeType < BaseRequestTest + test "content type" do + assert_equal Mime[:html], stub_request("CONTENT_TYPE" => "text/html").content_mime_type + end + + test "no content type" do + assert_nil stub_request.content_mime_type + end + + test "content type is XML" do + assert_equal Mime[:xml], stub_request("CONTENT_TYPE" => "application/xml").content_mime_type + end + + test "content type with charset" do + assert_equal Mime[:xml], stub_request("CONTENT_TYPE" => "application/xml; charset=UTF-8").content_mime_type + end + + test "user agent" do + assert_equal "TestAgent", stub_request("HTTP_USER_AGENT" => "TestAgent").user_agent + end + + test "negotiate_mime" do + request = stub_request( + "HTTP_ACCEPT" => "text/html", + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" + ) + + assert_nil request.negotiate_mime([Mime[:xml], Mime[:json]]) + assert_equal Mime[:html], request.negotiate_mime([Mime[:xml], Mime[:html]]) + assert_equal Mime[:html], request.negotiate_mime([Mime[:xml], Mime::ALL]) + end + + test "negotiate_mime with content_type" do + request = stub_request( + "CONTENT_TYPE" => "application/xml; charset=UTF-8", + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" + ) + + assert_equal Mime[:xml], request.negotiate_mime([Mime[:xml], Mime[:csv]]) + end +end + +class RequestParameters < BaseRequestTest + test "parameters" do + request = stub_request "CONTENT_TYPE" => "application/json", + "CONTENT_LENGTH" => 9, + "RAW_POST_DATA" => '{"foo":1}', + "QUERY_STRING" => "bar=2" + + assert_equal({ "foo" => 1, "bar" => "2" }, request.parameters) + assert_equal({ "foo" => 1 }, request.request_parameters) + assert_equal({ "bar" => "2" }, request.query_parameters) + end + + test "parameters not accessible after rack parse error" do + request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") + + 2.times do + assert_raises(ActionController::BadRequest) do + # rack will raise a Rack::Utils::ParameterTypeError when parsing this query string + request.parameters + end + end + end + + test "path parameters with invalid UTF8 encoding" do + request = stub_request + + err = assert_raises(ActionController::BadRequest) do + request.path_parameters = { foo: "\xBE" } + end + + assert_predicate err.message, :valid_encoding? + assert_equal "Invalid path parameters: Invalid encoding for parameter: �", err.message + end + + test "parameters not accessible after rack parse error of invalid UTF8 character" do + request = stub_request("QUERY_STRING" => "foo%81E=1") + assert_raises(ActionController::BadRequest) { request.parameters } + end + + test "parameters containing an invalid UTF8 character" do + request = stub_request("QUERY_STRING" => "foo=%81E") + assert_raises(ActionController::BadRequest) { request.parameters } + end + + test "parameters containing a deeply nested invalid UTF8 character" do + request = stub_request("QUERY_STRING" => "foo[bar]=%81E") + assert_raises(ActionController::BadRequest) { request.parameters } + end + + test "parameters not accessible after rack parse error 1" do + request = stub_request( + "REQUEST_METHOD" => "POST", + "CONTENT_LENGTH" => "a%=".length, + "CONTENT_TYPE" => "application/x-www-form-urlencoded; charset=utf-8", + "rack.input" => StringIO.new("a%=") + ) + + assert_raises(ActionController::BadRequest) do + # rack will raise a Rack::Utils::ParameterTypeError when parsing this query string + request.parameters + end + end + + test "we have access to the original exception" do + request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") + + e = assert_raises(ActionController::BadRequest) do + # rack will raise a Rack::Utils::ParameterTypeError when parsing this query string + request.parameters + end + + assert_not_nil e.cause + assert_equal e.cause.backtrace, e.backtrace + end +end + +class RequestParameterFilter < BaseRequestTest + test "parameter filter is deprecated" do + assert_deprecated do + ActionDispatch::Http::ParameterFilter.new(["blah"]) + end + end + + test "filtered_parameters returns params filtered" do + request = stub_request( + "action_dispatch.request.parameters" => { + "lifo" => "Pratik", + "amount" => "420", + "step" => "1" + }, + "action_dispatch.parameter_filter" => [:lifo, :amount] + ) + + params = request.filtered_parameters + assert_equal "[FILTERED]", params["lifo"] + assert_equal "[FILTERED]", params["amount"] + assert_equal "1", params["step"] + end + + test "filtered_env filters env as a whole" do + request = stub_request( + "action_dispatch.request.parameters" => { + "amount" => "420", + "step" => "1" + }, + "RAW_POST_DATA" => "yada yada", + "action_dispatch.parameter_filter" => [:lifo, :amount] + ) + request = stub_request(request.filtered_env) + + assert_equal "[FILTERED]", request.raw_post + assert_equal "[FILTERED]", request.params["amount"] + assert_equal "1", request.params["step"] + end + + test "filtered_path returns path with filtered query string" do + %w(; &).each do |sep| + request = stub_request( + "QUERY_STRING" => %w(username=sikachu secret=bd4f21f api_key=b1bc3b3cd352f68d79d7).join(sep), + "PATH_INFO" => "/authenticate", + "action_dispatch.parameter_filter" => [:secret, :api_key] + ) + + path = request.filtered_path + assert_equal %w(/authenticate?username=sikachu secret=[FILTERED] api_key=[FILTERED]).join(sep), path + end + end + + test "filtered_path should not unescape a genuine '[FILTERED]' value" do + request = stub_request( + "QUERY_STRING" => "secret=bd4f21f&genuine=%5BFILTERED%5D", + "PATH_INFO" => "/authenticate", + "action_dispatch.parameter_filter" => [:secret] + ) + + path = request.filtered_path + assert_equal request.script_name + "/authenticate?secret=[FILTERED]&genuine=%5BFILTERED%5D", path + end + + test "filtered_path should preserve duplication of keys in query string" do + request = stub_request( + "QUERY_STRING" => "username=sikachu&secret=bd4f21f&username=fxn", + "PATH_INFO" => "/authenticate", + "action_dispatch.parameter_filter" => [:secret] + ) + + path = request.filtered_path + assert_equal request.script_name + "/authenticate?username=sikachu&secret=[FILTERED]&username=fxn", path + end + + test "filtered_path should ignore searchparts" do + request = stub_request( + "QUERY_STRING" => "secret", + "PATH_INFO" => "/authenticate", + "action_dispatch.parameter_filter" => [:secret] + ) + + path = request.filtered_path + assert_equal request.script_name + "/authenticate?secret", path + end +end + +class RequestEtag < BaseRequestTest + test "always matches *" do + request = stub_request("HTTP_IF_NONE_MATCH" => "*") + + assert_equal "*", request.if_none_match + assert_equal ["*"], request.if_none_match_etags + + assert request.etag_matches?('"strong"') + assert request.etag_matches?('W/"weak"') + assert_not request.etag_matches?(nil) + end + + test "doesn't match absent If-None-Match" do + request = stub_request + + assert_nil request.if_none_match + assert_equal [], request.if_none_match_etags + + assert_not request.etag_matches?("foo") + assert_not request.etag_matches?(nil) + end + + test "matches opaque ETag validators without unquoting" do + header = '"the-etag"' + request = stub_request("HTTP_IF_NONE_MATCH" => header) + + assert_equal header, request.if_none_match + assert_equal ['"the-etag"'], request.if_none_match_etags + + assert request.etag_matches?('"the-etag"') + assert_not request.etag_matches?("the-etag") + end + + test "if_none_match_etags multiple" do + header = 'etag1, etag2, "third etag", "etag4"' + expected = ["etag1", "etag2", '"third etag"', '"etag4"'] + request = stub_request("HTTP_IF_NONE_MATCH" => header) + + assert_equal header, request.if_none_match + assert_equal expected, request.if_none_match_etags + expected.each do |etag| + assert request.etag_matches?(etag), etag + end + end +end + +class RequestVariant < BaseRequestTest + def setup + super + @request = stub_request + end + + test "setting variant to a symbol" do + @request.variant = :phone + + assert_predicate @request.variant, :phone? + assert_not_predicate @request.variant, :tablet? + assert @request.variant.any?(:phone, :tablet) + assert_not @request.variant.any?(:tablet, :desktop) + end + + test "setting variant to an array of symbols" do + @request.variant = [:phone, :tablet] + + assert_predicate @request.variant, :phone? + assert_predicate @request.variant, :tablet? + assert_not_predicate @request.variant, :desktop? + assert @request.variant.any?(:tablet, :desktop) + assert_not @request.variant.any?(:desktop, :watch) + end + + test "clearing variant" do + @request.variant = nil + + assert_empty @request.variant + assert_not_predicate @request.variant, :phone? + assert_not @request.variant.any?(:phone, :tablet) + end + + test "setting variant to a non-symbol value" do + assert_raise ArgumentError do + @request.variant = "phone" + end + end + + test "setting variant to an array containing a non-symbol value" do + assert_raise ArgumentError do + @request.variant = [:phone, "tablet"] + end + end +end + +class RequestFormData < BaseRequestTest + test "media_type is from the FORM_DATA_MEDIA_TYPES array" do + assert_predicate stub_request("CONTENT_TYPE" => "application/x-www-form-urlencoded"), :form_data? + assert_predicate stub_request("CONTENT_TYPE" => "multipart/form-data"), :form_data? + end + + test "media_type is not from the FORM_DATA_MEDIA_TYPES array" do + assert_not_predicate stub_request("CONTENT_TYPE" => "application/xml"), :form_data? + assert_not_predicate stub_request("CONTENT_TYPE" => "multipart/related"), :form_data? + end + + test "no Content-Type header is provided and the request_method is POST" do + request = stub_request("REQUEST_METHOD" => "POST") + + assert_equal "", request.media_type + assert_equal "POST", request.request_method + assert_not_predicate request, :form_data? + end +end + +class EarlyHintsRequestTest < BaseRequestTest + def setup + super + @env["rack.early_hints"] = lambda { |links| links } + @request = stub_request + end + + test "when early hints is set in the env link headers are sent" do + early_hints = @request.send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload") + expected_hints = { "Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload" } + + assert_equal expected_hints, early_hints + end +end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb new file mode 100644 index 0000000000..60817c1c4d --- /dev/null +++ b/actionpack/test/dispatch/response_test.rb @@ -0,0 +1,542 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "timeout" +require "rack/content_length" + +class ResponseTest < ActiveSupport::TestCase + def setup + @response = ActionDispatch::Response.create + @response.request = ActionDispatch::Request.empty + end + + def test_can_wait_until_commit + t = Thread.new { + @response.await_commit + } + @response.commit! + assert_predicate @response, :committed? + assert t.join(0.5) + end + + def test_stream_close + @response.stream.close + assert_predicate @response.stream, :closed? + end + + def test_stream_write + @response.stream.write "foo" + @response.stream.close + assert_equal "foo", @response.body + end + + def test_write_after_close + @response.stream.close + + e = assert_raises(IOError) do + @response.stream.write "omg" + end + assert_equal "closed stream", e.message + end + + def test_each_isnt_called_if_str_body_is_written + # Controller writes and reads response body + each_counter = 0 + @response.body = Object.new.tap { |o| o.singleton_class.define_method(:each) { |&block| each_counter += 1; block.call "foo" } } + @response["X-Foo"] = @response.body + + assert_equal 1, each_counter, "#each was not called once" + + # Build response + status, headers, body = @response.to_a + + assert_equal 200, status + assert_equal "foo", headers["X-Foo"] + assert_equal "foo", body.each.to_a.join + + # Show that #each was not called twice + assert_equal 1, each_counter, "#each was not called once" + end + + def test_set_header_after_read_body_during_action + @response.body + + # set header after the action reads back @response.body + @response["x-header"] = "Best of all possible worlds." + + # the response can be built. + status, headers, body = @response.to_a + assert_equal 200, status + assert_equal "", body.body + + assert_equal "Best of all possible worlds.", headers["x-header"] + end + + def test_read_body_during_action + @response.body = "Hello, World!" + + # even though there's no explicitly set content-type, + assert_nil @response.content_type + + # after the action reads back @response.body, + assert_equal "Hello, World!", @response.body + + # the response can be built. + status, headers, body = @response.to_a + assert_equal 200, status + assert_equal({ + "Content-Type" => "text/html; charset=utf-8" + }, headers) + + parts = [] + body.each { |part| parts << part } + assert_equal ["Hello, World!"], parts + end + + def test_response_body_encoding + body = ["hello".encode(Encoding::UTF_8)] + response = ActionDispatch::Response.new 200, {}, body + response.request = ActionDispatch::Request.empty + assert_equal Encoding::UTF_8, response.body.encoding + end + + def test_response_charset_writer + @response.charset = "utf-16" + assert_equal "utf-16", @response.charset + @response.charset = nil + assert_equal "utf-8", @response.charset + end + + def test_setting_content_type_header_impacts_content_type_method + @response.headers["Content-Type"] = "application/aaron" + assert_equal "application/aaron", @response.content_type + end + + def test_empty_content_type_returns_nil + @response.headers["Content-Type"] = "" + assert_nil @response.content_type + end + + test "simple output" do + @response.body = "Hello, World!" + + status, headers, body = @response.to_a + assert_equal 200, status + assert_equal({ + "Content-Type" => "text/html; charset=utf-8" + }, headers) + + parts = [] + body.each { |part| parts << part } + assert_equal ["Hello, World!"], parts + end + + test "status handled properly in initialize" do + assert_equal 200, ActionDispatch::Response.new("200 OK").status + end + + def test_only_set_charset_still_defaults_to_text_html + response = ActionDispatch::Response.new + response.charset = "utf-16" + _, headers, _ = response.to_a + assert_equal "text/html; charset=utf-16", headers["Content-Type"] + end + + test "utf8 output" do + @response.body = [1090, 1077, 1089, 1090].pack("U*") + + status, headers, _ = @response.to_a + assert_equal 200, status + assert_equal({ + "Content-Type" => "text/html; charset=utf-8" + }, headers) + end + + test "content length" do + [100, 101, 102, 204].each do |c| + @response = ActionDispatch::Response.new + @response.status = c.to_s + @response.set_header "Content-Length", "0" + _, headers, _ = @response.to_a + assert_not headers.has_key?("Content-Length"), "#{c} must not have a Content-Length header field" + end + end + + test "does not contain a message-body" do + [100, 101, 102, 204, 304].each do |c| + @response = ActionDispatch::Response.new + @response.status = c.to_s + @response.body = "Body must not be included" + _, _, body = @response.to_a + assert_empty body, "#{c} must not have a message-body but actually contains #{body}" + end + end + + test "content type" do + [204, 304].each do |c| + @response = ActionDispatch::Response.new + @response.status = c.to_s + _, headers, _ = @response.to_a + assert_not headers.has_key?("Content-Type"), "#{c} should not have Content-Type header" + end + + [200, 302, 404, 500].each do |c| + @response = ActionDispatch::Response.new + @response.status = c.to_s + _, headers, _ = @response.to_a + assert headers.has_key?("Content-Type"), "#{c} did not have Content-Type header" + end + end + + test "does not include Status header" do + @response.status = "200 OK" + _, headers, _ = @response.to_a + assert_not headers.has_key?("Status") + end + + test "response code" do + @response.status = "200 OK" + assert_equal 200, @response.response_code + + @response.status = "200" + assert_equal 200, @response.response_code + + @response.status = 200 + assert_equal 200, @response.response_code + end + + test "code" do + @response.status = "200 OK" + assert_equal "200", @response.code + + @response.status = "200" + assert_equal "200", @response.code + + @response.status = 200 + assert_equal "200", @response.code + end + + test "message" do + @response.status = "200 OK" + assert_equal "OK", @response.message + + @response.status = "200" + assert_equal "OK", @response.message + + @response.status = 200 + assert_equal "OK", @response.message + end + + test "cookies" do + @response.set_cookie("user_name", value: "david", path: "/") + _status, headers, _body = @response.to_a + assert_equal "user_name=david; path=/", headers["Set-Cookie"] + assert_equal({ "user_name" => "david" }, @response.cookies) + end + + test "multiple cookies" do + @response.set_cookie("user_name", value: "david", path: "/") + @response.set_cookie("login", value: "foo&bar", path: "/", expires: Time.utc(2005, 10, 10, 5)) + _status, headers, _body = @response.to_a + assert_equal "user_name=david; path=/\nlogin=foo%26bar; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000", headers["Set-Cookie"] + assert_equal({ "login" => "foo&bar", "user_name" => "david" }, @response.cookies) + end + + test "delete cookies" do + @response.set_cookie("user_name", value: "david", path: "/") + @response.set_cookie("login", value: "foo&bar", path: "/", expires: Time.utc(2005, 10, 10, 5)) + @response.delete_cookie("login") + assert_equal({ "user_name" => "david", "login" => nil }, @response.cookies) + end + + test "read ETag and Cache-Control" do + resp = ActionDispatch::Response.new.tap { |response| + response.cache_control[:public] = true + response.etag = "123" + response.body = "Hello" + } + resp.to_a + + assert_predicate resp, :etag? + assert_predicate resp, :weak_etag? + assert_not_predicate resp, :strong_etag? + assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.etag) + assert_equal({ public: true }, resp.cache_control) + + assert_equal("public", resp.headers["Cache-Control"]) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.headers["ETag"]) + end + + test "read strong ETag" do + resp = ActionDispatch::Response.new.tap { |response| + response.cache_control[:public] = true + response.strong_etag = "123" + response.body = "Hello" + } + resp.to_a + + assert_predicate resp, :etag? + assert_not_predicate resp, :weak_etag? + assert_predicate resp, :strong_etag? + assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag) + end + + test "read charset and content type" do + resp = ActionDispatch::Response.new.tap { |response| + response.charset = "utf-16" + response.content_type = Mime[:xml] + response.body = "Hello" + } + resp.to_a + + assert_equal("utf-16", resp.charset) + assert_equal(Mime[:xml], resp.content_type) + + assert_equal("application/xml; charset=utf-16", resp.headers["Content-Type"]) + end + + test "read content type with default charset utf-8" do + resp = ActionDispatch::Response.new(200, "Content-Type" => "text/xml") + assert_equal("utf-8", resp.charset) + end + + test "read content type with charset utf-16" do + original = ActionDispatch::Response.default_charset + begin + ActionDispatch::Response.default_charset = "utf-16" + resp = ActionDispatch::Response.new(200, "Content-Type" => "text/xml") + assert_equal("utf-16", resp.charset) + ensure + ActionDispatch::Response.default_charset = original + end + end + + test "read x_frame_options, x_content_type_options, x_xss_protection, x_download_options and x_permitted_cross_domain_policies, referrer_policy" do + original_default_headers = ActionDispatch::Response.default_headers + begin + ActionDispatch::Response.default_headers = { + "X-Frame-Options" => "DENY", + "X-Content-Type-Options" => "nosniff", + "X-XSS-Protection" => "1;", + "X-Download-Options" => "noopen", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "strict-origin-when-cross-origin" + } + resp = ActionDispatch::Response.create.tap { |response| + response.body = "Hello" + } + resp.to_a + + assert_equal("DENY", resp.headers["X-Frame-Options"]) + assert_equal("nosniff", resp.headers["X-Content-Type-Options"]) + assert_equal("1;", resp.headers["X-XSS-Protection"]) + assert_equal("noopen", resp.headers["X-Download-Options"]) + assert_equal("none", resp.headers["X-Permitted-Cross-Domain-Policies"]) + assert_equal("strict-origin-when-cross-origin", resp.headers["Referrer-Policy"]) + ensure + ActionDispatch::Response.default_headers = original_default_headers + end + end + + test "read custom default_header" do + original_default_headers = ActionDispatch::Response.default_headers + begin + ActionDispatch::Response.default_headers = { + "X-XX-XXXX" => "Here is my phone number" + } + resp = ActionDispatch::Response.create.tap { |response| + response.body = "Hello" + } + resp.to_a + + assert_equal("Here is my phone number", resp.headers["X-XX-XXXX"]) + ensure + ActionDispatch::Response.default_headers = original_default_headers + end + end + + test "respond_to? accepts include_private" do + assert_not_respond_to @response, :method_missing + assert @response.respond_to?(:method_missing, true) + end + + test "can be explicitly destructured into status, headers and an enumerable body" do + response = ActionDispatch::Response.new(404, { "Content-Type" => "text/plain" }, ["Not Found"]) + response.request = ActionDispatch::Request.empty + status, headers, body = *response + + assert_equal 404, status + assert_equal({ "Content-Type" => "text/plain" }, headers) + assert_equal ["Not Found"], body.each.to_a + end + + test "[response.to_a].flatten does not recurse infinitely" do + Timeout.timeout(1) do # use a timeout to prevent it stalling indefinitely + status, headers, body = [@response.to_a].flatten + assert_equal @response.status, status + assert_equal @response.headers, headers + assert_equal @response.body, body.each.to_a.join + end + end + + test "compatibility with Rack::ContentLength" do + @response.body = "Hello" + app = lambda { |env| @response.to_a } + env = Rack::MockRequest.env_for("/") + + _status, headers, _body = app.call(env) + assert_nil headers["Content-Length"] + + _status, headers, _body = Rack::ContentLength.new(app).call(env) + assert_equal "5", headers["Content-Length"] + end +end + +class ResponseHeadersTest < ActiveSupport::TestCase + def setup + @response = ActionDispatch::Response.create + @response.set_header "Foo", "1" + end + + test "has_header?" do + assert @response.has_header? "Foo" + assert_not @response.has_header? "foo" + assert_not @response.has_header? nil + end + + test "get_header" do + assert_equal "1", @response.get_header("Foo") + assert_nil @response.get_header("foo") + assert_nil @response.get_header(nil) + end + + test "set_header" do + assert_equal "2", @response.set_header("Foo", "2") + assert @response.has_header?("Foo") + assert_equal "2", @response.get_header("Foo") + + assert_nil @response.set_header("Foo", nil) + assert @response.has_header?("Foo") + assert_nil @response.get_header("Foo") + end + + test "delete_header" do + assert_nil @response.delete_header(nil) + + assert_nil @response.delete_header("foo") + assert @response.has_header?("Foo") + + assert_equal "1", @response.delete_header("Foo") + assert_not @response.has_header?("Foo") + end + + test "add_header" do + # Add a value to an existing header + assert_equal "1,2", @response.add_header("Foo", "2") + assert_equal "1,2", @response.get_header("Foo") + + # Add nil to an existing header + assert_equal "1,2", @response.add_header("Foo", nil) + assert_equal "1,2", @response.get_header("Foo") + + # Add nil to a nonexistent header + assert_nil @response.add_header("Bar", nil) + assert_not @response.has_header?("Bar") + assert_nil @response.get_header("Bar") + + # Add a value to a nonexistent header + assert_equal "1", @response.add_header("Bar", "1") + assert @response.has_header?("Bar") + assert_equal "1", @response.get_header("Bar") + end +end + +class ResponseIntegrationTest < ActionDispatch::IntegrationTest + test "response cache control from railsish app" do + @app = lambda { |env| + ActionDispatch::Response.new.tap { |resp| + resp.cache_control[:public] = true + resp.etag = "123" + resp.body = "Hello" + resp.request = ActionDispatch::Request.empty + }.to_a + } + + get "/" + assert_response :success + + assert_equal("public", @response.headers["Cache-Control"]) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"]) + + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag) + assert_equal({ public: true }, @response.cache_control) + end + + test "response cache control from rackish app" do + @app = lambda { |env| + [200, + { "ETag" => 'W/"202cb962ac59075b964b07152d234b70"', + "Cache-Control" => "public" }, ["Hello"]] + } + + get "/" + assert_response :success + + assert_equal("public", @response.headers["Cache-Control"]) + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"]) + + assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag) + assert_equal({ public: true }, @response.cache_control) + end + + test "response charset and content type from railsish app" do + @app = lambda { |env| + ActionDispatch::Response.new.tap { |resp| + resp.charset = "utf-16" + resp.content_type = Mime[:xml] + resp.body = "Hello" + resp.request = ActionDispatch::Request.empty + }.to_a + } + + get "/" + assert_response :success + + assert_equal("utf-16", @response.charset) + assert_equal(Mime[:xml], @response.content_type) + + assert_equal("application/xml; charset=utf-16", @response.headers["Content-Type"]) + end + + test "response charset and content type from rackish app" do + @app = lambda { |env| + [200, + { "Content-Type" => "application/xml; charset=utf-16" }, + ["Hello"]] + } + + get "/" + assert_response :success + + assert_equal("utf-16", @response.charset) + assert_equal(Mime[:xml], @response.content_type) + + assert_equal("application/xml; charset=utf-16", @response.headers["Content-Type"]) + end + + test "strong ETag validator" do + @app = lambda { |env| + ActionDispatch::Response.new.tap { |resp| + resp.strong_etag = "123" + resp.body = "Hello" + resp.request = ActionDispatch::Request.empty + }.to_a + } + + get "/" + assert_response :ok + + assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"]) + assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag) + end +end diff --git a/actionpack/test/dispatch/routing/concerns_test.rb b/actionpack/test/dispatch/routing/concerns_test.rb new file mode 100644 index 0000000000..503a7ccd56 --- /dev/null +++ b/actionpack/test/dispatch/routing/concerns_test.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ReviewsController < ResourcesController; end + +class RoutingConcernsTest < ActionDispatch::IntegrationTest + class Reviewable + def self.call(mapper, options = {}) + mapper.resources :reviews, options + end + end + + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + concern :commentable do |options| + resources :comments, options + end + + concern :image_attachable do + resources :images, only: :index + end + + concern :reviewable, Reviewable + + resources :posts, concerns: [:commentable, :image_attachable, :reviewable] do + resource :video, concerns: :commentable do + concerns :reviewable, as: :video_reviews + end + end + + resource :picture, concerns: :commentable do + resources :posts, concerns: :commentable + end + + scope "/videos" do + concerns :commentable, except: :destroy + end + end + end + + include Routes.url_helpers + APP = RoutedRackApp.new Routes + def app; APP end + + def test_accessing_concern_from_resources + get "/posts/1/comments" + assert_equal "200", @response.code + assert_equal "/posts/1/comments", post_comments_path(post_id: 1) + end + + def test_accessing_concern_from_resource + get "/picture/comments" + assert_equal "200", @response.code + assert_equal "/picture/comments", picture_comments_path + end + + def test_accessing_concern_from_nested_resource + get "/posts/1/video/comments" + assert_equal "200", @response.code + assert_equal "/posts/1/video/comments", post_video_comments_path(post_id: 1) + end + + def test_accessing_concern_from_nested_resources + get "/picture/posts/1/comments" + assert_equal "200", @response.code + assert_equal "/picture/posts/1/comments", picture_post_comments_path(post_id: 1) + end + + def test_accessing_concern_from_resources_with_more_than_one_concern + get "/posts/1/images" + assert_equal "200", @response.code + assert_equal "/posts/1/images", post_images_path(post_id: 1) + end + + def test_accessing_concern_from_resources_using_only_option + get "/posts/1/image/1" + assert_equal "404", @response.code + end + + def test_accessing_callable_concern_ + get "/posts/1/reviews/1" + assert_equal "200", @response.code + assert_equal "/posts/1/reviews/1", post_review_path(post_id: 1, id: 1) + end + + def test_callable_concerns_accept_options + get "/posts/1/video/reviews/1" + assert_equal "200", @response.code + assert_equal "/posts/1/video/reviews/1", post_video_video_review_path(post_id: 1, id: 1) + end + + def test_accessing_concern_from_a_scope + get "/videos/comments" + assert_equal "200", @response.code + end + + def test_concerns_accept_options + delete "/videos/comments/1" + assert_equal "404", @response.code + end + + def test_with_an_invalid_concern_name + e = assert_raise ArgumentError do + ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + resources :posts, concerns: :foo + end + end + end + + assert_equal "No concern named foo was found!", e.message + end + + def test_concerns_executes_block_in_context_of_current_mapper + mapper = ActionDispatch::Routing::Mapper.new(ActionDispatch::Routing::RouteSet.new) + mapper.concern :test_concern do + resources :things + return self + end + + assert_equal mapper, mapper.concerns(:test_concern) + end +end diff --git a/actionpack/test/dispatch/routing/custom_url_helpers_test.rb b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb new file mode 100644 index 0000000000..a1a1e79884 --- /dev/null +++ b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb @@ -0,0 +1,333 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class TestCustomUrlHelpers < ActionDispatch::IntegrationTest + class Linkable + attr_reader :id + + def self.name + super.demodulize + end + + def initialize(id) + @id = id + end + + def linkable_type + self.class.name.underscore + end + end + + class Category < Linkable; end + class Collection < Linkable; end + class Product < Linkable; end + class Manufacturer < Linkable; end + + class Model + extend ActiveModel::Naming + include ActiveModel::Conversion + + attr_reader :id + + def initialize(id = nil) + @id = id + end + + remove_method :model_name + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, self.class.name.demodulize) + end + + def persisted? + false + end + end + + class Basket < Model; end + class User < Model; end + class Video < Model; end + + class Article + attr_reader :id + + def self.name + "Article" + end + + def initialize(id) + @id = id + end + end + + class Page + attr_reader :id + + def self.name + super.demodulize + end + + def initialize(id) + @id = id + end + end + + class CategoryPage < Page; end + class ProductPage < Page; end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + default_url_options host: "www.example.com" + + root to: "pages#index" + get "/basket", to: "basket#show", as: :basket + get "/posts/:id", to: "posts#show", as: :post + get "/profile", to: "users#profile", as: :profile + get "/media/:id", to: "media#show", as: :media + get "/pages/:id", to: "pages#show", as: :page + + resources :categories, :collections, :products, :manufacturers + + namespace :admin do + get "/dashboard", to: "dashboard#index" + end + + direct(:website) { "http://www.rubyonrails.org" } + direct("string") { "http://www.rubyonrails.org" } + direct(:helper) { basket_url } + direct(:linkable) { |linkable| [:"#{linkable.linkable_type}", { id: linkable.id }] } + direct(:nested) { |linkable| route_for(:linkable, linkable) } + direct(:params) { |params| params } + direct(:symbol) { :basket } + direct(:hash) { { controller: "basket", action: "show" } } + direct(:array) { [:admin, :dashboard] } + direct(:options) { |options| [:products, options] } + direct(:defaults, size: 10) { |options| [:products, options] } + + direct(:browse, page: 1, size: 10) do |options| + [:products, options.merge(params.permit(:page, :size).to_h.symbolize_keys)] + end + + resolve("Article") { |article| [:post, { id: article.id }] } + resolve("Basket") { |basket| [:basket] } + resolve("Manufacturer") { |manufacturer| route_for(:linkable, manufacturer) } + resolve("User", anchor: "details") { |user, options| [:profile, options] } + resolve("Video") { |video| [:media, { id: video.id }] } + resolve(%w[Page CategoryPage ProductPage]) { |page| [:page, { id: page.id }] } + end + + APP = build_app Routes + + def app + APP + end + + include Routes.url_helpers + + def setup + @category = Category.new("1") + @collection = Collection.new("2") + @product = Product.new("3") + @manufacturer = Manufacturer.new("apple") + @basket = Basket.new + @user = User.new + @video = Video.new("4") + @article = Article.new("5") + @page = Page.new("6") + @category_page = CategoryPage.new("7") + @product_page = ProductPage.new("8") + @path_params = { "controller" => "pages", "action" => "index" } + @unsafe_params = ActionController::Parameters.new(@path_params) + @safe_params = ActionController::Parameters.new(@path_params).permit(:controller, :action) + end + + def params + ActionController::Parameters.new(page: 2, size: 25) + end + + def test_direct_paths + assert_equal "/", website_path + assert_equal "/", Routes.url_helpers.website_path + + assert_equal "/", string_path + assert_equal "/", Routes.url_helpers.string_path + + assert_equal "/basket", helper_path + assert_equal "/basket", Routes.url_helpers.helper_path + + assert_equal "/categories/1", linkable_path(@category) + assert_equal "/categories/1", Routes.url_helpers.linkable_path(@category) + assert_equal "/collections/2", linkable_path(@collection) + assert_equal "/collections/2", Routes.url_helpers.linkable_path(@collection) + assert_equal "/products/3", linkable_path(@product) + assert_equal "/products/3", Routes.url_helpers.linkable_path(@product) + + assert_equal "/categories/1", nested_path(@category) + assert_equal "/categories/1", Routes.url_helpers.nested_path(@category) + + assert_equal "/", params_path(@safe_params) + assert_equal "/", Routes.url_helpers.params_path(@safe_params) + assert_raises(ActionController::UnfilteredParameters) { params_path(@unsafe_params) } + assert_raises(ActionController::UnfilteredParameters) { Routes.url_helpers.params_path(@unsafe_params) } + + assert_equal "/basket", symbol_path + assert_equal "/basket", Routes.url_helpers.symbol_path + assert_equal "/basket", hash_path + assert_equal "/basket", Routes.url_helpers.hash_path + assert_equal "/admin/dashboard", array_path + assert_equal "/admin/dashboard", Routes.url_helpers.array_path + + assert_equal "/products?page=2", options_path(page: 2) + assert_equal "/products?page=2", Routes.url_helpers.options_path(page: 2) + assert_equal "/products?size=10", defaults_path + assert_equal "/products?size=10", Routes.url_helpers.defaults_path + assert_equal "/products?size=20", defaults_path(size: 20) + assert_equal "/products?size=20", Routes.url_helpers.defaults_path(size: 20) + + assert_equal "/products?page=2&size=25", browse_path + assert_raises(NameError) { Routes.url_helpers.browse_path } + end + + def test_direct_urls + assert_equal "http://www.rubyonrails.org", website_url + assert_equal "http://www.rubyonrails.org", Routes.url_helpers.website_url + + assert_equal "http://www.rubyonrails.org", string_url + assert_equal "http://www.rubyonrails.org", Routes.url_helpers.string_url + + assert_equal "http://www.example.com/basket", helper_url + assert_equal "http://www.example.com/basket", Routes.url_helpers.helper_url + + assert_equal "http://www.example.com/categories/1", linkable_url(@category) + assert_equal "http://www.example.com/categories/1", Routes.url_helpers.linkable_url(@category) + assert_equal "http://www.example.com/collections/2", linkable_url(@collection) + assert_equal "http://www.example.com/collections/2", Routes.url_helpers.linkable_url(@collection) + assert_equal "http://www.example.com/products/3", linkable_url(@product) + assert_equal "http://www.example.com/products/3", Routes.url_helpers.linkable_url(@product) + + assert_equal "http://www.example.com/categories/1", nested_url(@category) + assert_equal "http://www.example.com/categories/1", Routes.url_helpers.nested_url(@category) + + assert_equal "http://www.example.com/", params_url(@safe_params) + assert_equal "http://www.example.com/", Routes.url_helpers.params_url(@safe_params) + assert_raises(ActionController::UnfilteredParameters) { params_url(@unsafe_params) } + assert_raises(ActionController::UnfilteredParameters) { Routes.url_helpers.params_url(@unsafe_params) } + + assert_equal "http://www.example.com/basket", symbol_url + assert_equal "http://www.example.com/basket", Routes.url_helpers.symbol_url + assert_equal "http://www.example.com/basket", hash_url + assert_equal "http://www.example.com/basket", Routes.url_helpers.hash_url + assert_equal "http://www.example.com/admin/dashboard", array_url + assert_equal "http://www.example.com/admin/dashboard", Routes.url_helpers.array_url + + assert_equal "http://www.example.com/products?page=2", options_url(page: 2) + assert_equal "http://www.example.com/products?page=2", Routes.url_helpers.options_url(page: 2) + assert_equal "http://www.example.com/products?size=10", defaults_url + assert_equal "http://www.example.com/products?size=10", Routes.url_helpers.defaults_url + assert_equal "http://www.example.com/products?size=20", defaults_url(size: 20) + assert_equal "http://www.example.com/products?size=20", Routes.url_helpers.defaults_url(size: 20) + + assert_equal "http://www.example.com/products?page=2&size=25", browse_url + assert_raises(NameError) { Routes.url_helpers.browse_url } + end + + def test_resolve_paths + assert_equal "/basket", polymorphic_path(@basket) + assert_equal "/basket", Routes.url_helpers.polymorphic_path(@basket) + + assert_equal "/profile#details", polymorphic_path(@user) + assert_equal "/profile#details", Routes.url_helpers.polymorphic_path(@user) + + assert_equal "/profile#password", polymorphic_path(@user, anchor: "password") + assert_equal "/profile#password", Routes.url_helpers.polymorphic_path(@user, anchor: "password") + + assert_equal "/media/4", polymorphic_path(@video) + assert_equal "/media/4", Routes.url_helpers.polymorphic_path(@video) + assert_equal "/media/4", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @video) + + assert_equal "/posts/5", polymorphic_path(@article) + assert_equal "/posts/5", Routes.url_helpers.polymorphic_path(@article) + assert_equal "/posts/5", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @article) + + assert_equal "/pages/6", polymorphic_path(@page) + assert_equal "/pages/6", Routes.url_helpers.polymorphic_path(@page) + assert_equal "/pages/6", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @page) + + assert_equal "/pages/7", polymorphic_path(@category_page) + assert_equal "/pages/7", Routes.url_helpers.polymorphic_path(@category_page) + assert_equal "/pages/7", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @category_page) + + assert_equal "/pages/8", polymorphic_path(@product_page) + assert_equal "/pages/8", Routes.url_helpers.polymorphic_path(@product_page) + assert_equal "/pages/8", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @product_page) + + assert_equal "/manufacturers/apple", polymorphic_path(@manufacturer) + assert_equal "/manufacturers/apple", Routes.url_helpers.polymorphic_path(@manufacturer) + end + + def test_resolve_urls + assert_equal "http://www.example.com/basket", polymorphic_url(@basket) + assert_equal "http://www.example.com/basket", Routes.url_helpers.polymorphic_url(@basket) + assert_equal "http://www.example.com/basket", polymorphic_url(@basket) + assert_equal "http://www.example.com/basket", Routes.url_helpers.polymorphic_url(@basket) + + assert_equal "http://www.example.com/profile#details", polymorphic_url(@user) + assert_equal "http://www.example.com/profile#details", Routes.url_helpers.polymorphic_url(@user) + + assert_equal "http://www.example.com/profile#password", polymorphic_url(@user, anchor: "password") + assert_equal "http://www.example.com/profile#password", Routes.url_helpers.polymorphic_url(@user, anchor: "password") + + assert_equal "http://www.example.com/media/4", polymorphic_url(@video) + assert_equal "http://www.example.com/media/4", Routes.url_helpers.polymorphic_url(@video) + assert_equal "http://www.example.com/media/4", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @video) + + assert_equal "http://www.example.com/posts/5", polymorphic_url(@article) + assert_equal "http://www.example.com/posts/5", Routes.url_helpers.polymorphic_url(@article) + assert_equal "http://www.example.com/posts/5", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @article) + + assert_equal "http://www.example.com/pages/6", polymorphic_url(@page) + assert_equal "http://www.example.com/pages/6", Routes.url_helpers.polymorphic_url(@page) + assert_equal "http://www.example.com/pages/6", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @page) + + assert_equal "http://www.example.com/pages/7", polymorphic_url(@category_page) + assert_equal "http://www.example.com/pages/7", Routes.url_helpers.polymorphic_url(@category_page) + assert_equal "http://www.example.com/pages/7", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @category_page) + + assert_equal "http://www.example.com/pages/8", polymorphic_url(@product_page) + assert_equal "http://www.example.com/pages/8", Routes.url_helpers.polymorphic_url(@product_page) + assert_equal "http://www.example.com/pages/8", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @product_page) + + assert_equal "http://www.example.com/manufacturers/apple", polymorphic_url(@manufacturer) + assert_equal "http://www.example.com/manufacturers/apple", Routes.url_helpers.polymorphic_url(@manufacturer) + end + + def test_defining_direct_inside_a_scope_raises_runtime_error + routes = ActionDispatch::Routing::RouteSet.new + + assert_raises RuntimeError do + routes.draw do + namespace :admin do + direct(:rubyonrails) { "http://www.rubyonrails.org" } + end + end + end + end + + def test_defining_resolve_inside_a_scope_raises_runtime_error + routes = ActionDispatch::Routing::RouteSet.new + + assert_raises RuntimeError do + routes.draw do + namespace :admin do + resolve("User") { "/profile" } + end + end + end + end + + def test_defining_direct_url_registers_helper_method + assert_equal "http://www.example.com/basket", Routes.url_helpers.symbol_url + assert_equal true, Routes.named_routes.route_defined?(:symbol_url), "'symbol_url' named helper not found" + assert_equal true, Routes.named_routes.route_defined?(:symbol_path), "'symbol_path' named helper not found" + end +end diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb new file mode 100644 index 0000000000..fe1f1995d8 --- /dev/null +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -0,0 +1,495 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "rails/engine" +require "action_dispatch/routing/inspector" +require "io/console/size" + +class MountedRackApp + def self.call(env) + end +end + +class Rails::DummyController +end + +module ActionDispatch + module Routing + class RoutesInspectorTest < ActiveSupport::TestCase + setup do + @set = ActionDispatch::Routing::RouteSet.new + end + + def test_displaying_routes_for_engines + engine = Class.new(Rails::Engine) do + def self.inspect + "Blog::Engine" + end + end + engine.routes.draw do + get "/cart", to: "cart#show" + end + + output = draw do + get "/custom/assets", to: "custom_assets#show" + mount engine => "/blog", :as => "blog" + end + + assert_equal [ + " Prefix Verb URI Pattern Controller#Action", + "custom_assets GET /custom/assets(.:format) custom_assets#show", + " blog /blog Blog::Engine", + "", + "Routes for Blog::Engine:", + " cart GET /cart(.:format) cart#show" + ], output + end + + def test_displaying_routes_for_engines_without_routes + engine = Class.new(Rails::Engine) do + def self.inspect + "Blog::Engine" + end + end + engine.routes.draw do + end + + output = draw do + mount engine => "/blog", as: "blog" + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " blog /blog Blog::Engine", + "", + "Routes for Blog::Engine:" + ], output + end + + def test_cart_inspect + output = draw do + get "/cart", to: "cart#show" + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " cart GET /cart(.:format) cart#show" + ], output + end + + def test_articles_inspect_with_multiple_verbs + output = draw do + match "articles/:id", to: "articles#update", via: [:put, :patch] + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " PUT|PATCH /articles/:id(.:format) articles#update" + ], output + end + + def test_inspect_shows_custom_assets + output = draw do + get "/custom/assets", to: "custom_assets#show" + end + + assert_equal [ + " Prefix Verb URI Pattern Controller#Action", + "custom_assets GET /custom/assets(.:format) custom_assets#show" + ], output + end + + def test_inspect_routes_shows_resources_route + output = draw do + resources :articles + end + + assert_equal [ + " Prefix Verb URI Pattern Controller#Action", + " articles GET /articles(.:format) articles#index", + " POST /articles(.:format) articles#create", + " new_article GET /articles/new(.:format) articles#new", + "edit_article GET /articles/:id/edit(.:format) articles#edit", + " article GET /articles/:id(.:format) articles#show", + " PATCH /articles/:id(.:format) articles#update", + " PUT /articles/:id(.:format) articles#update", + " DELETE /articles/:id(.:format) articles#destroy" + ], output + end + + def test_inspect_routes_shows_root_route + output = draw do + root to: "pages#main" + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " root GET / pages#main" + ], output + end + + def test_inspect_routes_shows_dynamic_action_route + output = draw do + ActiveSupport::Deprecation.silence do + get "api/:action" => "api" + end + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " GET /api/:action(.:format) api#:action" + ], output + end + + def test_inspect_routes_shows_controller_and_action_only_route + output = draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action" + end + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " GET /:controller/:action(.:format) :controller#:action" + ], output + end + + def test_inspect_routes_shows_controller_and_action_route_with_constraints + output = draw do + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))", id: /\d+/ + end + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " GET /:controller(/:action(/:id))(.:format) :controller#:action {:id=>/\\d+/}" + ], output + end + + def test_rails_routes_shows_route_with_defaults + output = draw do + get "photos/:id" => "photos#show", :defaults => { format: "jpg" } + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + ' GET /photos/:id(.:format) photos#show {:format=>"jpg"}' + ], output + end + + def test_rails_routes_shows_route_with_constraints + output = draw do + get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " GET /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}" + ], output + end + + def test_rails_routes_shows_routes_with_dashes + output = draw do + get "about-us" => "pages#about_us" + get "our-work/latest" + + resources :photos, only: [:show] do + get "user-favorites", on: :collection + get "preview-photo", on: :member + get "summary-text" + end + end + + assert_equal [ + " Prefix Verb URI Pattern Controller#Action", + " about_us GET /about-us(.:format) pages#about_us", + " our_work_latest GET /our-work/latest(.:format) our_work#latest", + "user_favorites_photos GET /photos/user-favorites(.:format) photos#user_favorites", + " preview_photo_photo GET /photos/:id/preview-photo(.:format) photos#preview_photo", + " photo_summary_text GET /photos/:photo_id/summary-text(.:format) photos#summary_text", + " photo GET /photos/:id(.:format) photos#show" + ], output + end + + def test_rails_routes_shows_route_with_rack_app + output = draw do + get "foo/:id" => MountedRackApp, :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " GET /foo/:id(.:format) MountedRackApp {:id=>/[A-Z]\\d{5}/}" + ], output + end + + def test_rails_routes_shows_named_route_with_mounted_rack_app + output = draw do + mount MountedRackApp => "/foo" + end + + assert_equal [ + " Prefix Verb URI Pattern Controller#Action", + "mounted_rack_app /foo MountedRackApp" + ], output + end + + def test_rails_routes_shows_overridden_named_route_with_mounted_rack_app_with_name + output = draw do + mount MountedRackApp => "/foo", as: "blog" + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " blog /foo MountedRackApp" + ], output + end + + def test_rails_routes_shows_route_with_rack_app_nested_with_dynamic_constraints + constraint = Class.new do + def inspect + "( my custom constraint )" + end + end + + output = draw do + scope constraint: constraint.new do + mount MountedRackApp => "/foo" + end + end + + assert_equal [ + " Prefix Verb URI Pattern Controller#Action", + "mounted_rack_app /foo MountedRackApp {:constraint=>( my custom constraint )}" + ], output + end + + def test_rails_routes_dont_show_app_mounted_in_assets_prefix + output = draw do + get "/sprockets" => MountedRackApp + end + assert_no_match(/MountedRackApp/, output.first) + assert_no_match(/\/sprockets/, output.first) + end + + def test_rails_routes_shows_route_defined_in_under_assets_prefix + output = draw do + scope "/sprockets" do + get "/foo" => "foo#bar" + end + end + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " foo GET /sprockets/foo(.:format) foo#bar" + ], output + end + + def test_redirect + output = draw do + get "/foo" => redirect("/foo/bar"), :constraints => { subdomain: "admin" } + get "/bar" => redirect(path: "/foo/bar", status: 307) + get "/foobar" => redirect { "/foo/bar" } + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " foo GET /foo(.:format) redirect(301, /foo/bar) {:subdomain=>\"admin\"}", + " bar GET /bar(.:format) redirect(307, path: /foo/bar)", + "foobar GET /foobar(.:format) redirect(301)" + ], output + end + + def test_routes_can_be_filtered + output = draw(grep: "posts") do + resources :articles + resources :posts + end + + assert_equal [" Prefix Verb URI Pattern Controller#Action", + " posts GET /posts(.:format) posts#index", + " POST /posts(.:format) posts#create", + " new_post GET /posts/new(.:format) posts#new", + "edit_post GET /posts/:id/edit(.:format) posts#edit", + " post GET /posts/:id(.:format) posts#show", + " PATCH /posts/:id(.:format) posts#update", + " PUT /posts/:id(.:format) posts#update", + " DELETE /posts/:id(.:format) posts#destroy"], output + end + + def test_routes_when_expanded + previous_console_winsize = IO.console.winsize + IO.console.winsize = [0, 23] + + engine = Class.new(Rails::Engine) do + def self.inspect + "Blog::Engine" + end + end + engine.routes.draw do + get "/cart", to: "cart#show" + end + + output = draw(formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do + get "/custom/assets", to: "custom_assets#show" + get "/custom/furnitures", to: "custom_furnitures#show" + mount engine => "/blog", :as => "blog" + end + + assert_equal ["--[ Route 1 ]----------", + "Prefix | custom_assets", + "Verb | GET", + "URI | /custom/assets(.:format)", + "Controller#Action | custom_assets#show", + "--[ Route 2 ]----------", + "Prefix | custom_furnitures", + "Verb | GET", + "URI | /custom/furnitures(.:format)", + "Controller#Action | custom_furnitures#show", + "--[ Route 3 ]----------", + "Prefix | blog", + "Verb | ", + "URI | /blog", + "Controller#Action | Blog::Engine", + "", + "[ Routes for Blog::Engine ]", + "--[ Route 1 ]----------", + "Prefix | cart", + "Verb | GET", + "URI | /cart(.:format)", + "Controller#Action | cart#show"], output + ensure + IO.console.winsize = previous_console_winsize + end + + def test_no_routes_matched_filter_when_expanded + output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do + get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "No routes were found for this grep pattern.", + "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html." + ], output + end + + def test_not_routes_when_expanded + output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) { } + + assert_equal [ + "You don't have any routes defined!", + "", + "Please add some routes in config/routes.rb.", + "", + "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html." + ], output + end + + def test_routes_can_be_filtered_with_namespaced_controllers + output = draw(grep: "admin/posts") do + resources :articles + namespace :admin do + resources :posts + end + end + + assert_equal [" Prefix Verb URI Pattern Controller#Action", + " admin_posts GET /admin/posts(.:format) admin/posts#index", + " POST /admin/posts(.:format) admin/posts#create", + " new_admin_post GET /admin/posts/new(.:format) admin/posts#new", + "edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit", + " admin_post GET /admin/posts/:id(.:format) admin/posts#show", + " PATCH /admin/posts/:id(.:format) admin/posts#update", + " PUT /admin/posts/:id(.:format) admin/posts#update", + " DELETE /admin/posts/:id(.:format) admin/posts#destroy"], output + end + + def test_regression_route_with_controller_regexp + output = draw do + ActiveSupport::Deprecation.silence do + get ":controller(/:action)", controller: /api\/[^\/]+/, format: false + end + end + + assert_equal ["Prefix Verb URI Pattern Controller#Action", + " GET /:controller(/:action) :controller#:action"], output + end + + def test_inspect_routes_shows_resources_route_when_assets_disabled + @set = ActionDispatch::Routing::RouteSet.new + + output = draw do + get "/cart", to: "cart#show" + end + + assert_equal [ + "Prefix Verb URI Pattern Controller#Action", + " cart GET /cart(.:format) cart#show" + ], output + end + + def test_routes_with_undefined_filter + output = draw(controller: "Rails::MissingController") do + get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "No routes were found for this controller.", + "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html." + ], output + end + + def test_no_routes_matched_filter + output = draw(grep: "rails/dummy") do + get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "No routes were found for this grep pattern.", + "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html." + ], output + end + + def test_no_routes_were_defined + output = draw(grep: "Rails::DummyController") { } + + assert_equal [ + "You don't have any routes defined!", + "", + "Please add some routes in config/routes.rb.", + "", + "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html." + ], output + end + + def test_displaying_routes_for_internal_engines + engine = Class.new(Rails::Engine) do + def self.inspect + "Blog::Engine" + end + end + engine.routes.draw do + get "/cart", to: "cart#show" + post "/cart", to: "cart#create" + patch "/cart", to: "cart#update" + end + + output = draw do + get "/custom/assets", to: "custom_assets#show" + mount engine => "/blog", as: "blog", internal: true + end + + assert_equal [ + " Prefix Verb URI Pattern Controller#Action", + "custom_assets GET /custom/assets(.:format) custom_assets#show", + ], output + end + + private + def draw(formatter: ActionDispatch::Routing::ConsoleFormatter::Sheet.new, **options, &block) + @set.draw(&block) + inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes) + inspector.format(formatter, options).split("\n") + end + end + end +end diff --git a/actionpack/test/dispatch/routing/ipv6_redirect_test.rb b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb new file mode 100644 index 0000000000..31559bffc7 --- /dev/null +++ b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class IPv6IntegrationTest < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new + include Routes.url_helpers + + class ::BadRouteRequestController < ActionController::Base + include Routes.url_helpers + def index + render plain: foo_path + end + + def foo + redirect_to action: :index + end + end + + Routes.draw do + get "/", to: "bad_route_request#index", as: :index + get "/foo", to: "bad_route_request#foo", as: :foo + end + + def _routes + Routes + end + + APP = build_app Routes + def app + APP + end + + test "bad IPv6 redirection" do + # def test_simple_redirect + request_env = { + "REMOTE_ADDR" => "fd07:2fa:6cff:2112:225:90ff:fec7:22aa", + "HTTP_HOST" => "[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]:3000", + "SERVER_NAME" => "[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]", + "SERVER_PORT" => 3000 } + + get "/foo", env: request_env + assert_response :redirect + assert_equal "http://[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]:3000/", redirect_to_url + end +end diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb new file mode 100644 index 0000000000..e61d47b160 --- /dev/null +++ b/actionpack/test/dispatch/routing/route_set_test.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Routing + class RouteSetTest < ActiveSupport::TestCase + class SimpleApp + def initialize(response) + @response = response + end + + def call(env) + [ 200, { "Content-Type" => "text/plain" }, [response] ] + end + end + + setup do + @set = RouteSet.new + end + + test "not being empty when route is added" do + assert empty? + + draw do + get "foo", to: SimpleApp.new("foo#index") + end + + assert_not empty? + end + + test "url helpers are added when route is added" do + draw do + get "foo", to: SimpleApp.new("foo#index") + end + + assert_equal "/foo", url_helpers.foo_path + assert_raises NoMethodError do + assert_equal "/bar", url_helpers.bar_path + end + + draw do + get "foo", to: SimpleApp.new("foo#index") + get "bar", to: SimpleApp.new("bar#index") + end + + assert_equal "/foo", url_helpers.foo_path + assert_equal "/bar", url_helpers.bar_path + end + + test "url helpers are updated when route is updated" do + draw do + get "bar", to: SimpleApp.new("bar#index"), as: :bar + end + + assert_equal "/bar", url_helpers.bar_path + + draw do + get "baz", to: SimpleApp.new("baz#index"), as: :bar + end + + assert_equal "/baz", url_helpers.bar_path + end + + test "url helpers are removed when route is removed" do + draw do + get "foo", to: SimpleApp.new("foo#index") + get "bar", to: SimpleApp.new("bar#index") + end + + assert_equal "/foo", url_helpers.foo_path + assert_equal "/bar", url_helpers.bar_path + + draw do + get "foo", to: SimpleApp.new("foo#index") + end + + assert_equal "/foo", url_helpers.foo_path + assert_raises NoMethodError do + assert_equal "/bar", url_helpers.bar_path + end + end + + test "only_path: true with *_url and no :host option" do + draw do + get "foo", to: SimpleApp.new("foo#index") + end + + assert_equal "/foo", url_helpers.foo_url(only_path: true) + end + + test "only_path: false with *_url and no :host option" do + draw do + get "foo", to: SimpleApp.new("foo#index") + end + + assert_raises ArgumentError do + assert_equal "http://example.com/foo", url_helpers.foo_url(only_path: false) + end + end + + test "only_path: false with *_url and local :host option" do + draw do + get "foo", to: SimpleApp.new("foo#index") + end + + assert_equal "http://example.com/foo", url_helpers.foo_url(only_path: false, host: "example.com") + end + + test "only_path: false with *_url and global :host option" do + @set.default_url_options = { host: "example.com" } + + draw do + get "foo", to: SimpleApp.new("foo#index") + end + + assert_equal "http://example.com/foo", url_helpers.foo_url(only_path: false) + end + + test "explicit keys win over implicit keys" do + draw do + resources :foo do + resources :bar, to: SimpleApp.new("foo#show") + end + end + + assert_equal "/foo/1/bar/2", url_helpers.foo_bar_path(1, 2) + assert_equal "/foo/1/bar/2", url_helpers.foo_bar_path(2, foo_id: 1) + end + + test "having an optional scope with resources" do + draw do + scope "(/:foo)" do + resources :users + end + end + + assert_equal "/users/1", url_helpers.user_path(1) + assert_equal "/users/1", url_helpers.user_path(1, foo: nil) + assert_equal "/a/users/1", url_helpers.user_path(1, foo: "a") + end + + test "implicit path components consistently return the same result" do + draw do + resources :users, to: SimpleApp.new("foo#index") + end + assert_equal "/users/1.json", url_helpers.user_path(1, :json) + assert_equal "/users/1.json", url_helpers.user_path(1, format: :json) + assert_equal "/users/1.json", url_helpers.user_path(1, :json) + end + + private + def draw(&block) + @set.draw(&block) + end + + def url_helpers + @set.url_helpers + end + + def empty? + @set.empty? + end + end + end +end diff --git a/actionpack/test/dispatch/routing_assertions_test.rb b/actionpack/test/dispatch/routing_assertions_test.rb new file mode 100644 index 0000000000..009b6d9bc3 --- /dev/null +++ b/actionpack/test/dispatch/routing_assertions_test.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "rails/engine" +require "controller/fake_controllers" + +class SecureArticlesController < ArticlesController; end +class BlockArticlesController < ArticlesController; end +class QueryArticlesController < ArticlesController; end + +class SecureBooksController < BooksController; end +class BlockBooksController < BooksController; end +class QueryBooksController < BooksController; end + +class RoutingAssertionsTest < ActionController::TestCase + def setup + engine = Class.new(Rails::Engine) do + def self.name + "blog_engine" + end + end + engine.routes.draw do + resources :books + + scope "secure", constraints: { protocol: "https://" } do + resources :books, controller: "secure_books" + end + + scope "block", constraints: lambda { |r| r.ssl? } do + resources :books, controller: "block_books" + end + + scope "query", constraints: lambda { |r| r.params[:use_query] == "true" } do + resources :books, controller: "query_books" + end + end + + @routes = ActionDispatch::Routing::RouteSet.new + @routes.draw do + resources :articles + + scope "secure", constraints: { protocol: "https://" } do + resources :articles, controller: "secure_articles" + end + + scope "block", constraints: lambda { |r| r.ssl? } do + resources :articles, controller: "block_articles" + end + + scope "query", constraints: lambda { |r| r.params[:use_query] == "true" } do + resources :articles, controller: "query_articles" + end + + mount engine => "/shelf" + + get "/shelf/foo", controller: "query_articles", action: "index" + end + end + + def test_assert_generates + assert_generates("/articles", controller: "articles", action: "index") + assert_generates("/articles/1", controller: "articles", action: "show", id: "1") + end + + def test_assert_generates_with_defaults + assert_generates("/articles/1/edit", { controller: "articles", action: "edit" }, { id: "1" }) + end + + def test_assert_generates_with_extras + assert_generates("/articles", { controller: "articles", action: "index", page: "1" }, {}, { page: "1" }) + end + + def test_assert_recognizes + assert_recognizes({ controller: "articles", action: "index" }, "/articles") + assert_recognizes({ controller: "articles", action: "show", id: "1" }, "/articles/1") + end + + def test_assert_recognizes_with_extras + assert_recognizes({ controller: "articles", action: "index", page: "1" }, "/articles", page: "1") + end + + def test_assert_recognizes_with_method + assert_recognizes({ controller: "articles", action: "create" }, { path: "/articles", method: :post }) + assert_recognizes({ controller: "articles", action: "update", id: "1" }, { path: "/articles/1", method: :put }) + end + + def test_assert_recognizes_with_hash_constraint + assert_raise(Assertion) do + assert_recognizes({ controller: "secure_articles", action: "index" }, "http://test.host/secure/articles") + end + assert_recognizes({ controller: "secure_articles", action: "index", protocol: "https://" }, "https://test.host/secure/articles") + end + + def test_assert_recognizes_with_block_constraint + assert_raise(Assertion) do + assert_recognizes({ controller: "block_articles", action: "index" }, "http://test.host/block/articles") + end + assert_recognizes({ controller: "block_articles", action: "index" }, "https://test.host/block/articles") + end + + def test_assert_recognizes_with_query_constraint + assert_raise(Assertion) do + assert_recognizes({ controller: "query_articles", action: "index", use_query: "false" }, "/query/articles", use_query: "false") + end + assert_recognizes({ controller: "query_articles", action: "index", use_query: "true" }, "/query/articles", use_query: "true") + end + + def test_assert_recognizes_raises_message + err = assert_raise(Assertion) do + assert_recognizes({ controller: "secure_articles", action: "index" }, "http://test.host/secure/articles", {}, "This is a really bad msg") + end + + assert_match err.message, "This is a really bad msg" + end + + def test_assert_recognizes_with_engine + assert_recognizes({ controller: "books", action: "index" }, "/shelf/books") + assert_recognizes({ controller: "books", action: "show", id: "1" }, "/shelf/books/1") + end + + def test_assert_recognizes_with_engine_and_extras + assert_recognizes({ controller: "books", action: "index", page: "1" }, "/shelf/books", page: "1") + end + + def test_assert_recognizes_with_engine_and_method + assert_recognizes({ controller: "books", action: "create" }, { path: "/shelf/books", method: :post }) + assert_recognizes({ controller: "books", action: "update", id: "1" }, { path: "/shelf/books/1", method: :put }) + end + + def test_assert_recognizes_with_engine_and_hash_constraint + assert_raise(Assertion) do + assert_recognizes({ controller: "secure_books", action: "index" }, "http://test.host/shelf/secure/books") + end + assert_recognizes({ controller: "secure_books", action: "index", protocol: "https://" }, "https://test.host/shelf/secure/books") + end + + def test_assert_recognizes_with_engine_and_block_constraint + assert_raise(Assertion) do + assert_recognizes({ controller: "block_books", action: "index" }, "http://test.host/shelf/block/books") + end + assert_recognizes({ controller: "block_books", action: "index" }, "https://test.host/shelf/block/books") + end + + def test_assert_recognizes_with_engine_and_query_constraint + assert_raise(Assertion) do + assert_recognizes({ controller: "query_books", action: "index", use_query: "false" }, "/shelf/query/books", use_query: "false") + end + assert_recognizes({ controller: "query_books", action: "index", use_query: "true" }, "/shelf/query/books", use_query: "true") + end + + def test_assert_recognizes_raises_message_with_engine + err = assert_raise(Assertion) do + assert_recognizes({ controller: "secure_books", action: "index" }, "http://test.host/shelf/secure/books", {}, "This is a really bad msg") + end + + assert_match err.message, "This is a really bad msg" + end + + def test_assert_recognizes_continue_to_recoginize_after_it_tried_engines + assert_recognizes({ controller: "query_articles", action: "index" }, "/shelf/foo") + end + + def test_assert_routing + assert_routing("/articles", controller: "articles", action: "index") + end + + def test_assert_routing_raises_message + err = assert_raise(Assertion) do + assert_routing("/thisIsNotARoute", { controller: "articles", action: "edit", id: "1" }, { id: "1" }, {}, "This is a really bad msg") + end + + assert_match err.message, "This is a really bad msg" + end + + def test_assert_routing_with_defaults + assert_routing("/articles/1/edit", { controller: "articles", action: "edit", id: "1" }, { id: "1" }) + end + + def test_assert_routing_with_extras + assert_routing("/articles", { controller: "articles", action: "index", page: "1" }, {}, { page: "1" }) + end + + def test_assert_routing_with_hash_constraint + assert_raise(Assertion) do + assert_routing("http://test.host/secure/articles", controller: "secure_articles", action: "index") + end + assert_routing("https://test.host/secure/articles", controller: "secure_articles", action: "index", protocol: "https://") + end + + def test_assert_routing_with_block_constraint + assert_raise(Assertion) do + assert_routing("http://test.host/block/articles", controller: "block_articles", action: "index") + end + assert_routing("https://test.host/block/articles", controller: "block_articles", action: "index") + end + + def test_with_routing + with_routing do |routes| + routes.draw do + resources :articles, path: "artikel" + end + + assert_routing("/artikel", controller: "articles", action: "index") + assert_raise(Assertion) do + assert_routing("/articles", controller: "articles", action: "index") + end + end + end +end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb new file mode 100644 index 0000000000..4dffbd0db1 --- /dev/null +++ b/actionpack/test/dispatch/routing_test.rb @@ -0,0 +1,5140 @@ +# frozen_string_literal: true + +require "erb" +require "abstract_unit" +require "controller/fake_controllers" +require "active_support/messages/rotation_configuration" + +class TestRoutingMapper < ActionDispatch::IntegrationTest + SprocketsApp = lambda { |env| + [200, { "Content-Type" => "text/html" }, ["javascripts"]] + } + + class IpRestrictor + def self.matches?(request) + request.ip =~ /192\.168\.1\.1\d\d/ + end + end + + class GrumpyRestrictor + def self.matches?(request) + false + end + end + + class YoutubeFavoritesRedirector + def self.call(params, request) + "http://www.youtube.com/watch?v=#{params[:youtube_id]}" + end + end + + def test_logout + draw do + controller :sessions do + delete "logout" => :destroy + end + end + + delete "/logout" + assert_equal "sessions#destroy", @response.body + + assert_equal "/logout", logout_path + assert_equal "/logout", url_for(controller: "sessions", action: "destroy", only_path: true) + end + + def test_login + draw do + default_url_options host: "rubyonrails.org" + + controller :sessions do + get "login" => :new + post "login" => :create + end + end + + get "/login" + assert_equal "sessions#new", @response.body + assert_equal "/login", login_path + + post "/login" + assert_equal "sessions#create", @response.body + + assert_equal "/login", url_for(controller: "sessions", action: "create", only_path: true) + assert_equal "/login", url_for(controller: "sessions", action: "new", only_path: true) + + assert_equal "http://rubyonrails.org/login", url_for(controller: "sessions", action: "create") + assert_equal "http://rubyonrails.org/login", login_url + end + + def test_login_redirect + draw do + get "account/login", to: redirect("/login") + end + + get "/account/login" + verify_redirect "http://www.example.com/login" + end + + def test_logout_redirect_without_to + draw do + get "account/logout" => redirect("/logout"), :as => :logout_redirect + end + + assert_equal "/account/logout", logout_redirect_path + get "/account/logout" + verify_redirect "http://www.example.com/logout" + end + + def test_namespace_redirect + draw do + namespace :private do + root to: redirect("/private/index") + get "index", to: "private#index" + end + end + + get "/private" + verify_redirect "http://www.example.com/private/index" + end + + def test_redirect_with_failing_constraint + draw do + get "hi", to: redirect("/foo"), constraints: ::TestRoutingMapper::GrumpyRestrictor + end + + get "/hi" + assert_equal 404, status + end + + def test_redirect_with_passing_constraint + draw do + get "hi", to: redirect("/foo"), constraints: ->(req) { true } + end + + get "/hi" + assert_equal 301, status + end + + def test_accepts_a_constraint_object_responding_to_call + constraint = Class.new do + def call(*); true; end + def matches?(*); false; end + end + + draw do + get "/", to: "home#show", constraints: constraint.new + end + + assert_nothing_raised do + get "/" + end + end + + def test_namespace_with_controller_segment + assert_raise(ArgumentError) do + draw do + namespace :admin do + ActiveSupport::Deprecation.silence do + get "/:controller(/:action(/:id(.:format)))" + end + end + end + end + end + + def test_namespace_without_controller_segment + draw do + namespace :admin do + ActiveSupport::Deprecation.silence do + get "hello/:controllers/:action" + end + end + end + get "/admin/hello/foo/new" + assert_equal "foo", @request.params["controllers"] + end + + def test_session_singleton_resource + draw do + resource :session do + get :create + post :reset + end + end + + get "/session" + assert_equal "sessions#create", @response.body + assert_equal "/session", session_path + + post "/session" + assert_equal "sessions#create", @response.body + + put "/session" + assert_equal "sessions#update", @response.body + + delete "/session" + assert_equal "sessions#destroy", @response.body + + get "/session/new" + assert_equal "sessions#new", @response.body + assert_equal "/session/new", new_session_path + + get "/session/edit" + assert_equal "sessions#edit", @response.body + assert_equal "/session/edit", edit_session_path + + post "/session/reset" + assert_equal "sessions#reset", @response.body + assert_equal "/session/reset", reset_session_path + end + + def test_session_singleton_resource_for_api_app + config = ActionDispatch::Routing::RouteSet::Config.new + config.api_only = true + + self.class.stub_controllers(config) do |routes| + routes.draw do + resource :session do + get :create + post :reset + end + end + @app = RoutedRackApp.new routes + end + + get "/session" + assert_equal "sessions#create", @response.body + assert_equal "/session", session_path + + post "/session" + assert_equal "sessions#create", @response.body + + put "/session" + assert_equal "sessions#update", @response.body + + delete "/session" + assert_equal "sessions#destroy", @response.body + + post "/session/reset" + assert_equal "sessions#reset", @response.body + assert_equal "/session/reset", reset_session_path + + get "/session/new" + assert_equal "Not Found", @response.body + + get "/session/edit" + assert_equal "Not Found", @response.body + end + + def test_session_info_nested_singleton_resource + draw do + resource :session do + resource :info + end + end + + get "/session/info" + assert_equal "infos#show", @response.body + assert_equal "/session/info", session_info_path + end + + def test_member_on_resource + draw do + resource :session do + member do + get :crush + end + end + end + + get "/session/crush" + assert_equal "sessions#crush", @response.body + assert_equal "/session/crush", crush_session_path + end + + def test_redirect_modulo + draw do + get "account/modulo/:name", to: redirect("/%{name}s") + end + + get "/account/modulo/name" + verify_redirect "http://www.example.com/names" + end + + def test_redirect_proc + draw do + get "account/proc/:name", to: redirect { |params, req| "/#{params[:name].pluralize}" } + end + + get "/account/proc/person" + verify_redirect "http://www.example.com/people" + end + + def test_redirect_proc_with_request + draw do + get "account/proc_req" => redirect { |params, req| "/#{req.method}" } + end + + get "/account/proc_req" + verify_redirect "http://www.example.com/GET" + end + + def test_redirect_hash_with_subdomain + draw do + get "mobile", to: redirect(subdomain: "mobile") + end + + get "/mobile" + verify_redirect "http://mobile.example.com/mobile" + end + + def test_redirect_hash_with_domain_and_path + draw do + get "documentation", to: redirect(domain: "example-documentation.com", path: "") + end + + get "/documentation" + verify_redirect "http://www.example-documentation.com" + end + + def test_redirect_hash_with_path + draw do + get "new_documentation", to: redirect(path: "/documentation/new") + end + + get "/new_documentation" + verify_redirect "http://www.example.com/documentation/new" + end + + def test_redirect_hash_with_host + draw do + get "super_new_documentation", to: redirect(host: "super-docs.com") + end + + get "/super_new_documentation?section=top" + verify_redirect "http://super-docs.com/super_new_documentation?section=top" + end + + def test_redirect_hash_path_substitution + draw do + get "stores/:name", to: redirect(subdomain: "stores", path: "/%{name}") + end + + get "/stores/iernest" + verify_redirect "http://stores.example.com/iernest" + end + + def test_redirect_hash_path_substitution_with_catch_all + draw do + get "stores/:name(*rest)", to: redirect(subdomain: "stores", path: "/%{name}%{rest}") + end + + get "/stores/iernest/products" + verify_redirect "http://stores.example.com/iernest/products" + end + + def test_redirect_class + draw do + get "youtube_favorites/:youtube_id/:name", to: redirect(YoutubeFavoritesRedirector) + end + + get "/youtube_favorites/oHg5SJYRHA0/rick-rolld" + verify_redirect "http://www.youtube.com/watch?v=oHg5SJYRHA0" + end + + def test_openid + draw do + match "openid/login", via: [:get, :post], to: "openid#login" + end + + get "/openid/login" + assert_equal "openid#login", @response.body + + post "/openid/login" + assert_equal "openid#login", @response.body + end + + def test_bookmarks + draw do + scope "bookmark", controller: "bookmarks", as: :bookmark do + get :new, path: "build" + post :create, path: "create", as: "" + put :update + get :remove, action: :destroy, as: :remove + end + end + + get "/bookmark/build" + assert_equal "bookmarks#new", @response.body + assert_equal "/bookmark/build", bookmark_new_path + + post "/bookmark/create" + assert_equal "bookmarks#create", @response.body + assert_equal "/bookmark/create", bookmark_path + + put "/bookmark/update" + assert_equal "bookmarks#update", @response.body + assert_equal "/bookmark/update", bookmark_update_path + + get "/bookmark/remove" + assert_equal "bookmarks#destroy", @response.body + assert_equal "/bookmark/remove", bookmark_remove_path + end + + def test_pagemarks + draw do + scope "pagemark", controller: "pagemarks", as: :pagemark do + get "build", action: "new", as: "new" + post "create", as: "" + put "update" + get "remove", action: :destroy, as: :remove + get "", action: :show, as: :show + end + end + + get "/pagemark/build" + assert_equal "pagemarks#new", @response.body + assert_equal "/pagemark/build", pagemark_new_path + + post "/pagemark/create" + assert_equal "pagemarks#create", @response.body + assert_equal "/pagemark/create", pagemark_path + + put "/pagemark/update" + assert_equal "pagemarks#update", @response.body + assert_equal "/pagemark/update", pagemark_update_path + + get "/pagemark/remove" + assert_equal "pagemarks#destroy", @response.body + assert_equal "/pagemark/remove", pagemark_remove_path + + get "/pagemark" + assert_equal "pagemarks#show", @response.body + assert_equal "/pagemark", pagemark_show_path + end + + def test_admin + draw do + constraints(ip: /192\.168\.1\.\d\d\d/) do + get "admin" => "queenbee#index" + end + + constraints ::TestRoutingMapper::IpRestrictor do + get "admin/accounts" => "queenbee#accounts" + end + + get "admin/passwords" => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor + end + + get "/admin", headers: { "REMOTE_ADDR" => "192.168.1.100" } + assert_equal "queenbee#index", @response.body + + get "/admin", headers: { "REMOTE_ADDR" => "10.0.0.100" } + assert_equal "pass", @response.headers["X-Cascade"] + + get "/admin/accounts", headers: { "REMOTE_ADDR" => "192.168.1.100" } + assert_equal "queenbee#accounts", @response.body + + get "/admin/accounts", headers: { "REMOTE_ADDR" => "10.0.0.100" } + assert_equal "pass", @response.headers["X-Cascade"] + + get "/admin/passwords", headers: { "REMOTE_ADDR" => "192.168.1.100" } + assert_equal "queenbee#passwords", @response.body + + get "/admin/passwords", headers: { "REMOTE_ADDR" => "10.0.0.100" } + assert_equal "pass", @response.headers["X-Cascade"] + end + + def test_global + draw do + controller(:global) do + get "global/hide_notice" + get "global/export", action: :export, as: :export_request + get "/export/:id/:file", action: :export, as: :export_download, constraints: { file: /.*/ } + + ActiveSupport::Deprecation.silence do + get "global/:action" + end + end + end + + get "/global/dashboard" + assert_equal "global#dashboard", @response.body + + get "/global/export" + assert_equal "global#export", @response.body + + get "/global/hide_notice" + assert_equal "global#hide_notice", @response.body + + get "/export/123/foo.txt" + assert_equal "global#export", @response.body + + assert_equal "/global/export", export_request_path + assert_equal "/global/hide_notice", global_hide_notice_path + assert_equal "/export/123/foo.txt", export_download_path(id: 123, file: "foo.txt") + end + + def test_local + draw do + ActiveSupport::Deprecation.silence do + get "/local/:action", controller: "local" + end + end + + get "/local/dashboard" + assert_equal "local#dashboard", @response.body + end + + # tests the use of dup in url_for + def test_url_for_with_no_side_effects + draw do + get "/projects/status(.:format)" + end + + # without dup, additional (and possibly unwanted) values will be present in the options (eg. :host) + original_options = { controller: "projects", action: "status" } + options = original_options.dup + + url_for options + + # verify that the options passed in have not changed from the original ones + assert_equal original_options, options + end + + def test_url_for_does_not_modify_controller + draw do + get "/projects/status(.:format)" + end + + controller = "/projects" + options = { controller: controller, action: "status", only_path: true } + url = url_for(options) + + assert_equal "/projects/status", url + assert_equal "/projects", controller + end + + # tests the arguments modification free version of define_hash_access + def test_named_route_with_no_side_effects + draw do + resources :customers do + get "profile", on: :member + end + end + + original_options = { host: "test.host" } + options = original_options.dup + + profile_customer_url("customer_model", options) + + # verify that the options passed in have not changed from the original ones + assert_equal original_options, options + end + + def test_projects_status + draw do + get "/projects/status(.:format)" + end + + assert_equal "/projects/status", url_for(controller: "projects", action: "status", only_path: true) + assert_equal "/projects/status.json", url_for(controller: "projects", action: "status", format: "json", only_path: true) + end + + def test_projects + draw do + resources :projects, controller: :project + end + + get "/projects" + assert_equal "project#index", @response.body + assert_equal "/projects", projects_path + + post "/projects" + assert_equal "project#create", @response.body + + get "/projects.xml" + assert_equal "project#index", @response.body + assert_equal "/projects.xml", projects_path(format: "xml") + + get "/projects/new" + assert_equal "project#new", @response.body + assert_equal "/projects/new", new_project_path + + get "/projects/new.xml" + assert_equal "project#new", @response.body + assert_equal "/projects/new.xml", new_project_path(format: "xml") + + get "/projects/1" + assert_equal "project#show", @response.body + assert_equal "/projects/1", project_path(id: "1") + + get "/projects/1.xml" + assert_equal "project#show", @response.body + assert_equal "/projects/1.xml", project_path(id: "1", format: "xml") + + get "/projects/1/edit" + assert_equal "project#edit", @response.body + assert_equal "/projects/1/edit", edit_project_path(id: "1") + end + + def test_projects_for_api_app + config = ActionDispatch::Routing::RouteSet::Config.new + config.api_only = true + + self.class.stub_controllers(config) do |routes| + routes.draw do + resources :projects, controller: :project + end + @app = RoutedRackApp.new routes + end + + get "/projects" + assert_equal "project#index", @response.body + assert_equal "/projects", projects_path + + post "/projects" + assert_equal "project#create", @response.body + + get "/projects.xml" + assert_equal "project#index", @response.body + assert_equal "/projects.xml", projects_path(format: "xml") + + get "/projects/1" + assert_equal "project#show", @response.body + assert_equal "/projects/1", project_path(id: "1") + + get "/projects/1.xml" + assert_equal "project#show", @response.body + assert_equal "/projects/1.xml", project_path(id: "1", format: "xml") + + get "/projects/1/edit" + assert_equal "Not Found", @response.body + end + + def test_projects_with_post_action_and_new_path_on_collection + draw do + resources :projects, controller: :project do + post "new", action: "new", on: :collection, as: :new + end + end + + post "/projects/new" + assert_equal "project#new", @response.body + assert_equal "/projects/new", new_projects_path + end + + def test_projects_involvements + draw do + resources :projects, controller: :project do + resources :involvements, :attachments + end + end + + get "/projects/1/involvements" + assert_equal "involvements#index", @response.body + assert_equal "/projects/1/involvements", project_involvements_path(project_id: "1") + + get "/projects/1/involvements/new" + assert_equal "involvements#new", @response.body + assert_equal "/projects/1/involvements/new", new_project_involvement_path(project_id: "1") + + get "/projects/1/involvements/1" + assert_equal "involvements#show", @response.body + assert_equal "/projects/1/involvements/1", project_involvement_path(project_id: "1", id: "1") + + put "/projects/1/involvements/1" + assert_equal "involvements#update", @response.body + + delete "/projects/1/involvements/1" + assert_equal "involvements#destroy", @response.body + + get "/projects/1/involvements/1/edit" + assert_equal "involvements#edit", @response.body + assert_equal "/projects/1/involvements/1/edit", edit_project_involvement_path(project_id: "1", id: "1") + end + + def test_projects_attachments + draw do + resources :projects, controller: :project do + resources :involvements, :attachments + end + end + + get "/projects/1/attachments" + assert_equal "attachments#index", @response.body + assert_equal "/projects/1/attachments", project_attachments_path(project_id: "1") + end + + def test_projects_participants + draw do + resources :projects, controller: :project do + resources :participants do + put :update_all, on: :collection + end + end + end + + get "/projects/1/participants" + assert_equal "participants#index", @response.body + assert_equal "/projects/1/participants", project_participants_path(project_id: "1") + + put "/projects/1/participants/update_all" + assert_equal "participants#update_all", @response.body + assert_equal "/projects/1/participants/update_all", update_all_project_participants_path(project_id: "1") + end + + def test_projects_companies + draw do + resources :projects, controller: :project do + resources :companies do + resources :people + resource :avatar, controller: :avatar + end + end + end + + get "/projects/1/companies" + assert_equal "companies#index", @response.body + assert_equal "/projects/1/companies", project_companies_path(project_id: "1") + + get "/projects/1/companies/1/people" + assert_equal "people#index", @response.body + assert_equal "/projects/1/companies/1/people", project_company_people_path(project_id: "1", company_id: "1") + + get "/projects/1/companies/1/avatar" + assert_equal "avatar#show", @response.body + assert_equal "/projects/1/companies/1/avatar", project_company_avatar_path(project_id: "1", company_id: "1") + end + + def test_project_manager + draw do + resources :projects do + resource :manager, as: :super_manager do + post :fire + end + end + end + + get "/projects/1/manager" + assert_equal "managers#show", @response.body + assert_equal "/projects/1/manager", project_super_manager_path(project_id: "1") + + get "/projects/1/manager/new" + assert_equal "managers#new", @response.body + assert_equal "/projects/1/manager/new", new_project_super_manager_path(project_id: "1") + + post "/projects/1/manager/fire" + assert_equal "managers#fire", @response.body + assert_equal "/projects/1/manager/fire", fire_project_super_manager_path(project_id: "1") + end + + def test_project_images + draw do + resources :projects do + resources :images, as: :funny_images do + post :revise, on: :member + end + end + end + + get "/projects/1/images" + assert_equal "images#index", @response.body + assert_equal "/projects/1/images", project_funny_images_path(project_id: "1") + + get "/projects/1/images/new" + assert_equal "images#new", @response.body + assert_equal "/projects/1/images/new", new_project_funny_image_path(project_id: "1") + + post "/projects/1/images/1/revise" + assert_equal "images#revise", @response.body + assert_equal "/projects/1/images/1/revise", revise_project_funny_image_path(project_id: "1", id: "1") + end + + def test_projects_people + draw do + resources :projects do + resources :people do + nested do + scope "/:access_token" do + resource :avatar + end + end + + member do + put :accessible_projects + post :resend, :generate_new_password + end + end + end + end + + get "/projects/1/people" + assert_equal "people#index", @response.body + assert_equal "/projects/1/people", project_people_path(project_id: "1") + + get "/projects/1/people/1" + assert_equal "people#show", @response.body + assert_equal "/projects/1/people/1", project_person_path(project_id: "1", id: "1") + + get "/projects/1/people/1/7a2dec8/avatar" + assert_equal "avatars#show", @response.body + assert_equal "/projects/1/people/1/7a2dec8/avatar", project_person_avatar_path(project_id: "1", person_id: "1", access_token: "7a2dec8") + + put "/projects/1/people/1/accessible_projects" + assert_equal "people#accessible_projects", @response.body + assert_equal "/projects/1/people/1/accessible_projects", accessible_projects_project_person_path(project_id: "1", id: "1") + + post "/projects/1/people/1/resend" + assert_equal "people#resend", @response.body + assert_equal "/projects/1/people/1/resend", resend_project_person_path(project_id: "1", id: "1") + + post "/projects/1/people/1/generate_new_password" + assert_equal "people#generate_new_password", @response.body + assert_equal "/projects/1/people/1/generate_new_password", generate_new_password_project_person_path(project_id: "1", id: "1") + end + + def test_projects_with_resources_path_names + draw do + resources_path_names correlation_indexes: "info_about_correlation_indexes" + + resources :projects do + get :correlation_indexes, on: :collection + end + end + + get "/projects/info_about_correlation_indexes" + assert_equal "projects#correlation_indexes", @response.body + assert_equal "/projects/info_about_correlation_indexes", correlation_indexes_projects_path + end + + def test_projects_posts + draw do + resources :projects do + resources :posts do + get :archive, :toggle_view, on: :collection + post :preview, on: :member + + resource :subscription + + resources :comments do + post :preview, on: :collection + end + end + end + end + + get "/projects/1/posts" + assert_equal "posts#index", @response.body + assert_equal "/projects/1/posts", project_posts_path(project_id: "1") + + get "/projects/1/posts/archive" + assert_equal "posts#archive", @response.body + assert_equal "/projects/1/posts/archive", archive_project_posts_path(project_id: "1") + + get "/projects/1/posts/toggle_view" + assert_equal "posts#toggle_view", @response.body + assert_equal "/projects/1/posts/toggle_view", toggle_view_project_posts_path(project_id: "1") + + post "/projects/1/posts/1/preview" + assert_equal "posts#preview", @response.body + assert_equal "/projects/1/posts/1/preview", preview_project_post_path(project_id: "1", id: "1") + + get "/projects/1/posts/1/subscription" + assert_equal "subscriptions#show", @response.body + assert_equal "/projects/1/posts/1/subscription", project_post_subscription_path(project_id: "1", post_id: "1") + + get "/projects/1/posts/1/comments" + assert_equal "comments#index", @response.body + assert_equal "/projects/1/posts/1/comments", project_post_comments_path(project_id: "1", post_id: "1") + + post "/projects/1/posts/1/comments/preview" + assert_equal "comments#preview", @response.body + assert_equal "/projects/1/posts/1/comments/preview", preview_project_post_comments_path(project_id: "1", post_id: "1") + end + + def test_replies + draw do + resources :replies do + member do + put :answer, action: :mark_as_answer + delete :answer, action: :unmark_as_answer + end + end + end + + put "/replies/1/answer" + assert_equal "replies#mark_as_answer", @response.body + + delete "/replies/1/answer" + assert_equal "replies#unmark_as_answer", @response.body + end + + def test_resource_routes_with_only_and_except + draw do + resources :posts, only: [:index, :show] do + resources :comments, except: :destroy + end + end + + get "/posts" + assert_equal "posts#index", @response.body + assert_equal "/posts", posts_path + + get "/posts/1" + assert_equal "posts#show", @response.body + assert_equal "/posts/1", post_path(id: 1) + + get "/posts/1/comments" + assert_equal "comments#index", @response.body + assert_equal "/posts/1/comments", post_comments_path(post_id: 1) + + post "/posts" + assert_equal "pass", @response.headers["X-Cascade"] + put "/posts/1" + assert_equal "pass", @response.headers["X-Cascade"] + delete "/posts/1" + assert_equal "pass", @response.headers["X-Cascade"] + delete "/posts/1/comments" + assert_equal "pass", @response.headers["X-Cascade"] + end + + def test_resource_routes_only_create_update_destroy + draw do + resource :past, only: :destroy + resource :present, only: :update + resource :future, only: :create + end + + delete "/past" + assert_equal "pasts#destroy", @response.body + assert_equal "/past", past_path + + patch "/present" + assert_equal "presents#update", @response.body + assert_equal "/present", present_path + + put "/present" + assert_equal "presents#update", @response.body + assert_equal "/present", present_path + + post "/future" + assert_equal "futures#create", @response.body + assert_equal "/future", future_path + end + + def test_resources_routes_only_create_update_destroy + draw do + resources :relationships, only: [:create, :destroy] + resources :friendships, only: [:update] + end + + post "/relationships" + assert_equal "relationships#create", @response.body + assert_equal "/relationships", relationships_path + + delete "/relationships/1" + assert_equal "relationships#destroy", @response.body + assert_equal "/relationships/1", relationship_path(1) + + patch "/friendships/1" + assert_equal "friendships#update", @response.body + assert_equal "/friendships/1", friendship_path(1) + + put "/friendships/1" + assert_equal "friendships#update", @response.body + assert_equal "/friendships/1", friendship_path(1) + end + + def test_resource_with_slugs_in_ids + draw do + resources :posts + end + + get "/posts/rails-rocks" + assert_equal "posts#show", @response.body + assert_equal "/posts/rails-rocks", post_path(id: "rails-rocks") + end + + def test_resources_for_uncountable_names + draw do + resources :sheep do + get "_it", on: :member + end + end + + assert_equal "/sheep", sheep_index_path + assert_equal "/sheep/1", sheep_path(1) + assert_equal "/sheep/new", new_sheep_path + assert_equal "/sheep/1/edit", edit_sheep_path(1) + assert_equal "/sheep/1/_it", _it_sheep_path(1) + end + + def test_resource_does_not_modify_passed_options + options = { id: /.+?/, format: /json|xml/ } + draw { resource :user, options } + assert_equal({ id: /.+?/, format: /json|xml/ }, options) + end + + def test_resources_does_not_modify_passed_options + options = { id: /.+?/, format: /json|xml/ } + draw { resources :users, options } + assert_equal({ id: /.+?/, format: /json|xml/ }, options) + end + + def test_path_names + draw do + scope "pt", as: "pt" do + resources :projects, path_names: { edit: "editar", new: "novo" }, path: "projetos" + resource :admin, path_names: { new: "novo", activate: "ativar" }, path: "administrador" do + put :activate, on: :member + end + end + end + + get "/pt/projetos" + assert_equal "projects#index", @response.body + assert_equal "/pt/projetos", pt_projects_path + + get "/pt/projetos/1/editar" + assert_equal "projects#edit", @response.body + assert_equal "/pt/projetos/1/editar", edit_pt_project_path(1) + + get "/pt/administrador" + assert_equal "admins#show", @response.body + assert_equal "/pt/administrador", pt_admin_path + + get "/pt/administrador/novo" + assert_equal "admins#new", @response.body + assert_equal "/pt/administrador/novo", new_pt_admin_path + + put "/pt/administrador/ativar" + assert_equal "admins#activate", @response.body + assert_equal "/pt/administrador/ativar", activate_pt_admin_path + end + + def test_path_option_override + draw do + scope "pt", as: "pt" do + resources :projects, path_names: { new: "novo" }, path: "projetos" do + put :close, on: :member, path: "fechar" + get :open, on: :new, path: "abrir" + end + end + end + + get "/pt/projetos/novo/abrir" + assert_equal "projects#open", @response.body + assert_equal "/pt/projetos/novo/abrir", open_new_pt_project_path + + put "/pt/projetos/1/fechar" + assert_equal "projects#close", @response.body + assert_equal "/pt/projetos/1/fechar", close_pt_project_path(1) + end + + def test_sprockets + draw do + get "sprockets.js" => ::TestRoutingMapper::SprocketsApp + end + + get "/sprockets.js" + assert_equal "javascripts", @response.body + end + + def test_update_person_route + draw do + get "people/:id/update", to: "people#update", as: :update_person + end + + get "/people/1/update" + assert_equal "people#update", @response.body + + assert_equal "/people/1/update", update_person_path(id: 1) + end + + def test_update_project_person + draw do + get "/projects/:project_id/people/:id/update", to: "people#update", as: :update_project_person + end + + get "/projects/1/people/2/update" + assert_equal "people#update", @response.body + + assert_equal "/projects/1/people/2/update", update_project_person_path(project_id: 1, id: 2) + end + + def test_forum_products + draw do + namespace :forum do + resources :products, path: "" do + resources :questions + end + end + end + + get "/forum" + assert_equal "forum/products#index", @response.body + assert_equal "/forum", forum_products_path + + get "/forum/basecamp" + assert_equal "forum/products#show", @response.body + assert_equal "/forum/basecamp", forum_product_path(id: "basecamp") + + get "/forum/basecamp/questions" + assert_equal "forum/questions#index", @response.body + assert_equal "/forum/basecamp/questions", forum_product_questions_path(product_id: "basecamp") + + get "/forum/basecamp/questions/1" + assert_equal "forum/questions#show", @response.body + assert_equal "/forum/basecamp/questions/1", forum_product_question_path(product_id: "basecamp", id: 1) + end + + def test_articles_perma + draw do + get "articles/:year/:month/:day/:title", to: "articles#show", as: :article + end + + get "/articles/2009/08/18/rails-3" + assert_equal "articles#show", @response.body + + assert_equal "/articles/2009/8/18/rails-3", article_path(year: 2009, month: 8, day: 18, title: "rails-3") + end + + def test_account_namespace + draw do + namespace :account do + resource :subscription, :credit, :credit_card + end + end + + get "/account/subscription" + assert_equal "account/subscriptions#show", @response.body + assert_equal "/account/subscription", account_subscription_path + + get "/account/credit" + assert_equal "account/credits#show", @response.body + assert_equal "/account/credit", account_credit_path + + get "/account/credit_card" + assert_equal "account/credit_cards#show", @response.body + assert_equal "/account/credit_card", account_credit_card_path + end + + def test_nested_namespace + draw do + namespace :account do + namespace :admin do + resource :subscription + end + end + end + + get "/account/admin/subscription" + assert_equal "account/admin/subscriptions#show", @response.body + assert_equal "/account/admin/subscription", account_admin_subscription_path + end + + def test_namespace_nested_in_resources + draw do + resources :clients do + namespace :google do + resource :account do + namespace :secret do + resource :info + end + end + end + end + end + + get "/clients/1/google/account" + assert_equal "/clients/1/google/account", client_google_account_path(1) + assert_equal "google/accounts#show", @response.body + + get "/clients/1/google/account/secret/info" + assert_equal "/clients/1/google/account/secret/info", client_google_account_secret_info_path(1) + assert_equal "google/secret/infos#show", @response.body + end + + def test_namespace_with_options + draw do + namespace :users, path: "usuarios" do + root to: "home#index" + end + end + + get "/usuarios" + assert_equal "/usuarios", users_root_path + assert_equal "users/home#index", @response.body + end + + def test_namespaced_shallow_routes_with_module_option + draw do + namespace :foo, module: "bar" do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get "/foo/posts" + assert_equal "/foo/posts", foo_posts_path + assert_equal "bar/posts#index", @response.body + + get "/foo/posts/1" + assert_equal "/foo/posts/1", foo_post_path("1") + assert_equal "bar/posts#show", @response.body + + get "/foo/posts/1/comments" + assert_equal "/foo/posts/1/comments", foo_post_comments_path("1") + assert_equal "bar/comments#index", @response.body + + get "/foo/comments/2" + assert_equal "/foo/comments/2", foo_comment_path("2") + assert_equal "bar/comments#show", @response.body + end + + def test_namespaced_shallow_routes_with_path_option + draw do + namespace :foo, path: "bar" do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get "/bar/posts" + assert_equal "/bar/posts", foo_posts_path + assert_equal "foo/posts#index", @response.body + + get "/bar/posts/1" + assert_equal "/bar/posts/1", foo_post_path("1") + assert_equal "foo/posts#show", @response.body + + get "/bar/posts/1/comments" + assert_equal "/bar/posts/1/comments", foo_post_comments_path("1") + assert_equal "foo/comments#index", @response.body + + get "/bar/comments/2" + assert_equal "/bar/comments/2", foo_comment_path("2") + assert_equal "foo/comments#show", @response.body + end + + def test_namespaced_shallow_routes_with_as_option + draw do + namespace :foo, as: "bar" do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get "/foo/posts" + assert_equal "/foo/posts", bar_posts_path + assert_equal "foo/posts#index", @response.body + + get "/foo/posts/1" + assert_equal "/foo/posts/1", bar_post_path("1") + assert_equal "foo/posts#show", @response.body + + get "/foo/posts/1/comments" + assert_equal "/foo/posts/1/comments", bar_post_comments_path("1") + assert_equal "foo/comments#index", @response.body + + get "/foo/comments/2" + assert_equal "/foo/comments/2", bar_comment_path("2") + assert_equal "foo/comments#show", @response.body + end + + def test_namespaced_shallow_routes_with_shallow_path_option + draw do + namespace :foo, shallow_path: "bar" do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get "/foo/posts" + assert_equal "/foo/posts", foo_posts_path + assert_equal "foo/posts#index", @response.body + + get "/foo/posts/1" + assert_equal "/foo/posts/1", foo_post_path("1") + assert_equal "foo/posts#show", @response.body + + get "/foo/posts/1/comments" + assert_equal "/foo/posts/1/comments", foo_post_comments_path("1") + assert_equal "foo/comments#index", @response.body + + get "/bar/comments/2" + assert_equal "/bar/comments/2", foo_comment_path("2") + assert_equal "foo/comments#show", @response.body + end + + def test_namespaced_shallow_routes_with_shallow_prefix_option + draw do + namespace :foo, shallow_prefix: "bar" do + resources :posts, only: [:index, :show] do + resources :comments, only: [:index, :show], shallow: true + end + end + end + + get "/foo/posts" + assert_equal "/foo/posts", foo_posts_path + assert_equal "foo/posts#index", @response.body + + get "/foo/posts/1" + assert_equal "/foo/posts/1", foo_post_path("1") + assert_equal "foo/posts#show", @response.body + + get "/foo/posts/1/comments" + assert_equal "/foo/posts/1/comments", foo_post_comments_path("1") + assert_equal "foo/comments#index", @response.body + + get "/foo/comments/2" + assert_equal "/foo/comments/2", bar_comment_path("2") + assert_equal "foo/comments#show", @response.body + end + + def test_namespace_containing_numbers + draw do + namespace :v2 do + resources :subscriptions + end + end + + get "/v2/subscriptions" + assert_equal "v2/subscriptions#index", @response.body + assert_equal "/v2/subscriptions", v2_subscriptions_path + end + + def test_articles_with_id + draw do + controller :articles do + scope "/articles", as: "article" do + scope path: "/:title", title: /[a-z]+/, as: :with_title do + get "/:id", action: :with_id, as: "" + end + end + end + end + + get "/articles/rails/1" + assert_equal "articles#with_id", @response.body + + get "/articles/123/1" + assert_equal "pass", @response.headers["X-Cascade"] + + assert_equal "/articles/rails/1", article_with_title_path(title: "rails", id: 1) + end + + def test_access_token_rooms + draw do + scope ":access_token", constraints: { access_token: /\w{5,5}/ } do + resources :rooms + end + end + + get "/12345/rooms" + assert_equal "rooms#index", @response.body + + get "/12345/rooms/1" + assert_equal "rooms#show", @response.body + + get "/12345/rooms/1/edit" + assert_equal "rooms#edit", @response.body + end + + def test_root + draw do + root to: "projects#index" + end + + assert_equal "/", root_path + get "/" + assert_equal "projects#index", @response.body + end + + def test_scoped_root + draw do + scope "(:locale)", locale: /en|pl/ do + root to: "projects#index" + end + end + + assert_equal "/en", root_path(locale: "en") + get "/en" + assert_equal "projects#index", @response.body + end + + def test_scoped_root_as_name + draw do + scope "(:locale)", locale: /en|pl/ do + root to: "projects#index", as: "projects" + end + end + + assert_equal "/en", projects_path(locale: "en") + assert_equal "/", projects_path + get "/en" + assert_equal "projects#index", @response.body + end + + def test_optionally_scoped_root_unscoped_access + draw do + scope "(:locale)" do + scope "(:platform)" do + scope "(:browser)" do + root to: "projects#index" + end + end + end + end + + assert_equal "/", root_path + get "/" + assert_equal "projects#index", @response.body + end + + def test_scope_with_format_option + draw do + get "direct/index", as: :no_format_direct, format: false + + scope format: false do + get "scoped/index", as: :no_format_scoped + end + end + + assert_equal "/direct/index", no_format_direct_path + assert_equal "/direct/index?format=html", no_format_direct_path(format: "html") + + assert_equal "/scoped/index", no_format_scoped_path + assert_equal "/scoped/index?format=html", no_format_scoped_path(format: "html") + + get "/scoped/index" + assert_equal "scoped#index", @response.body + + get "/scoped/index.html" + assert_equal "Not Found", @response.body + end + + def test_resources_with_format_false_from_scope + draw do + scope format: false do + resources :posts + resource :user + end + end + + get "/posts" + assert_response :success + assert_equal "posts#index", @response.body + assert_equal "/posts", posts_path + + get "/posts.html" + assert_response :not_found + assert_equal "Not Found", @response.body + assert_equal "/posts?format=html", posts_path(format: "html") + + get "/user" + assert_response :success + assert_equal "users#show", @response.body + assert_equal "/user", user_path + + get "/user.html" + assert_response :not_found + assert_equal "Not Found", @response.body + assert_equal "/user?format=html", user_path(format: "html") + end + + def test_index + draw do + get "/info" => "projects#info", :as => "info" + end + + assert_equal "/info", info_path + get "/info" + assert_equal "projects#info", @response.body + end + + def test_match_with_many_paths_containing_a_slash + draw do + get "get/first", "get/second", "get/third", to: "get#show" + end + + get "/get/first" + assert_equal "get#show", @response.body + + get "/get/second" + assert_equal "get#show", @response.body + + get "/get/third" + assert_equal "get#show", @response.body + end + + def test_match_shorthand_with_no_scope + draw do + get "account/overview" + end + + assert_equal "/account/overview", account_overview_path + get "/account/overview" + assert_equal "account#overview", @response.body + end + + def test_match_shorthand_inside_namespace + draw do + namespace :account do + get "shorthand" + end + end + + assert_equal "/account/shorthand", account_shorthand_path + get "/account/shorthand" + assert_equal "account#shorthand", @response.body + end + + def test_match_shorthand_with_multiple_paths_inside_namespace + draw do + namespace :proposals do + put "activate", "inactivate" + end + end + + put "/proposals/activate" + assert_equal "proposals#activate", @response.body + + put "/proposals/inactivate" + assert_equal "proposals#inactivate", @response.body + end + + def test_match_shorthand_inside_namespace_with_controller + draw do + namespace :api do + get "products/list" + end + end + + assert_equal "/api/products/list", api_products_list_path + get "/api/products/list" + assert_equal "api/products#list", @response.body + end + + def test_match_shorthand_inside_scope_with_variables_with_controller + draw do + scope ":locale" do + match "questions/new", via: [:get] + end + end + + get "/de/questions/new" + assert_equal "questions#new", @response.body + assert_equal "de", @request.params[:locale] + end + + def test_match_shorthand_inside_nested_namespaces_and_scopes_with_controller + draw do + namespace :api do + namespace :v3 do + scope ":locale" do + get "products/list" + end + end + end + end + + get "/api/v3/en/products/list" + assert_equal "api/v3/products#list", @response.body + end + + def test_not_matching_shorthand_with_dynamic_parameters + draw do + ActiveSupport::Deprecation.silence do + get ":controller/:action/admin" + end + end + + get "/finances/overview/admin" + assert_equal "finances#overview", @response.body + end + + def test_controller_option_with_nesting_and_leading_slash + draw do + scope "/job", controller: "job" do + scope ":id", action: "manage_applicant" do + get "/active" + end + end + end + + get "/job/5/active" + assert_equal "job#manage_applicant", @response.body + end + + def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper + draw do + resources :replies do + collection do + get "page/:page" => "replies#index", :page => %r{\d+} + get ":page" => "replies#index", :page => %r{\d+} + end + end + end + + assert_equal "/replies", replies_path + end + + def test_scoped_controller_with_namespace_and_action + draw do + namespace :account do + ActiveSupport::Deprecation.silence do + get ":action/callback", action: /twitter|github/, controller: "callbacks", as: :callback + end + end + end + + assert_equal "/account/twitter/callback", account_callback_path("twitter") + get "/account/twitter/callback" + assert_equal "account/callbacks#twitter", @response.body + + get "/account/whatever/callback" + assert_equal "Not Found", @response.body + end + + def test_convention_match_nested_and_with_leading_slash + draw do + get "/account/nested/overview" + end + + assert_equal "/account/nested/overview", account_nested_overview_path + get "/account/nested/overview" + assert_equal "account/nested#overview", @response.body + end + + def test_convention_with_explicit_end + draw do + get "sign_in" => "sessions#new" + end + + get "/sign_in" + assert_equal "sessions#new", @response.body + assert_equal "/sign_in", sign_in_path + end + + def test_redirect_with_complete_url_and_status + draw do + get "account/google" => redirect("http://www.google.com/", status: 302) + end + + get "/account/google" + verify_redirect "http://www.google.com/", 302 + end + + def test_redirect_with_port + draw do + get "account/login", to: redirect("/login") + end + + previous_host, self.host = host, "www.example.com:3000" + + get "/account/login" + verify_redirect "http://www.example.com:3000/login" + ensure + self.host = previous_host + end + + def test_normalize_namespaced_matches + draw do + namespace :account do + get "description", action: :description, as: "description" + end + end + + assert_equal "/account/description", account_description_path + + get "/account/description" + assert_equal "account#description", @response.body + end + + def test_namespaced_roots + draw do + namespace :account do + root to: "account#index" + end + end + + assert_equal "/account", account_root_path + get "/account" + assert_equal "account/account#index", @response.body + end + + def test_optional_scoped_root + draw do + scope "(:locale)", locale: /en|pl/ do + root to: "projects#index" + end + end + + assert_equal "/en", root_path("en") + get "/en" + assert_equal "projects#index", @response.body + end + + def test_optional_scoped_path + draw do + scope "(:locale)", locale: /en|pl/ do + resources :descriptions + end + end + + assert_equal "/en/descriptions", descriptions_path("en") + assert_equal "/descriptions", descriptions_path(nil) + assert_equal "/en/descriptions/1", description_path("en", 1) + assert_equal "/descriptions/1", description_path(nil, 1) + + get "/en/descriptions" + assert_equal "descriptions#index", @response.body + + get "/descriptions" + assert_equal "descriptions#index", @response.body + + get "/en/descriptions/1" + assert_equal "descriptions#show", @response.body + + get "/descriptions/1" + assert_equal "descriptions#show", @response.body + end + + def test_nested_optional_scoped_path + draw do + namespace :admin do + scope "(:locale)", locale: /en|pl/ do + resources :descriptions + end + end + end + + assert_equal "/admin/en/descriptions", admin_descriptions_path("en") + assert_equal "/admin/descriptions", admin_descriptions_path(nil) + assert_equal "/admin/en/descriptions/1", admin_description_path("en", 1) + assert_equal "/admin/descriptions/1", admin_description_path(nil, 1) + + get "/admin/en/descriptions" + assert_equal "admin/descriptions#index", @response.body + + get "/admin/descriptions" + assert_equal "admin/descriptions#index", @response.body + + get "/admin/en/descriptions/1" + assert_equal "admin/descriptions#show", @response.body + + get "/admin/descriptions/1" + assert_equal "admin/descriptions#show", @response.body + end + + def test_nested_optional_path_shorthand + draw do + scope "(:locale)", locale: /en|pl/ do + get "registrations/new" + end + end + + get "/registrations/new" + assert_nil @request.params[:locale] + + get "/en/registrations/new" + assert_equal "en", @request.params[:locale] + end + + def test_default_string_params + draw do + get "inline_pages/(:id)", to: "pages#show", id: "home" + get "default_pages/(:id)", to: "pages#show", defaults: { id: "home" } + + defaults id: "home" do + get "scoped_pages/(:id)", to: "pages#show" + end + end + + get "/inline_pages" + assert_equal "home", @request.params[:id] + + get "/default_pages" + assert_equal "home", @request.params[:id] + + get "/scoped_pages" + assert_equal "home", @request.params[:id] + end + + def test_default_integer_params + draw do + get "inline_pages/(:page)", to: "pages#show", page: 1 + get "default_pages/(:page)", to: "pages#show", defaults: { page: 1 } + + defaults page: 1 do + get "scoped_pages/(:page)", to: "pages#show" + end + end + + get "/inline_pages" + assert_equal 1, @request.params[:page] + + get "/default_pages" + assert_equal 1, @request.params[:page] + + get "/scoped_pages" + assert_equal 1, @request.params[:page] + end + + def test_keyed_default_string_params_with_match + draw do + match "/", to: "pages#show", via: :get, defaults: { id: "home" } + end + + get "/" + assert_equal "home", @request.params[:id] + end + + def test_default_string_params_with_match + draw do + match "/", to: "pages#show", via: :get, id: "home" + end + + get "/" + assert_equal "home", @request.params[:id] + end + + def test_keyed_default_string_params_with_root + draw do + root to: "pages#show", defaults: { id: "home" } + end + + get "/" + assert_equal "home", @request.params[:id] + end + + def test_default_string_params_with_root + draw do + root to: "pages#show", id: "home" + end + + get "/" + assert_equal "home", @request.params[:id] + end + + def test_resource_constraints + draw do + resources :products, constraints: { id: /\d{4}/ } do + root to: "products#root" + get :favorite, on: :collection + resources :images + end + + resource :dashboard, constraints: { ip: /192\.168\.1\.\d{1,3}/ } + end + + get "/products/1" + assert_equal "pass", @response.headers["X-Cascade"] + get "/products" + assert_equal "products#root", @response.body + get "/products/favorite" + assert_equal "products#favorite", @response.body + get "/products/0001" + assert_equal "products#show", @response.body + + get "/products/1/images" + assert_equal "pass", @response.headers["X-Cascade"] + get "/products/0001/images" + assert_equal "images#index", @response.body + get "/products/0001/images/0001" + assert_equal "images#show", @response.body + + get "/dashboard", headers: { "REMOTE_ADDR" => "10.0.0.100" } + assert_equal "pass", @response.headers["X-Cascade"] + get "/dashboard", headers: { "REMOTE_ADDR" => "192.168.1.100" } + assert_equal "dashboards#show", @response.body + end + + def test_root_works_in_the_resources_scope + draw do + resources :products do + root to: "products#root" + end + end + + get "/products" + assert_equal "products#root", @response.body + assert_equal "/products", products_root_path + end + + def test_module_scope + draw do + resource :token, module: :api + end + + get "/token" + assert_equal "api/tokens#show", @response.body + assert_equal "/token", token_path + end + + def test_path_scope + draw do + scope path: "api" do + resource :me + get "/" => "mes#index" + end + end + + get "/api/me" + assert_equal "mes#show", @response.body + assert_equal "/api/me", me_path + + get "/api" + assert_equal "mes#index", @response.body + end + + def test_symbol_scope + draw do + scope path: "api" do + scope :v2 do + resource :me, as: "v2_me" + get "/" => "mes#index" + end + + scope :v3, :admin do + resource :me, as: "v3_me" + end + end + end + + get "/api/v2/me" + assert_equal "mes#show", @response.body + assert_equal "/api/v2/me", v2_me_path + + get "/api/v2" + assert_equal "mes#index", @response.body + + get "/api/v3/admin/me" + assert_equal "mes#show", @response.body + end + + def test_url_generator_for_generic_route + draw do + ActiveSupport::Deprecation.silence do + get "whatever/:controller(/:action(/:id))" + end + end + + get "/whatever/foo/bar" + assert_equal "foo#bar", @response.body + + assert_equal "http://www.example.com/whatever/foo/bar/1", + url_for(controller: "foo", action: "bar", id: 1) + end + + def test_url_generator_for_namespaced_generic_route + draw do + ActiveSupport::Deprecation.silence do + get "whatever/:controller(/:action(/:id))", id: /\d+/ + end + end + + get "/whatever/foo/bar/show" + assert_equal "foo/bar#show", @response.body + + get "/whatever/foo/bar/show/1" + assert_equal "foo/bar#show", @response.body + + assert_equal "http://www.example.com/whatever/foo/bar/show", + url_for(controller: "foo/bar", action: "show") + + assert_equal "http://www.example.com/whatever/foo/bar/show/1", + url_for(controller: "foo/bar", action: "show", id: "1") + end + + def test_resource_new_actions + draw do + resources :replies do + new do + post :preview + end + end + + scope "pt", as: "pt" do + resources :projects, path_names: { new: "novo" }, path: "projetos" do + post :preview, on: :new + end + + resource :admin, path_names: { new: "novo" }, path: "administrador" do + post :preview, on: :new + end + + resources :products, path_names: { new: "novo" } do + new do + post :preview + end + end + end + + resource :profile do + new do + post :preview + end + end + end + + assert_equal "/replies/new/preview", preview_new_reply_path + assert_equal "/pt/projetos/novo/preview", preview_new_pt_project_path + assert_equal "/pt/administrador/novo/preview", preview_new_pt_admin_path + assert_equal "/pt/products/novo/preview", preview_new_pt_product_path + assert_equal "/profile/new/preview", preview_new_profile_path + + post "/replies/new/preview" + assert_equal "replies#preview", @response.body + + post "/pt/projetos/novo/preview" + assert_equal "projects#preview", @response.body + + post "/pt/administrador/novo/preview" + assert_equal "admins#preview", @response.body + + post "/pt/products/novo/preview" + assert_equal "products#preview", @response.body + + post "/profile/new/preview" + assert_equal "profiles#preview", @response.body + end + + def test_resource_merges_options_from_scope + draw do + scope only: :show do + resource :account + end + end + + assert_raise(NoMethodError) { new_account_path } + + get "/account/new" + assert_equal 404, status + end + + def test_resources_merges_options_from_scope + draw do + scope only: [:index, :show] do + resources :products do + resources :images + end + end + end + + assert_raise(NoMethodError) { edit_product_path("1") } + + get "/products/1/edit" + assert_equal 404, status + + assert_raise(NoMethodError) { edit_product_image_path("1", "2") } + + post "/products/1/images/2/edit" + assert_equal 404, status + end + + def test_shallow_nested_resources + draw do + shallow do + namespace :api do + resources :teams do + resources :players + resource :captain + end + end + end + + resources :threads, shallow: true do + resource :owner + resources :messages do + resources :comments do + member do + post :preview + end + end + end + end + end + + get "/api/teams" + assert_equal "api/teams#index", @response.body + assert_equal "/api/teams", api_teams_path + + get "/api/teams/new" + assert_equal "api/teams#new", @response.body + assert_equal "/api/teams/new", new_api_team_path + + get "/api/teams/1" + assert_equal "api/teams#show", @response.body + assert_equal "/api/teams/1", api_team_path(id: "1") + + get "/api/teams/1/edit" + assert_equal "api/teams#edit", @response.body + assert_equal "/api/teams/1/edit", edit_api_team_path(id: "1") + + get "/api/teams/1/players" + assert_equal "api/players#index", @response.body + assert_equal "/api/teams/1/players", api_team_players_path(team_id: "1") + + get "/api/teams/1/players/new" + assert_equal "api/players#new", @response.body + assert_equal "/api/teams/1/players/new", new_api_team_player_path(team_id: "1") + + get "/api/players/2" + assert_equal "api/players#show", @response.body + assert_equal "/api/players/2", api_player_path(id: "2") + + get "/api/players/2/edit" + assert_equal "api/players#edit", @response.body + assert_equal "/api/players/2/edit", edit_api_player_path(id: "2") + + get "/api/teams/1/captain" + assert_equal "api/captains#show", @response.body + assert_equal "/api/teams/1/captain", api_team_captain_path(team_id: "1") + + get "/api/teams/1/captain/new" + assert_equal "api/captains#new", @response.body + assert_equal "/api/teams/1/captain/new", new_api_team_captain_path(team_id: "1") + + get "/api/teams/1/captain/edit" + assert_equal "api/captains#edit", @response.body + assert_equal "/api/teams/1/captain/edit", edit_api_team_captain_path(team_id: "1") + + get "/threads" + assert_equal "threads#index", @response.body + assert_equal "/threads", threads_path + + get "/threads/new" + assert_equal "threads#new", @response.body + assert_equal "/threads/new", new_thread_path + + get "/threads/1" + assert_equal "threads#show", @response.body + assert_equal "/threads/1", thread_path(id: "1") + + get "/threads/1/edit" + assert_equal "threads#edit", @response.body + assert_equal "/threads/1/edit", edit_thread_path(id: "1") + + get "/threads/1/owner" + assert_equal "owners#show", @response.body + assert_equal "/threads/1/owner", thread_owner_path(thread_id: "1") + + get "/threads/1/messages" + assert_equal "messages#index", @response.body + assert_equal "/threads/1/messages", thread_messages_path(thread_id: "1") + + get "/threads/1/messages/new" + assert_equal "messages#new", @response.body + assert_equal "/threads/1/messages/new", new_thread_message_path(thread_id: "1") + + get "/messages/2" + assert_equal "messages#show", @response.body + assert_equal "/messages/2", message_path(id: "2") + + get "/messages/2/edit" + assert_equal "messages#edit", @response.body + assert_equal "/messages/2/edit", edit_message_path(id: "2") + + get "/messages/2/comments" + assert_equal "comments#index", @response.body + assert_equal "/messages/2/comments", message_comments_path(message_id: "2") + + get "/messages/2/comments/new" + assert_equal "comments#new", @response.body + assert_equal "/messages/2/comments/new", new_message_comment_path(message_id: "2") + + get "/comments/3" + assert_equal "comments#show", @response.body + assert_equal "/comments/3", comment_path(id: "3") + + get "/comments/3/edit" + assert_equal "comments#edit", @response.body + assert_equal "/comments/3/edit", edit_comment_path(id: "3") + + post "/comments/3/preview" + assert_equal "comments#preview", @response.body + assert_equal "/comments/3/preview", preview_comment_path(id: "3") + end + + def test_shallow_nested_resources_inside_resource + draw do + resource :membership, shallow: true do + resources :cards + end + end + + get "/membership/cards" + assert_equal "cards#index", @response.body + assert_equal "/membership/cards", membership_cards_path + + get "/membership/cards/new" + assert_equal "cards#new", @response.body + assert_equal "/membership/cards/new", new_membership_card_path + + post "/membership/cards" + assert_equal "cards#create", @response.body + + get "/cards/1" + assert_equal "cards#show", @response.body + assert_equal "/cards/1", card_path("1") + + get "/cards/1/edit" + assert_equal "cards#edit", @response.body + assert_equal "/cards/1/edit", edit_card_path("1") + + put "/cards/1" + assert_equal "cards#update", @response.body + + patch "/cards/1" + assert_equal "cards#update", @response.body + + delete "/cards/1" + assert_equal "cards#destroy", @response.body + end + + def test_shallow_deeply_nested_resources + draw do + resources :blogs do + resources :posts do + resources :comments, shallow: true + end + end + end + + get "/comments/1" + assert_equal "comments#show", @response.body + + assert_equal "/comments/1", comment_path("1") + assert_equal "/blogs/new", new_blog_path + assert_equal "/blogs/1/posts/new", new_blog_post_path(blog_id: 1) + assert_equal "/blogs/1/posts/2/comments/new", new_blog_post_comment_path(blog_id: 1, post_id: 2) + end + + def test_direct_children_of_shallow_resources + draw do + resources :blogs do + resources :posts, shallow: true do + resources :comments + end + end + end + + post "/posts/1/comments" + assert_equal "comments#create", @response.body + assert_equal "/posts/1/comments", post_comments_path("1") + + get "/posts/2/comments/new" + assert_equal "comments#new", @response.body + assert_equal "/posts/2/comments/new", new_post_comment_path("2") + + get "/posts/1/comments" + assert_equal "comments#index", @response.body + assert_equal "/posts/1/comments", post_comments_path("1") + end + + def test_shallow_nested_resources_within_scope + draw do + scope "/hello" do + shallow do + resources :notes do + resources :trackbacks + end + end + end + end + + get "/hello/notes/1/trackbacks" + assert_equal "trackbacks#index", @response.body + assert_equal "/hello/notes/1/trackbacks", note_trackbacks_path(note_id: 1) + + get "/hello/notes/1/edit" + assert_equal "notes#edit", @response.body + assert_equal "/hello/notes/1/edit", edit_note_path(id: "1") + + get "/hello/notes/1/trackbacks/new" + assert_equal "trackbacks#new", @response.body + assert_equal "/hello/notes/1/trackbacks/new", new_note_trackback_path(note_id: 1) + + get "/hello/trackbacks/1" + assert_equal "trackbacks#show", @response.body + assert_equal "/hello/trackbacks/1", trackback_path(id: "1") + + get "/hello/trackbacks/1/edit" + assert_equal "trackbacks#edit", @response.body + assert_equal "/hello/trackbacks/1/edit", edit_trackback_path(id: "1") + + put "/hello/trackbacks/1" + assert_equal "trackbacks#update", @response.body + + post "/hello/notes/1/trackbacks" + assert_equal "trackbacks#create", @response.body + + delete "/hello/trackbacks/1" + assert_equal "trackbacks#destroy", @response.body + + get "/hello/notes" + assert_equal "notes#index", @response.body + + post "/hello/notes" + assert_equal "notes#create", @response.body + + get "/hello/notes/new" + assert_equal "notes#new", @response.body + assert_equal "/hello/notes/new", new_note_path + + get "/hello/notes/1" + assert_equal "notes#show", @response.body + assert_equal "/hello/notes/1", note_path(id: 1) + + put "/hello/notes/1" + assert_equal "notes#update", @response.body + + delete "/hello/notes/1" + assert_equal "notes#destroy", @response.body + end + + def test_shallow_option_nested_resources_within_scope + draw do + scope "/hello" do + resources :notes, shallow: true do + resources :trackbacks + end + end + end + + get "/hello/notes/1/trackbacks" + assert_equal "trackbacks#index", @response.body + assert_equal "/hello/notes/1/trackbacks", note_trackbacks_path(note_id: 1) + + get "/hello/notes/1/edit" + assert_equal "notes#edit", @response.body + assert_equal "/hello/notes/1/edit", edit_note_path(id: "1") + + get "/hello/notes/1/trackbacks/new" + assert_equal "trackbacks#new", @response.body + assert_equal "/hello/notes/1/trackbacks/new", new_note_trackback_path(note_id: 1) + + get "/hello/trackbacks/1" + assert_equal "trackbacks#show", @response.body + assert_equal "/hello/trackbacks/1", trackback_path(id: "1") + + get "/hello/trackbacks/1/edit" + assert_equal "trackbacks#edit", @response.body + assert_equal "/hello/trackbacks/1/edit", edit_trackback_path(id: "1") + + put "/hello/trackbacks/1" + assert_equal "trackbacks#update", @response.body + + post "/hello/notes/1/trackbacks" + assert_equal "trackbacks#create", @response.body + + delete "/hello/trackbacks/1" + assert_equal "trackbacks#destroy", @response.body + + get "/hello/notes" + assert_equal "notes#index", @response.body + + post "/hello/notes" + assert_equal "notes#create", @response.body + + get "/hello/notes/new" + assert_equal "notes#new", @response.body + assert_equal "/hello/notes/new", new_note_path + + get "/hello/notes/1" + assert_equal "notes#show", @response.body + assert_equal "/hello/notes/1", note_path(id: 1) + + put "/hello/notes/1" + assert_equal "notes#update", @response.body + + delete "/hello/notes/1" + assert_equal "notes#destroy", @response.body + end + + def test_custom_resource_routes_are_scoped + draw do + resources :customers do + get :recent, on: :collection + get "profile", on: :member + get "secret/profile" => "customers#secret", :on => :member + post "preview" => "customers#preview", :as => :another_preview, :on => :new + resource :avatar do + get "thumbnail" => "avatars#thumbnail", :as => :thumbnail, :on => :member + end + resources :invoices do + get "outstanding" => "invoices#outstanding", :on => :collection + get "overdue", action: :overdue, on: :collection + get "print" => "invoices#print", :as => :print, :on => :member + post "preview" => "invoices#preview", :as => :preview, :on => :new + end + resources :notes, shallow: true do + get "preview" => "notes#preview", :as => :preview, :on => :new + get "print" => "notes#print", :as => :print, :on => :member + end + end + + namespace :api do + resources :customers do + get "recent" => "customers#recent", :as => :recent, :on => :collection + get "profile" => "customers#profile", :as => :profile, :on => :member + post "preview" => "customers#preview", :as => :preview, :on => :new + end + end + end + + assert_equal "/customers/recent", recent_customers_path + assert_equal "/customers/1/profile", profile_customer_path(id: "1") + assert_equal "/customers/1/secret/profile", secret_profile_customer_path(id: "1") + assert_equal "/customers/new/preview", another_preview_new_customer_path + assert_equal "/customers/1/avatar/thumbnail.jpg", thumbnail_customer_avatar_path(customer_id: "1", format: :jpg) + assert_equal "/customers/1/invoices/outstanding", outstanding_customer_invoices_path(customer_id: "1") + assert_equal "/customers/1/invoices/2/print", print_customer_invoice_path(customer_id: "1", id: "2") + assert_equal "/customers/1/invoices/new/preview", preview_new_customer_invoice_path(customer_id: "1") + assert_equal "/customers/1/notes/new/preview", preview_new_customer_note_path(customer_id: "1") + assert_equal "/notes/1/print", print_note_path(id: "1") + assert_equal "/api/customers/recent", recent_api_customers_path + assert_equal "/api/customers/1/profile", profile_api_customer_path(id: "1") + assert_equal "/api/customers/new/preview", preview_new_api_customer_path + + get "/customers/1/invoices/overdue" + assert_equal "invoices#overdue", @response.body + + get "/customers/1/secret/profile" + assert_equal "customers#secret", @response.body + end + + def test_shallow_nested_routes_ignore_module + draw do + scope module: :api do + resources :errors, shallow: true do + resources :notices + end + end + end + + get "/errors/1/notices" + assert_equal "api/notices#index", @response.body + assert_equal "/errors/1/notices", error_notices_path(error_id: "1") + + get "/notices/1" + assert_equal "api/notices#show", @response.body + assert_equal "/notices/1", notice_path(id: "1") + end + + def test_non_greedy_regexp + draw do + namespace :api do + scope(":version", version: /.+/) do + resources :users, id: /.+?/, format: /json|xml/ + end + end + end + + get "/api/1.0/users" + assert_equal "api/users#index", @response.body + assert_equal "/api/1.0/users", api_users_path(version: "1.0") + + get "/api/1.0/users.json" + assert_equal "api/users#index", @response.body + assert_equal true, @request.format.json? + assert_equal "/api/1.0/users.json", api_users_path(version: "1.0", format: :json) + + get "/api/1.0/users/first.last" + assert_equal "api/users#show", @response.body + assert_equal "first.last", @request.params[:id] + assert_equal "/api/1.0/users/first.last", api_user_path(version: "1.0", id: "first.last") + + get "/api/1.0/users/first.last.xml" + assert_equal "api/users#show", @response.body + assert_equal "first.last", @request.params[:id] + assert_equal true, @request.format.xml? + assert_equal "/api/1.0/users/first.last.xml", api_user_path(version: "1.0", id: "first.last", format: :xml) + end + + def test_match_without_via + assert_raises(ArgumentError) do + draw do + match "/foo/bar", to: "files#show" + end + end + end + + def test_match_with_empty_via + assert_raises(ArgumentError) do + draw do + match "/foo/bar", to: "files#show", via: [] + end + end + end + + def test_glob_parameter_accepts_regexp + draw do + get "/:locale/*file.:format", to: "files#show", file: /path\/to\/existing\/file/ + end + + get "/en/path/to/existing/file.html" + assert_equal 200, @response.status + end + + def test_resources_controller_name_is_not_pluralized + draw do + resources :content + end + + get "/content" + assert_equal "content#index", @response.body + end + + def test_url_generator_for_optional_prefix_dynamic_segment + draw do + get "(/:username)/followers" => "followers#index" + end + + get "/bob/followers" + assert_equal "followers#index", @response.body + assert_equal "http://www.example.com/bob/followers", + url_for(controller: "followers", action: "index", username: "bob") + + get "/followers" + assert_equal "followers#index", @response.body + assert_equal "http://www.example.com/followers", + url_for(controller: "followers", action: "index", username: nil) + end + + def test_url_generator_for_optional_suffix_static_and_dynamic_segment + draw do + get "/groups(/user/:username)" => "groups#index" + end + + get "/groups/user/bob" + assert_equal "groups#index", @response.body + assert_equal "http://www.example.com/groups/user/bob", + url_for(controller: "groups", action: "index", username: "bob") + + get "/groups" + assert_equal "groups#index", @response.body + assert_equal "http://www.example.com/groups", + url_for(controller: "groups", action: "index", username: nil) + end + + def test_url_generator_for_optional_prefix_static_and_dynamic_segment + draw do + get "(/user/:username)/photos" => "photos#index" + end + + get "/user/bob/photos" + assert_equal "photos#index", @response.body + assert_equal "http://www.example.com/user/bob/photos", + url_for(controller: "photos", action: "index", username: "bob") + + get "/photos" + assert_equal "photos#index", @response.body + assert_equal "http://www.example.com/photos", + url_for(controller: "photos", action: "index", username: nil) + end + + def test_url_recognition_for_optional_static_segments + draw do + scope "(groups)" do + scope "(discussions)" do + resources :messages + end + end + end + + get "/groups/discussions/messages" + assert_equal "messages#index", @response.body + + get "/groups/discussions/messages/1" + assert_equal "messages#show", @response.body + + get "/groups/messages" + assert_equal "messages#index", @response.body + + get "/groups/messages/1" + assert_equal "messages#show", @response.body + + get "/discussions/messages" + assert_equal "messages#index", @response.body + + get "/discussions/messages/1" + assert_equal "messages#show", @response.body + + get "/messages" + assert_equal "messages#index", @response.body + + get "/messages/1" + assert_equal "messages#show", @response.body + end + + def test_router_removes_invalid_conditions + draw do + scope constraints: { id: /\d+/ } do + get "/tickets", to: "tickets#index", as: :tickets + end + end + + get "/tickets" + assert_equal "tickets#index", @response.body + assert_equal "/tickets", tickets_path + end + + def test_constraints_are_merged_from_scope + draw do + scope constraints: { id: /\d{4}/ } do + resources :movies do + resources :reviews + resource :trailer + end + end + end + + get "/movies/0001" + assert_equal "movies#show", @response.body + assert_equal "/movies/0001", movie_path(id: "0001") + + get "/movies/00001" + assert_equal "Not Found", @response.body + assert_raises(ActionController::UrlGenerationError) { movie_path(id: "00001") } + + get "/movies/0001/reviews" + assert_equal "reviews#index", @response.body + assert_equal "/movies/0001/reviews", movie_reviews_path(movie_id: "0001") + + get "/movies/00001/reviews" + assert_equal "Not Found", @response.body + assert_raises(ActionController::UrlGenerationError) { movie_reviews_path(movie_id: "00001") } + + get "/movies/0001/reviews/0001" + assert_equal "reviews#show", @response.body + assert_equal "/movies/0001/reviews/0001", movie_review_path(movie_id: "0001", id: "0001") + + get "/movies/00001/reviews/0001" + assert_equal "Not Found", @response.body + assert_raises(ActionController::UrlGenerationError) { movie_path(movie_id: "00001", id: "00001") } + + get "/movies/0001/trailer" + assert_equal "trailers#show", @response.body + assert_equal "/movies/0001/trailer", movie_trailer_path(movie_id: "0001") + + get "/movies/00001/trailer" + assert_equal "Not Found", @response.body + assert_raises(ActionController::UrlGenerationError) { movie_trailer_path(movie_id: "00001") } + end + + def test_only_should_be_read_from_scope + draw do + scope only: [:index, :show] do + namespace :only do + resources :clubs do + resources :players + resource :chairman + end + end + end + end + + get "/only/clubs" + assert_equal "only/clubs#index", @response.body + assert_equal "/only/clubs", only_clubs_path + + get "/only/clubs/1/edit" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { edit_only_club_path(id: "1") } + + get "/only/clubs/1/players" + assert_equal "only/players#index", @response.body + assert_equal "/only/clubs/1/players", only_club_players_path(club_id: "1") + + get "/only/clubs/1/players/2/edit" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { edit_only_club_player_path(club_id: "1", id: "2") } + + get "/only/clubs/1/chairman" + assert_equal "only/chairmen#show", @response.body + assert_equal "/only/clubs/1/chairman", only_club_chairman_path(club_id: "1") + + get "/only/clubs/1/chairman/edit" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { edit_only_club_chairman_path(club_id: "1") } + end + + def test_except_should_be_read_from_scope + draw do + scope except: [:new, :create, :edit, :update, :destroy] do + namespace :except do + resources :clubs do + resources :players + resource :chairman + end + end + end + end + + get "/except/clubs" + assert_equal "except/clubs#index", @response.body + assert_equal "/except/clubs", except_clubs_path + + get "/except/clubs/1/edit" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { edit_except_club_path(id: "1") } + + get "/except/clubs/1/players" + assert_equal "except/players#index", @response.body + assert_equal "/except/clubs/1/players", except_club_players_path(club_id: "1") + + get "/except/clubs/1/players/2/edit" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { edit_except_club_player_path(club_id: "1", id: "2") } + + get "/except/clubs/1/chairman" + assert_equal "except/chairmen#show", @response.body + assert_equal "/except/clubs/1/chairman", except_club_chairman_path(club_id: "1") + + get "/except/clubs/1/chairman/edit" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { edit_except_club_chairman_path(club_id: "1") } + end + + def test_only_option_should_override_scope + draw do + scope only: :show do + namespace :only do + resources :sectors, only: :index + end + end + end + + get "/only/sectors" + assert_equal "only/sectors#index", @response.body + assert_equal "/only/sectors", only_sectors_path + + get "/only/sectors/1" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { only_sector_path(id: "1") } + end + + def test_only_option_should_not_inherit + draw do + scope only: :show do + namespace :only do + resources :sectors, only: :index do + resources :companies + resource :leader + end + end + end + end + + get "/only/sectors/1/companies/2" + assert_equal "only/companies#show", @response.body + assert_equal "/only/sectors/1/companies/2", only_sector_company_path(sector_id: "1", id: "2") + + get "/only/sectors/1/leader" + assert_equal "only/leaders#show", @response.body + assert_equal "/only/sectors/1/leader", only_sector_leader_path(sector_id: "1") + end + + def test_except_option_should_override_scope + draw do + scope except: :index do + namespace :except do + resources :sectors, except: [:show, :update, :destroy] + end + end + end + + get "/except/sectors" + assert_equal "except/sectors#index", @response.body + assert_equal "/except/sectors", except_sectors_path + + get "/except/sectors/1" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { except_sector_path(id: "1") } + end + + def test_except_option_should_not_inherit + draw do + scope except: :index do + namespace :except do + resources :sectors, except: [:show, :update, :destroy] do + resources :companies + resource :leader + end + end + end + end + + get "/except/sectors/1/companies/2" + assert_equal "except/companies#show", @response.body + assert_equal "/except/sectors/1/companies/2", except_sector_company_path(sector_id: "1", id: "2") + + get "/except/sectors/1/leader" + assert_equal "except/leaders#show", @response.body + assert_equal "/except/sectors/1/leader", except_sector_leader_path(sector_id: "1") + end + + def test_except_option_should_override_scoped_only + draw do + scope only: :show do + namespace :only do + resources :sectors, only: :index do + resources :managers, except: [:show, :update, :destroy] + end + end + end + end + + get "/only/sectors/1/managers" + assert_equal "only/managers#index", @response.body + assert_equal "/only/sectors/1/managers", only_sector_managers_path(sector_id: "1") + + get "/only/sectors/1/managers/2" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { only_sector_manager_path(sector_id: "1", id: "2") } + end + + def test_only_option_should_override_scoped_except + draw do + scope except: :index do + namespace :except do + resources :sectors, except: [:show, :update, :destroy] do + resources :managers, only: :index + end + end + end + end + + get "/except/sectors/1/managers" + assert_equal "except/managers#index", @response.body + assert_equal "/except/sectors/1/managers", except_sector_managers_path(sector_id: "1") + + get "/except/sectors/1/managers/2" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { except_sector_manager_path(sector_id: "1", id: "2") } + end + + def test_only_scope_should_override_parent_scope + draw do + scope only: :show do + namespace :only do + resources :sectors, only: :index do + resources :companies do + scope only: :index do + resources :divisions + end + end + end + end + end + end + + get "/only/sectors/1/companies/2/divisions" + assert_equal "only/divisions#index", @response.body + assert_equal "/only/sectors/1/companies/2/divisions", only_sector_company_divisions_path(sector_id: "1", company_id: "2") + + get "/only/sectors/1/companies/2/divisions/3" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { only_sector_company_division_path(sector_id: "1", company_id: "2", id: "3") } + end + + def test_except_scope_should_override_parent_scope + draw do + scope except: :index do + namespace :except do + resources :sectors, except: [:show, :update, :destroy] do + resources :companies do + scope except: [:show, :update, :destroy] do + resources :divisions + end + end + end + end + end + end + + get "/except/sectors/1/companies/2/divisions" + assert_equal "except/divisions#index", @response.body + assert_equal "/except/sectors/1/companies/2/divisions", except_sector_company_divisions_path(sector_id: "1", company_id: "2") + + get "/except/sectors/1/companies/2/divisions/3" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { except_sector_company_division_path(sector_id: "1", company_id: "2", id: "3") } + end + + def test_except_scope_should_override_parent_only_scope + draw do + scope only: :show do + namespace :only do + resources :sectors, only: :index do + resources :companies do + scope except: [:show, :update, :destroy] do + resources :departments + end + end + end + end + end + end + + get "/only/sectors/1/companies/2/departments" + assert_equal "only/departments#index", @response.body + assert_equal "/only/sectors/1/companies/2/departments", only_sector_company_departments_path(sector_id: "1", company_id: "2") + + get "/only/sectors/1/companies/2/departments/3" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { only_sector_company_department_path(sector_id: "1", company_id: "2", id: "3") } + end + + def test_only_scope_should_override_parent_except_scope + draw do + scope except: :index do + namespace :except do + resources :sectors, except: [:show, :update, :destroy] do + resources :companies do + scope only: :index do + resources :departments + end + end + end + end + end + end + + get "/except/sectors/1/companies/2/departments" + assert_equal "except/departments#index", @response.body + assert_equal "/except/sectors/1/companies/2/departments", except_sector_company_departments_path(sector_id: "1", company_id: "2") + + get "/except/sectors/1/companies/2/departments/3" + assert_equal "Not Found", @response.body + assert_raise(NoMethodError) { except_sector_company_department_path(sector_id: "1", company_id: "2", id: "3") } + end + + def test_resources_are_not_pluralized + draw do + namespace :transport do + resources :taxis + end + end + + get "/transport/taxis" + assert_equal "transport/taxis#index", @response.body + assert_equal "/transport/taxis", transport_taxis_path + + get "/transport/taxis/new" + assert_equal "transport/taxis#new", @response.body + assert_equal "/transport/taxis/new", new_transport_taxi_path + + post "/transport/taxis" + assert_equal "transport/taxis#create", @response.body + + get "/transport/taxis/1" + assert_equal "transport/taxis#show", @response.body + assert_equal "/transport/taxis/1", transport_taxi_path(id: "1") + + get "/transport/taxis/1/edit" + assert_equal "transport/taxis#edit", @response.body + assert_equal "/transport/taxis/1/edit", edit_transport_taxi_path(id: "1") + + put "/transport/taxis/1" + assert_equal "transport/taxis#update", @response.body + + delete "/transport/taxis/1" + assert_equal "transport/taxis#destroy", @response.body + end + + def test_singleton_resources_are_not_singularized + draw do + namespace :medical do + resource :taxis + end + end + + get "/medical/taxis/new" + assert_equal "medical/taxis#new", @response.body + assert_equal "/medical/taxis/new", new_medical_taxis_path + + post "/medical/taxis" + assert_equal "medical/taxis#create", @response.body + + get "/medical/taxis" + assert_equal "medical/taxis#show", @response.body + assert_equal "/medical/taxis", medical_taxis_path + + get "/medical/taxis/edit" + assert_equal "medical/taxis#edit", @response.body + assert_equal "/medical/taxis/edit", edit_medical_taxis_path + + put "/medical/taxis" + assert_equal "medical/taxis#update", @response.body + + delete "/medical/taxis" + assert_equal "medical/taxis#destroy", @response.body + end + + def test_greedy_resource_id_regexp_doesnt_match_edit_and_custom_action + draw do + resources :sections, id: /.+/ do + get :preview, on: :member + end + end + + get "/sections/1/edit" + assert_equal "sections#edit", @response.body + assert_equal "/sections/1/edit", edit_section_path(id: "1") + + get "/sections/1/preview" + assert_equal "sections#preview", @response.body + assert_equal "/sections/1/preview", preview_section_path(id: "1") + end + + def test_resource_constraints_are_pushed_to_scope + draw do + namespace :wiki do + resources :articles, id: /[^\/]+/ do + resources :comments, only: [:create, :new] + end + end + end + + get "/wiki/articles/Ruby_on_Rails_3.0" + assert_equal "wiki/articles#show", @response.body + assert_equal "/wiki/articles/Ruby_on_Rails_3.0", wiki_article_path(id: "Ruby_on_Rails_3.0") + + get "/wiki/articles/Ruby_on_Rails_3.0/comments/new" + assert_equal "wiki/comments#new", @response.body + assert_equal "/wiki/articles/Ruby_on_Rails_3.0/comments/new", new_wiki_article_comment_path(article_id: "Ruby_on_Rails_3.0") + + post "/wiki/articles/Ruby_on_Rails_3.0/comments" + assert_equal "wiki/comments#create", @response.body + assert_equal "/wiki/articles/Ruby_on_Rails_3.0/comments", wiki_article_comments_path(article_id: "Ruby_on_Rails_3.0") + end + + def test_resources_path_can_be_a_symbol + draw do + resources :wiki_pages, path: :pages + resource :wiki_account, path: :my_account + end + + get "/pages" + assert_equal "wiki_pages#index", @response.body + assert_equal "/pages", wiki_pages_path + + get "/pages/Ruby_on_Rails" + assert_equal "wiki_pages#show", @response.body + assert_equal "/pages/Ruby_on_Rails", wiki_page_path(id: "Ruby_on_Rails") + + get "/my_account" + assert_equal "wiki_accounts#show", @response.body + assert_equal "/my_account", wiki_account_path + end + + def test_redirect_https + draw do + get "secure", to: redirect("/secure/login") + end + + with_https do + get "/secure" + verify_redirect "https://www.example.com/secure/login" + end + end + + def test_path_parameters_is_not_stale + draw do + scope "/countries/:country", constraints: lambda { |params, req| %w(all France).include?(params[:country]) } do + get "/", to: "countries#index" + get "/cities", to: "countries#cities" + end + + get "/countries/:country/(*other)", to: redirect { |params, req| params[:other] ? "/countries/all/#{params[:other]}" : "/countries/all" } + end + + get "/countries/France" + assert_equal "countries#index", @response.body + + get "/countries/France/cities" + assert_equal "countries#cities", @response.body + + get "/countries/UK" + verify_redirect "http://www.example.com/countries/all" + + get "/countries/UK/cities" + verify_redirect "http://www.example.com/countries/all/cities" + end + + def test_constraints_block_not_carried_to_following_routes + draw do + scope "/italians" do + get "/writers", to: "italians#writers", constraints: ::TestRoutingMapper::IpRestrictor + get "/sculptors", to: "italians#sculptors" + get "/painters/:painter", to: "italians#painters", constraints: { painter: /michelangelo/ } + end + end + + get "/italians/writers" + assert_equal "Not Found", @response.body + + get "/italians/sculptors" + assert_equal "italians#sculptors", @response.body + + get "/italians/painters/botticelli" + assert_equal "Not Found", @response.body + + get "/italians/painters/michelangelo" + assert_equal "italians#painters", @response.body + end + + def test_custom_resource_actions_defined_using_string + draw do + resources :customers do + resources :invoices do + get "aged/:months", on: :collection, action: :aged, as: :aged + end + + get "inactive", on: :collection + post "deactivate", on: :member + get "old", on: :collection, as: :stale + end + end + + get "/customers/inactive" + assert_equal "customers#inactive", @response.body + assert_equal "/customers/inactive", inactive_customers_path + + post "/customers/1/deactivate" + assert_equal "customers#deactivate", @response.body + assert_equal "/customers/1/deactivate", deactivate_customer_path(id: "1") + + get "/customers/old" + assert_equal "customers#old", @response.body + assert_equal "/customers/old", stale_customers_path + + get "/customers/1/invoices/aged/3" + assert_equal "invoices#aged", @response.body + assert_equal "/customers/1/invoices/aged/3", aged_customer_invoices_path(customer_id: "1", months: "3") + end + + def test_route_defined_in_resources_scope_level + draw do + resources :customers do + get "export" + end + end + + get "/customers/1/export" + assert_equal "customers#export", @response.body + assert_equal "/customers/1/export", customer_export_path(customer_id: "1") + end + + def test_named_character_classes_in_regexp_constraints + draw do + get "/purchases/:token/:filename", + to: "purchases#fetch", + token: /[[:alnum:]]{10}/, + filename: /(.+)/, + as: :purchase + end + + get "/purchases/315004be7e/Ruby_on_Rails_3.pdf" + assert_equal "purchases#fetch", @response.body + assert_equal "/purchases/315004be7e/Ruby_on_Rails_3.pdf", purchase_path(token: "315004be7e", filename: "Ruby_on_Rails_3.pdf") + end + + def test_nested_resource_constraints + draw do + resources :lists, id: /([A-Za-z0-9]{25})|default/ do + resources :todos, id: /\d+/ + end + end + + get "/lists/01234012340123401234fffff" + assert_equal "lists#show", @response.body + assert_equal "/lists/01234012340123401234fffff", list_path(id: "01234012340123401234fffff") + + get "/lists/01234012340123401234fffff/todos/1" + assert_equal "todos#show", @response.body + assert_equal "/lists/01234012340123401234fffff/todos/1", list_todo_path(list_id: "01234012340123401234fffff", id: "1") + + get "/lists/2/todos/1" + assert_equal "Not Found", @response.body + assert_raises(ActionController::UrlGenerationError) { list_todo_path(list_id: "2", id: "1") } + end + + def test_redirect_argument_error + routes = Class.new { include ActionDispatch::Routing::Redirection }.new + assert_raises(ArgumentError) { routes.redirect Object.new } + end + + def test_named_route_check + before, after = nil + + draw do + before = has_named_route?(:hello) + get "/hello", as: :hello, to: "hello#world" + after = has_named_route?(:hello) + end + + assert_not before, "expected to not have named route :hello before route definition" + assert after, "expected to have named route :hello after route definition" + end + + def test_explicitly_avoiding_the_named_route + draw do + scope as: "routes" do + get "/c/:id", as: :collision, to: "collision#show" + get "/collision", to: "collision#show" + get "/no_collision", to: "collision#show", as: nil + end + end + + assert_not respond_to?(:routes_no_collision_path) + end + + def test_controller_name_with_leading_slash_raise_error + assert_raise(ArgumentError) do + draw { get "/feeds/:service", to: "/feeds#show" } + end + + assert_raise(ArgumentError) do + draw { get "/feeds/:service", controller: "/feeds", action: "show" } + end + + assert_raise(ArgumentError) do + draw { get "/api/feeds/:service", to: "/api/feeds#show" } + end + + assert_raise(ArgumentError) do + draw { resources :feeds, controller: "/feeds" } + end + end + + def test_invalid_route_name_raises_error + assert_raise(ArgumentError) do + draw { get "/products", to: "products#index", as: "products " } + end + + assert_raise(ArgumentError) do + draw { get "/products", to: "products#index", as: " products" } + end + + assert_raise(ArgumentError) do + draw { get "/products", to: "products#index", as: "products!" } + end + + assert_raise(ArgumentError) do + draw { get "/products", to: "products#index", as: "products index" } + end + + assert_raise(ArgumentError) do + draw { get "/products", to: "products#index", as: "1products" } + end + end + + def test_duplicate_route_name_raises_error + assert_raise(ArgumentError) do + draw do + get "/collision", to: "collision#show", as: "collision" + get "/duplicate", to: "duplicate#show", as: "collision" + end + end + end + + def test_duplicate_route_name_via_resources_raises_error + assert_raise(ArgumentError) do + draw do + resources :collisions + get "/collision", to: "collision#show", as: "collision" + end + end + end + + def test_nested_route_in_nested_resource + draw do + resources :posts, only: [:index, :show] do + resources :comments, except: :destroy do + get "views" => "comments#views", :as => :views + end + end + end + + get "/posts/1/comments/2/views" + assert_equal "comments#views", @response.body + assert_equal "/posts/1/comments/2/views", post_comment_views_path(post_id: "1", comment_id: "2") + end + + def test_root_in_deeply_nested_scope + draw do + resources :posts, only: [:index, :show] do + namespace :admin do + root to: "index#index" + end + end + end + + get "/posts/1/admin" + assert_equal "admin/index#index", @response.body + assert_equal "/posts/1/admin", post_admin_root_path(post_id: "1") + end + + def test_custom_param + draw do + resources :profiles, param: :username do + get :details, on: :member + resources :messages + end + end + + get "/profiles/bob" + assert_equal "profiles#show", @response.body + assert_equal "bob", @request.params[:username] + + get "/profiles/bob/details" + assert_equal "bob", @request.params[:username] + + get "/profiles/bob/messages/34" + assert_equal "bob", @request.params[:profile_username] + assert_equal "34", @request.params[:id] + end + + def test_custom_param_constraint + draw do + resources :profiles, param: :username, username: /[a-z]+/ do + get :details, on: :member + resources :messages + end + end + + get "/profiles/bob1" + assert_equal 404, @response.status + + get "/profiles/bob1/details" + assert_equal 404, @response.status + + get "/profiles/bob1/messages/34" + assert_equal 404, @response.status + end + + def test_shallow_custom_param + draw do + resources :orders do + constraints download: /[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/ do + resources :downloads, param: :download, shallow: true + end + end + end + + get "/downloads/0c0c0b68-d24b-11e1-a861-001ff3fffe6f.zip" + assert_equal "downloads#show", @response.body + assert_equal "0c0c0b68-d24b-11e1-a861-001ff3fffe6f", @request.params[:download] + end + + def test_action_from_path_is_not_frozen + draw do + get "search" => "search" + end + + get "/search" + assert_not_predicate @request.params[:action], :frozen? + end + + def test_multiple_positional_args_with_the_same_name + draw do + get "/downloads/:id/:id.tar" => "downloads#show", as: :download, format: false + end + + expected_params = { + controller: "downloads", + action: "show", + id: "1" + } + + get "/downloads/1/1.tar" + assert_equal "downloads#show", @response.body + assert_equal expected_params, @request.path_parameters + assert_equal "/downloads/1/1.tar", download_path("1") + assert_equal "/downloads/1/1.tar", download_path("1", "1") + end + + def test_absolute_controller_namespace + draw do + namespace :foo do + get "/", to: "/bar#index", as: "root" + end + end + + get "/foo" + assert_equal "bar#index", @response.body + assert_equal "/foo", foo_root_path + end + + def test_namespace_as_controller + draw do + namespace :foo do + get "/", to: "/bar#index", as: "root" + end + end + + get "/foo" + assert_equal "bar#index", @response.body + assert_equal "/foo", foo_root_path + end + + def test_trailing_slash + draw do + resources :streams + end + + get "/streams" + assert @response.ok?, "route without trailing slash should work" + + get "/streams/" + assert @response.ok?, "route with trailing slash should work" + + get "/streams?foobar" + assert @response.ok?, "route without trailing slash and with QUERY_STRING should work" + + get "/streams/?foobar" + assert @response.ok?, "route with trailing slash and with QUERY_STRING should work" + end + + def test_route_with_dashes_in_path + draw do + get "/contact-us", to: "pages#contact_us" + end + + get "/contact-us" + assert_equal "pages#contact_us", @response.body + assert_equal "/contact-us", contact_us_path + end + + def test_shorthand_route_with_dashes_in_path + draw do + get "/about-us/index" + end + + get "/about-us/index" + assert_equal "about_us#index", @response.body + assert_equal "/about-us/index", about_us_index_path + end + + def test_resource_routes_with_dashes_in_path + draw do + resources :photos, only: [:show] do + get "user-favorites", on: :collection + get "preview-photo", on: :member + get "summary-text" + end + end + + get "/photos/user-favorites" + assert_equal "photos#user_favorites", @response.body + assert_equal "/photos/user-favorites", user_favorites_photos_path + + get "/photos/1/preview-photo" + assert_equal "photos#preview_photo", @response.body + assert_equal "/photos/1/preview-photo", preview_photo_photo_path("1") + + get "/photos/1/summary-text" + assert_equal "photos#summary_text", @response.body + assert_equal "/photos/1/summary-text", photo_summary_text_path("1") + + get "/photos/1" + assert_equal "photos#show", @response.body + assert_equal "/photos/1", photo_path("1") + end + + def test_shallow_path_inside_namespace_is_not_added_twice + draw do + namespace :admin do + shallow do + resources :posts do + resources :comments + end + end + end + end + + get "/admin/posts/1/comments" + assert_equal "admin/comments#index", @response.body + assert_equal "/admin/posts/1/comments", admin_post_comments_path("1") + end + + def test_mix_string_to_controller_action + draw do + get "/projects", controller: "project_files", + action: "index", + to: "comments#index" + end + get "/projects" + assert_equal "comments#index", @response.body + end + + def test_mix_string_to_controller + draw do + get "/projects", controller: "project_files", + to: "comments#index" + end + get "/projects" + assert_equal "comments#index", @response.body + end + + def test_mix_string_to_action + draw do + get "/projects", action: "index", + to: "comments#index" + end + get "/projects" + assert_equal "comments#index", @response.body + end + + def test_shallow_path_and_prefix_are_not_added_to_non_shallow_routes + draw do + scope shallow_path: "projects", shallow_prefix: "project" do + resources :projects do + resources :files, controller: "project_files", shallow: true + end + end + end + + get "/projects" + assert_equal "projects#index", @response.body + assert_equal "/projects", projects_path + + get "/projects/new" + assert_equal "projects#new", @response.body + assert_equal "/projects/new", new_project_path + + post "/projects" + assert_equal "projects#create", @response.body + + get "/projects/1" + assert_equal "projects#show", @response.body + assert_equal "/projects/1", project_path("1") + + get "/projects/1/edit" + assert_equal "projects#edit", @response.body + assert_equal "/projects/1/edit", edit_project_path("1") + + patch "/projects/1" + assert_equal "projects#update", @response.body + + delete "/projects/1" + assert_equal "projects#destroy", @response.body + + get "/projects/1/files" + assert_equal "project_files#index", @response.body + assert_equal "/projects/1/files", project_files_path("1") + + get "/projects/1/files/new" + assert_equal "project_files#new", @response.body + assert_equal "/projects/1/files/new", new_project_file_path("1") + + post "/projects/1/files" + assert_equal "project_files#create", @response.body + + get "/projects/files/2" + assert_equal "project_files#show", @response.body + assert_equal "/projects/files/2", project_file_path("2") + + get "/projects/files/2/edit" + assert_equal "project_files#edit", @response.body + assert_equal "/projects/files/2/edit", edit_project_file_path("2") + + patch "/projects/files/2" + assert_equal "project_files#update", @response.body + + delete "/projects/files/2" + assert_equal "project_files#destroy", @response.body + end + + def test_scope_path_is_copied_to_shallow_path + draw do + scope path: "foo" do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal "/foo/comments/1", comment_path("1") + end + + def test_scope_as_is_copied_to_shallow_prefix + draw do + scope as: "foo" do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal "/comments/1", foo_comment_path("1") + end + + def test_scope_shallow_prefix_is_not_overwritten_by_as + draw do + scope as: "foo", shallow_prefix: "bar" do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal "/comments/1", bar_comment_path("1") + end + + def test_scope_shallow_path_is_not_overwritten_by_path + draw do + scope path: "foo", shallow_path: "bar" do + resources :posts do + resources :comments, shallow: true + end + end + end + + assert_equal "/bar/comments/1", comment_path("1") + end + + def test_resource_where_as_is_empty + draw do + resource :post, as: "" + + scope "post", as: "post" do + resource :comment, as: "" + end + end + + assert_equal "/post/new", new_path + assert_equal "/post/comment/new", new_post_path + end + + def test_resources_where_as_is_empty + draw do + resources :posts, as: "" + + scope "posts", as: "posts" do + resources :comments, as: "" + end + end + + assert_equal "/posts/new", new_path + assert_equal "/posts/comments/new", new_posts_path + end + + def test_scope_where_as_is_empty + draw do + scope "post", as: "" do + resource :user + resources :comments + end + end + + assert_equal "/post/user/new", new_user_path + assert_equal "/post/comments/new", new_comment_path + end + + def test_head_fetch_with_mount_on_root + draw do + get "/home" => "test#index" + mount lambda { |env| [200, {}, [env["REQUEST_METHOD"]]] }, at: "/" + end + + # HEAD request should match `get /home` rather than the + # lower-precedence Rack app mounted at `/`. + head "/home" + assert_response :ok + assert_equal "test#index", @response.body + + # But the Rack app can still respond to its own HEAD requests. + head "/foobar" + assert_response :ok + assert_equal "HEAD", @response.body + end + + def test_passing_action_parameters_to_url_helpers_raises_error_if_parameters_are_not_permitted + draw do + root to: "projects#index" + end + params = ActionController::Parameters.new(id: "1") + + assert_raises ActionController::UnfilteredParameters do + root_path(params) + end + end + + def test_passing_action_parameters_to_url_helpers_is_allowed_if_parameters_are_permitted + draw do + root to: "projects#index" + end + params = ActionController::Parameters.new(id: "1") + params.permit! + + assert_equal "/?id=1", root_path(params) + end + + def test_dynamic_controller_segments_are_deprecated + assert_deprecated do + draw do + get "/:controller", action: "index" + end + end + end + + def test_dynamic_action_segments_are_deprecated + assert_deprecated do + draw do + get "/pages/:action", controller: "pages" + end + end + end + + def test_multiple_roots_raises_error + ex = assert_raises(ArgumentError) { + draw do + root "pages#index", constraints: { host: "www.example.com" } + root "admin/pages#index", constraints: { host: "admin.example.com" } + end + } + assert_match(/Invalid route name, already in use: 'root'/, ex.message) + end + + def test_multiple_named_roots + draw do + namespace :foo do + root "pages#index", constraints: { host: "www.example.com" } + root "admin/pages#index", constraints: { host: "admin.example.com" }, as: :admin_root + end + + root "pages#index", constraints: { host: "www.example.com" } + root "admin/pages#index", constraints: { host: "admin.example.com" }, as: :admin_root + end + + get "http://www.example.com/foo" + assert_equal "foo/pages#index", @response.body + + get "http://admin.example.com/foo" + assert_equal "foo/admin/pages#index", @response.body + + get "http://www.example.com/" + assert_equal "pages#index", @response.body + + get "http://admin.example.com/" + assert_equal "admin/pages#index", @response.body + end + + def test_multiple_namespaced_roots + draw do + namespace :foo do + root "test#index" + end + + root "test#index" + + namespace :bar do + root "test#index" + end + end + + assert_equal "/foo", foo_root_path + assert_equal "/", root_path + assert_equal "/bar", bar_root_path + end + + def test_nested_routes_under_format_resource + draw do + resources :formats do + resources :items + end + end + + get "/formats/1/items.json" + assert_equal 200, @response.status + assert_equal "items#index", @response.body + assert_equal "/formats/1/items.json", format_items_path(1, :json) + + get "/formats/1/items/2.json" + assert_equal 200, @response.status + assert_equal "items#show", @response.body + assert_equal "/formats/1/items/2.json", format_item_path(1, 2, :json) + end + +private + + def draw(&block) + self.class.stub_controllers do |routes| + routes.default_url_options = { host: "www.example.com" } + routes.draw(&block) + @app = RoutedRackApp.new routes + end + end + + def url_for(options = {}) + @app.routes.url_helpers.url_for(options) + end + + def method_missing(method, *args, &block) + if method.to_s =~ /_(path|url)$/ + @app.routes.url_helpers.send(method, *args, &block) + else + super + end + end + + def with_https + old_https = https? + https! + yield + ensure + https!(old_https) + end + + def verify_redirect(url, status = 301) + assert_equal status, @response.status + assert_equal url, @response.headers["Location"] + assert_equal expected_redirect_body(url), @response.body + end + + def expected_redirect_body(url) + %(<html><body>You are being <a href="#{ERB::Util.h(url)}">redirected</a>.</body></html>) + end +end + +class TestAltApp < ActionDispatch::IntegrationTest + class AltRequest < ActionDispatch::Request + attr_accessor :path_parameters, :path_info, :script_name + attr_reader :env + + def initialize(env) + @path_parameters = {} + @env = env + @path_info = "/" + @script_name = "" + super + end + + def request_method + "GET" + end + + def ip + "127.0.0.1" + end + + def x_header + @env["HTTP_X_HEADER"] || "" + end + end + + class XHeader + def call(env) + [200, { "Content-Type" => "text/html" }, ["XHeader"]] + end + end + + class AltApp + def call(env) + [200, { "Content-Type" => "text/html" }, ["Alternative App"]] + end + end + + AltRoutes = Class.new(ActionDispatch::Routing::RouteSet) { + def request_class + AltRequest + end + }.new + AltRoutes.draw do + get "/" => TestAltApp::XHeader.new, :constraints => { x_header: /HEADER/ } + get "/" => TestAltApp::AltApp.new + end + + APP = build_app AltRoutes + + def app + APP + end + + def test_alt_request_without_header + get "/" + assert_equal "Alternative App", @response.body + end + + def test_alt_request_with_matched_header + get "/", headers: { "HTTP_X_HEADER" => "HEADER" } + assert_equal "XHeader", @response.body + end + + def test_alt_request_with_unmatched_header + get "/", headers: { "HTTP_X_HEADER" => "NON_MATCH" } + assert_equal "Alternative App", @response.body + end +end + +class TestAppendingRoutes < ActionDispatch::IntegrationTest + def simple_app(resp) + lambda { |e| [ 200, { "Content-Type" => "text/plain" }, [resp] ] } + end + + def setup + super + s = self + routes = ActionDispatch::Routing::RouteSet.new + routes.append do + get "/hello" => s.simple_app("fail") + get "/goodbye" => s.simple_app("goodbye") + end + + routes.draw do + get "/hello" => s.simple_app("hello") + end + @app = self.class.build_app routes + end + + def test_goodbye_should_be_available + get "/goodbye" + assert_equal "goodbye", @response.body + end + + def test_hello_should_not_be_overwritten + get "/hello" + assert_equal "hello", @response.body + end + + def test_missing_routes_are_still_missing + get "/random" + assert_equal 404, @response.status + end +end + +class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest + module ::Admin + class StorageFilesController < ActionController::Base + def index + render plain: "admin/storage_files#index" + end + end + end + + def draw(&block) + routes = ActionDispatch::Routing::RouteSet.new + routes.draw(&block) + @app = self.class.build_app routes + end + + def test_missing_controller + ex = assert_raises(ArgumentError) { + draw do + get "/foo/bar", action: :index + end + } + assert_match(/Missing :controller/, ex.message) + end + + def test_missing_controller_with_to + ex = assert_raises(ArgumentError) { + draw do + get "/foo/bar", to: "foo" + end + } + assert_match(/Missing :controller/, ex.message) + end + + def test_missing_action_on_hash + ex = assert_raises(ArgumentError) { + draw do + get "/foo/bar", to: "foo#" + end + } + assert_match(/Missing :action/, ex.message) + end + + def test_valid_controller_options_inside_namespace + draw do + namespace :admin do + resources :storage_files, controller: "storage_files" + end + end + + get "/admin/storage_files" + assert_equal "admin/storage_files#index", @response.body + end + + def test_resources_with_valid_namespaced_controller_option + draw do + resources :storage_files, controller: "admin/storage_files" + end + + get "/storage_files" + assert_equal "admin/storage_files#index", @response.body + end + + def test_warn_with_ruby_constant_syntax_controller_option + e = assert_raise(ArgumentError) do + draw do + namespace :admin do + resources :storage_files, controller: "StorageFiles" + end + end + end + + assert_match "'admin/StorageFiles' is not a supported controller name", e.message + end + + def test_warn_with_ruby_constant_syntax_namespaced_controller_option + e = assert_raise(ArgumentError) do + draw do + resources :storage_files, controller: "Admin::StorageFiles" + end + end + + assert_match "'Admin::StorageFiles' is not a supported controller name", e.message + end + + def test_warn_with_ruby_constant_syntax_no_colons + e = assert_raise(ArgumentError) do + draw do + resources :storage_files, controller: "Admin" + end + end + + assert_match "'Admin' is not a supported controller name", e.message + end +end + +class TestDefaultScope < ActionDispatch::IntegrationTest + module ::Blog + class PostsController < ActionController::Base + def index + render plain: "blog/posts#index" + end + end + end + + DefaultScopeRoutes = ActionDispatch::Routing::RouteSet.new + DefaultScopeRoutes.default_scope = { module: :blog } + DefaultScopeRoutes.draw do + resources :posts + end + + APP = build_app DefaultScopeRoutes + + def app + APP + end + + include DefaultScopeRoutes.url_helpers + + def test_default_scope + get "/posts" + assert_equal "blog/posts#index", @response.body + end +end + +class TestHttpMethods < ActionDispatch::IntegrationTest + RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) + RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) + RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY) + RFC3648 = %w(ORDERPATCH) + RFC3744 = %w(ACL) + RFC5323 = %w(SEARCH) + RFC4791 = %w(MKCALENDAR) + RFC5789 = %w(PATCH) + + def simple_app(response) + lambda { |env| [ 200, { "Content-Type" => "text/plain" }, [response] ] } + end + + attr_reader :app + + def setup + s = self + routes = ActionDispatch::Routing::RouteSet.new + @app = RoutedRackApp.new routes + + routes.draw do + (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method| + match "/" => s.simple_app(method), :via => method.underscore.to_sym + end + end + end + + (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method| + test "request method #{method.underscore} can be matched" do + get "/", headers: { "REQUEST_METHOD" => method } + assert_equal method, @response.body + end + end +end + +class TestUriPathEscaping < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + get "/:segment" => lambda { |env| + path_params = env["action_dispatch.request.path_parameters"] + [200, { "Content-Type" => "text/plain" }, [path_params[:segment]]] + }, :as => :segment + + get "/*splat" => lambda { |env| + path_params = env["action_dispatch.request.path_parameters"] + [200, { "Content-Type" => "text/plain" }, [path_params[:splat]]] + }, :as => :splat + end + end + + include Routes.url_helpers + APP = build_app Routes + def app; APP end + + test "escapes slash in generated path segment" do + assert_equal "/a%20b%2Fc+d", segment_path(segment: "a b/c+d") + end + + test "unescapes recognized path segment" do + get "/a%20b%2Fc+d" + assert_equal "a b/c+d", @response.body + end + + test "does not escape slash in generated path splat" do + assert_equal "/a%20b/c+d", splat_path(splat: "a b/c+d") + end + + test "unescapes recognized path splat" do + get "/a%20b/c+d" + assert_equal "a b/c+d", @response.body + end +end + +class TestUnicodePaths < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + get "/ほげ" => lambda { |env| + [200, { "Content-Type" => "text/plain" }, []] + }, :as => :unicode_path + end + end + + include Routes.url_helpers + APP = build_app Routes + def app; APP end + + test "recognizes unicode path" do + get "/#{Rack::Utils.escape("ほげ")}" + assert_equal "200", @response.code + end +end + +class TestMultipleNestedController < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + namespace :foo do + namespace :bar do + get "baz" => "baz#index" + end + end + get "pooh" => "pooh#index" + end + end + + module ::Foo + module Bar + class BazController < ActionController::Base + include Routes.url_helpers + + def index + render inline: "<%= url_for :controller => '/pooh', :action => 'index' %>" + end + end + end + end + + APP = build_app Routes + def app; APP end + + test "controller option which starts with '/' from multiple nested controller" do + get "/foo/bar/baz" + assert_equal "/pooh", @response.body + end +end + +class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + + get "/~user" => ok + get "/young-and-fine" => ok + end + end + + include Routes.url_helpers + APP = build_app Routes + def app; APP end + + test "recognizes tilde path" do + get "/~user" + assert_equal "200", @response.code + end + + test "recognizes minus path" do + get "/young-and-fine" + assert_equal "200", @response.code + end +end + +class TestRedirectInterpolation < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + + get "/foo/:id" => redirect("/foo/bar/%{id}") + get "/bar/:id" => redirect(path: "/foo/bar/%{id}") + get "/baz/:id" => redirect("/baz?id=%{id}&foo=?&bar=1#id-%{id}") + get "/foo/bar/:id" => ok + get "/baz" => ok + end + end + + APP = build_app Routes + def app; APP end + + test "redirect escapes interpolated parameters with redirect proc" do + get "/foo/1%3E" + verify_redirect "http://www.example.com/foo/bar/1%3E" + end + + test "redirect escapes interpolated parameters with option proc" do + get "/bar/1%3E" + verify_redirect "http://www.example.com/foo/bar/1%3E" + end + + test "path redirect escapes interpolated parameters correctly" do + get "/foo/1%201" + verify_redirect "http://www.example.com/foo/bar/1%201" + + get "/baz/1%201" + verify_redirect "http://www.example.com/baz?id=1+1&foo=?&bar=1#id-1%201" + end + +private + def verify_redirect(url, status = 301) + assert_equal status, @response.status + assert_equal url, @response.headers["Location"] + assert_equal expected_redirect_body(url), @response.body + end + + def expected_redirect_body(url) + %(<html><body>You are being <a href="#{ERB::Util.h(url)}">redirected</a>.</body></html>) + end +end + +class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + + get "/:foo" => ok, :constraints => lambda { |r| r.params[:foo] == "foo" } + get "/:bar" => ok + end + end + + APP = build_app Routes + def app; APP end + + test "parameters are reset between constraint checks" do + get "/bar" + assert_nil @request.params[:foo] + assert_equal "bar", @request.params[:bar] + end +end + +class TestGlobRoutingMapper < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + + get "/*id" => redirect("/not_cars"), :constraints => { id: /dummy/ } + get "/cars" => ok + end + end + + # include Routes.url_helpers + APP = build_app Routes + def app; APP end + + def test_glob_constraint + get "/dummy" + assert_equal "301", @response.code + assert_equal "/not_cars", @response.header["Location"].match("/[^/]+$")[0] + end + + def test_glob_constraint_skip_route + get "/cars" + assert_equal "200", @response.code + end + def test_glob_constraint_skip_all + get "/missing" + assert_equal "404", @response.code + end +end + +class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + get "/foo" => ok, as: :foo + + ActiveSupport::Deprecation.silence do + get "/post(/:action(/:id))" => ok, as: :posts + end + + get "/:foo/:foo_type/bars/:id" => ok, as: :bar + get "/projects/:id.:format" => ok, as: :project + get "/pages/:id" => ok, as: :page + get "/wiki/*page" => ok, as: :wiki + end + end + + include Routes.url_helpers + APP = build_app Routes + def app; APP end + + test "enabled when not mounted and default_url_options is empty" do + assert_predicate Routes.url_helpers, :optimize_routes_generation? + end + + test "named route called as singleton method" do + assert_equal "/foo", Routes.url_helpers.foo_path + end + + test "named route called on included module" do + assert_equal "/foo", foo_path + end + + test "nested optional segments are removed" do + assert_equal "/post", Routes.url_helpers.posts_path + assert_equal "/post", posts_path + end + + test "segments with same prefix are replaced correctly" do + assert_equal "/foo/baz/bars/1", Routes.url_helpers.bar_path("foo", "baz", "1") + assert_equal "/foo/baz/bars/1", bar_path("foo", "baz", "1") + end + + test "segments separated with a period are replaced correctly" do + assert_equal "/projects/1.json", Routes.url_helpers.project_path(1, :json) + assert_equal "/projects/1.json", project_path(1, :json) + end + + test "segments with question marks are escaped" do + assert_equal "/pages/foo%3Fbar", Routes.url_helpers.page_path("foo?bar") + assert_equal "/pages/foo%3Fbar", page_path("foo?bar") + end + + test "segments with slashes are escaped" do + assert_equal "/pages/foo%2Fbar", Routes.url_helpers.page_path("foo/bar") + assert_equal "/pages/foo%2Fbar", page_path("foo/bar") + end + + test "glob segments with question marks are escaped" do + assert_equal "/wiki/foo%3Fbar", Routes.url_helpers.wiki_path("foo?bar") + assert_equal "/wiki/foo%3Fbar", wiki_path("foo?bar") + end + + test "glob segments with slashes are not escaped" do + assert_equal "/wiki/foo/bar", Routes.url_helpers.wiki_path("foo/bar") + assert_equal "/wiki/foo/bar", wiki_path("foo/bar") + end +end + +class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest + class CategoriesController < ActionController::Base + def show + render plain: "categories#show" + end + end + + class ProductsController < ActionController::Base + def show + render plain: "products#show" + end + end + + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + scope module: "test_named_route_url_helpers" do + get "/categories/:id" => "categories#show", :as => :category + get "/products/:id" => "products#show", :as => :product + end + end + end + + APP = build_app Routes + def app; APP end + + include Routes.url_helpers + + test "url helpers do not ignore nil parameters when using non-optimized routes" do + Routes.stub :optimize_routes_generation?, false do + get "/categories/1" + assert_response :success + assert_raises(ActionController::UrlGenerationError) { product_path(nil) } + end + end +end + +class TestUrlConstraints < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + + constraints subdomain: "admin" do + get "/" => ok, :as => :admin_root + end + + scope constraints: { protocol: "https://" } do + get "/" => ok, :as => :secure_root + end + + get "/" => ok, :as => :alternate_root, :constraints => { port: 8080 } + + get "/search" => ok, :constraints => { subdomain: false } + + get "/logs" => ok, :constraints => { subdomain: true } + end + end + + include Routes.url_helpers + APP = build_app Routes + def app; APP end + + test "constraints are copied to defaults when using constraints method" do + assert_equal "http://admin.example.com/", admin_root_url + + get "http://admin.example.com/" + assert_response :success + end + + test "constraints are copied to defaults when using scope constraints hash" do + assert_equal "https://www.example.com/", secure_root_url + + get "https://www.example.com/" + assert_response :success + end + + test "constraints are copied to defaults when using route constraints hash" do + assert_equal "http://www.example.com:8080/", alternate_root_url + + get "http://www.example.com:8080/" + assert_response :success + end + + test "false constraint expressions check for absence of values" do + get "http://example.com/search" + assert_response :success + assert_equal "http://example.com/search", search_url + + get "http://api.example.com/search" + assert_response :not_found + end + + test "true constraint expressions check for presence of values" do + get "http://api.example.com/logs" + assert_response :success + assert_equal "http://api.example.com/logs", logs_url + + get "http://example.com/logs" + assert_response :not_found + end +end + +class TestInvalidUrls < ActionDispatch::IntegrationTest + class FooController < ActionController::Base + def self.binary_params_for?(action) + action == "show" + end + + def show + render plain: "foo#show" + end + end + + test "invalid UTF-8 encoding returns a bad request" do + with_routing do |set| + set.draw do + get "/bar/:id", to: redirect("/foo/show/%{id}") + + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + get "/foobar/:id", to: ok + + ActiveSupport::Deprecation.silence do + get "/:controller(/:action(/:id))" + end + end + + get "/%E2%EF%BF%BD%A6" + assert_response :bad_request + + get "/foo/%E2%EF%BF%BD%A6" + assert_response :bad_request + + get "/bar/%E2%EF%BF%BD%A6" + assert_response :bad_request + + get "/foobar/%E2%EF%BF%BD%A6" + assert_response :bad_request + end + end + + test "params encoded with binary_params_for? are treated as ASCII 8bit" do + with_routing do |set| + set.draw do + get "/foo/show(/:id)", to: "test_invalid_urls/foo#show" + end + + get "/foo/show/%E2%EF%BF%BD%A6" + assert_response :ok + end + end +end + +class TestOptionalRootSegments < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + get "/(page/:page)", to: "pages#index", as: :root + end + end + + APP = build_app Routes + def app + APP + end + + include Routes.url_helpers + + def test_optional_root_segments + get "/" + assert_equal "pages#index", @response.body + assert_equal "/", root_path + + get "/page/1" + assert_equal "pages#index", @response.body + assert_equal "1", @request.params[:page] + assert_equal "/page/1", root_path("1") + assert_equal "/page/1", root_path(page: "1") + end +end + +class TestPortConstraints < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + + get "/integer", to: ok, constraints: { port: 8080 } + get "/string", to: ok, constraints: { port: "8080" } + get "/array/:idx", to: ok, constraints: { port: [8080], idx: %w[first last] } + get "/regexp", to: ok, constraints: { port: /8080/ } + end + end + + include Routes.url_helpers + APP = build_app Routes + def app; APP end + + def test_integer_port_constraints + get "http://www.example.com/integer" + assert_response :not_found + + get "http://www.example.com:8080/integer" + assert_response :success + end + + def test_string_port_constraints + get "http://www.example.com/string" + assert_response :not_found + + get "http://www.example.com:8080/string" + assert_response :success + end + + def test_array_port_constraints + get "http://www.example.com/array" + assert_response :not_found + + get "http://www.example.com:8080/array/middle" + assert_response :not_found + + get "http://www.example.com:8080/array/first" + assert_response :success + end + + def test_regexp_port_constraints + get "http://www.example.com/regexp" + assert_response :not_found + + get "http://www.example.com:8080/regexp" + assert_response :success + end +end + +class TestFormatConstraints < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + + get "/string", to: ok, constraints: { format: "json" } + get "/regexp", to: ok, constraints: { format: /json/ } + get "/json_only", to: ok, format: true, constraints: { format: /json/ } + get "/xml_only", to: ok, format: "xml" + end + end + + include Routes.url_helpers + APP = build_app Routes + def app; APP end + + def test_string_format_constraints + get "http://www.example.com/string" + assert_response :success + + get "http://www.example.com/string.json" + assert_response :success + + get "http://www.example.com/string.html" + assert_response :not_found + end + + def test_regexp_format_constraints + get "http://www.example.com/regexp" + assert_response :success + + get "http://www.example.com/regexp.json" + assert_response :success + + get "http://www.example.com/regexp.html" + assert_response :not_found + end + + def test_enforce_with_format_true_with_constraint + get "http://www.example.com/json_only.json" + assert_response :success + + get "http://www.example.com/json_only.html" + assert_response :not_found + + get "http://www.example.com/json_only" + assert_response :not_found + end + + def test_enforce_with_string + get "http://www.example.com/xml_only.xml" + assert_response :success + + get "http://www.example.com/xml_only" + assert_response :success + + get "http://www.example.com/xml_only.json" + assert_response :not_found + end +end + +class TestCallableConstraintValidation < ActionDispatch::IntegrationTest + def test_constraint_with_object_not_callable + assert_raises(ArgumentError) do + ActionDispatch::Routing::RouteSet.new.draw do + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + get "/test", to: ok, constraints: Object.new + end + end + end +end + +class TestRouteDefaults < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + resources :posts, bucket_type: "post" + resources :projects, defaults: { bucket_type: "project" } + end + end + + APP = build_app Routes + def app + APP + end + + include Routes.url_helpers + + def test_route_options_are_required_for_url_for + assert_raises(ActionController::UrlGenerationError) do + assert_equal "/posts/1", url_for(controller: "posts", action: "show", id: 1, only_path: true) + end + + assert_equal "/posts/1", url_for(controller: "posts", action: "show", id: 1, bucket_type: "post", only_path: true) + end + + def test_route_defaults_are_not_required_for_url_for + assert_equal "/projects/1", url_for(controller: "projects", action: "show", id: 1, only_path: true) + end +end + +class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + rack_app = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + mount rack_app, at: "/account", as: "account" + mount rack_app, at: "/:locale/account", as: "localized_account" + end + end + + APP = build_app Routes + def app + APP + end + + include Routes.url_helpers + + def test_mounted_application_doesnt_match_unnamed_route + assert_raise(ActionController::UrlGenerationError) do + assert_equal "/account?controller=products", url_for(controller: "products", action: "index", only_path: true) + end + + assert_raise(ActionController::UrlGenerationError) do + assert_equal "/de/account?controller=products", url_for(controller: "products", action: "index", locale: "de", only_path: true) + end + end +end + +class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest + stub_controllers do |routes| + Routes = routes + Routes.draw do + get "/account", to: redirect("/myaccount"), as: "account" + get "/:locale/account", to: redirect("/%{locale}/myaccount"), as: "localized_account" + end + end + + APP = build_app Routes + def app + APP + end + + include Routes.url_helpers + + def test_redirect_doesnt_match_unnamed_route + assert_raise(ActionController::UrlGenerationError) do + assert_equal "/account?controller=products", url_for(controller: "products", action: "index", only_path: true) + end + + assert_raise(ActionController::UrlGenerationError) do + assert_equal "/de/account?controller=products", url_for(controller: "products", action: "index", locale: "de", only_path: true) + end + end +end + +class TestUrlGenerationErrors < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + get "/products/:id" => "products#show", :as => :product + end + end + + APP = build_app Routes + def app; APP end + + include Routes.url_helpers + + test "url helpers raise a 'missing keys' error for a nil param with optimized helpers" do + url, missing = { action: "show", controller: "products", id: nil }, [:id] + message = "No route matches #{url.inspect}, missing required keys: #{missing.inspect}" + + error = assert_raises(ActionController::UrlGenerationError) { product_path(nil) } + assert_equal message, error.message + end + + test "url helpers raise a 'constraint failure' error for a nil param with non-optimized helpers" do + url, missing = { action: "show", controller: "products", id: nil }, [:id] + message = "No route matches #{url.inspect}, possible unmatched constraints: #{missing.inspect}" + + error = assert_raises(ActionController::UrlGenerationError, message) { product_path(id: nil) } + assert_equal message, error.message + end + + test "url helpers raise message with mixed parameters when generation fails" do + url, missing = { action: "show", controller: "products", id: nil, "id" => "url-tested" }, [:id] + message = "No route matches #{url.inspect}, possible unmatched constraints: #{missing.inspect}" + + # Optimized url helper + error = assert_raises(ActionController::UrlGenerationError) { product_path(nil, "id" => "url-tested") } + assert_equal message, error.message + + # Non-optimized url helper + error = assert_raises(ActionController::UrlGenerationError, message) { product_path(id: nil, "id" => "url-tested") } + assert_equal message, error.message + end +end + +class TestDefaultUrlOptions < ActionDispatch::IntegrationTest + class PostsController < ActionController::Base + def archive + render plain: "posts#archive" + end + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + default_url_options locale: "en" + scope ":locale", format: false do + get "/posts/:year/:month/:day", to: "posts#archive", as: "archived_posts" + end + end + + APP = build_app Routes + + def app + APP + end + + include Routes.url_helpers + + def test_positional_args_with_format_false + assert_equal "/en/posts/2014/12/13", archived_posts_path(2014, 12, 13) + end +end + +class TestErrorsInController < ActionDispatch::IntegrationTest + class ::PostsController < ActionController::Base + def foo + nil.i_do_not_exist + end + + def bar + NonExistingClass.new + end + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + ActiveSupport::Deprecation.silence do + get "/:controller(/:action)" + end + end + + APP = build_app Routes + + def app + APP + end + + def test_legit_no_method_errors_are_not_caught + get "/posts/foo" + assert_equal 500, response.status + end + + def test_legit_name_errors_are_not_caught + get "/posts/bar" + assert_equal 500, response.status + end + + def test_legit_routing_not_found_responses + get "/posts/baz" + assert_equal 404, response.status + + get "/i_do_not_exist" + assert_equal 404, response.status + end +end + +class TestPartialDynamicPathSegments < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } + + get "/songs/song-:song", to: ok + get "/songs/:song-song", to: ok + get "/:artist/song-:song", to: ok + get "/:artist/:song-song", to: ok + + get "/optional/songs(/song-:song)", to: ok + get "/optional/songs(/:song-song)", to: ok + get "/optional/:artist(/song-:song)", to: ok + get "/optional/:artist(/:song-song)", to: ok + end + + APP = build_app Routes + + def app + APP + end + + def test_paths_with_partial_dynamic_segments_are_recognised + get "/david-bowie/changes-song" + assert_equal 200, response.status + assert_params artist: "david-bowie", song: "changes" + + get "/david-bowie/song-changes" + assert_equal 200, response.status + assert_params artist: "david-bowie", song: "changes" + + get "/songs/song-changes" + assert_equal 200, response.status + assert_params song: "changes" + + get "/songs/changes-song" + assert_equal 200, response.status + assert_params song: "changes" + + get "/optional/songs/song-changes" + assert_equal 200, response.status + assert_params song: "changes" + + get "/optional/songs/changes-song" + assert_equal 200, response.status + assert_params song: "changes" + + get "/optional/david-bowie/changes-song" + assert_equal 200, response.status + assert_params artist: "david-bowie", song: "changes" + + get "/optional/david-bowie/song-changes" + assert_equal 200, response.status + assert_params artist: "david-bowie", song: "changes" + end + + private + + def assert_params(params) + assert_equal(params, request.path_parameters) + end +end + +class TestPathParameters < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + scope module: "test_path_parameters" do + scope ":locale", locale: /en|ar/ do + root to: "home#index" + get "/about", to: "pages#about" + end + end + + ActiveSupport::Deprecation.silence do + get ":controller(/:action/(:id))" + end + end + end + + class HomeController < ActionController::Base + include Routes.url_helpers + + def index + render inline: "<%= root_path %>" + end + end + + class PagesController < ActionController::Base + include Routes.url_helpers + + def about + render inline: "<%= root_path(locale: :ar) %> | <%= url_for(locale: :ar) %>" + end + end + + APP = build_app Routes + def app; APP end + + def test_path_parameters_are_not_mutated + get "/en/about" + assert_equal "/ar | /ar/about", @response.body + end +end + +class TestInternalRoutingParams < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| + app.draw do + get "/test_internal/:internal" => "internal#internal" + end + end + + class ::InternalController < ActionController::Base + def internal + head :ok + end + end + + APP = build_app Routes + + def app + APP + end + + def test_paths_with_partial_dynamic_segments_are_recognised + get "/test_internal/123" + assert_equal 200, response.status + + assert_equal( + { controller: "internal", action: "internal", internal: "123" }, + request.path_parameters + ) + end +end + +class FlashRedirectTest < ActionDispatch::IntegrationTest + SessionKey = "_myapp_session" + Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33") + Rotations = ActiveSupport::Messages::RotationConfiguration.new + + class KeyGeneratorMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.key_generator"] ||= Generator + env["action_dispatch.cookies_rotations"] ||= Rotations + + @app.call(env) + end + end + + class FooController < ActionController::Base + def bar + render plain: (flash[:foo] || "foo") + end + end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + get "/foo", to: redirect { |params, req| req.flash[:foo] = "bar"; "/bar" } + get "/bar", to: "flash_redirect_test/foo#bar" + end + + APP = build_app Routes do |middleware| + middleware.use KeyGeneratorMiddleware + middleware.use ActionDispatch::Session::CookieStore, key: SessionKey + middleware.use ActionDispatch::Flash + middleware.delete ActionDispatch::ShowExceptions + end + + def app + APP + end + + include Routes.url_helpers + + def test_block_redirect_commits_flash + get "/foo", env: { "action_dispatch.key_generator" => Generator } + assert_response :redirect + + follow_redirect! + assert_equal "bar", response.body + end +end + +class TestRecognizePath < ActionDispatch::IntegrationTest + class PageConstraint + attr_reader :key, :pattern + + def initialize(key, pattern) + @key = key + @pattern = pattern + end + + def matches?(request) + request.path_parameters[key] =~ pattern + end + end + + stub_controllers do |routes| + Routes = routes + routes.draw do + get "/hash/:foo", to: "pages#show", constraints: { foo: /foo/ } + get "/hash/:bar", to: "pages#show", constraints: { bar: /bar/ } + + get "/proc/:foo", to: "pages#show", constraints: proc { |r| r.path_parameters[:foo] =~ /foo/ } + get "/proc/:bar", to: "pages#show", constraints: proc { |r| r.path_parameters[:bar] =~ /bar/ } + + get "/class/:foo", to: "pages#show", constraints: PageConstraint.new(:foo, /foo/) + get "/class/:bar", to: "pages#show", constraints: PageConstraint.new(:bar, /bar/) + end + end + + APP = build_app Routes + def app + APP + end + + def test_hash_constraints_dont_leak_between_routes + expected_params = { controller: "pages", action: "show", bar: "bar" } + actual_params = recognize_path("/hash/bar") + + assert_equal expected_params, actual_params + end + + def test_proc_constraints_dont_leak_between_routes + expected_params = { controller: "pages", action: "show", bar: "bar" } + actual_params = recognize_path("/proc/bar") + + assert_equal expected_params, actual_params + end + + def test_class_constraints_dont_leak_between_routes + expected_params = { controller: "pages", action: "show", bar: "bar" } + actual_params = recognize_path("/class/bar") + + assert_equal expected_params, actual_params + end + + private + + def recognize_path(*args) + Routes.recognize_path(*args) + end +end + +class TestRelativeUrlRootGeneration < ActionDispatch::IntegrationTest + config = ActionDispatch::Routing::RouteSet::Config.new("/blog", false) + + stub_controllers(config) do |routes| + Routes = routes + + routes.draw do + get "/", to: "posts#index", as: :posts + get "/:id", to: "posts#show", as: :post + end + end + + include Routes.url_helpers + + APP = build_app Routes + + def app + APP + end + + def test_url_helpers + assert_equal "/blog/", posts_path({}) + assert_equal "/blog/", Routes.url_helpers.posts_path({}) + + assert_equal "/blog/1", post_path(id: "1") + assert_equal "/blog/1", Routes.url_helpers.post_path(id: "1") + end + + def test_optimized_url_helpers + assert_equal "/blog/", posts_path + assert_equal "/blog/", Routes.url_helpers.posts_path + + assert_equal "/blog/1", post_path("1") + assert_equal "/blog/1", Routes.url_helpers.post_path("1") + end +end diff --git a/actionpack/test/dispatch/runner_test.rb b/actionpack/test/dispatch/runner_test.rb new file mode 100644 index 0000000000..f16c7963af --- /dev/null +++ b/actionpack/test/dispatch/runner_test.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class RunnerTest < ActiveSupport::TestCase + test "runner preserves the setting of integration_session" do + runner = Class.new do + def before_setup + end + end.new + + runner.extend(ActionDispatch::Integration::Runner) + runner.integration_session.host! "lvh.me" + + runner.before_setup + + assert_equal "lvh.me", runner.integration_session.host + end +end diff --git a/actionpack/test/dispatch/session/abstract_store_test.rb b/actionpack/test/dispatch/session/abstract_store_test.rb new file mode 100644 index 0000000000..47616db15a --- /dev/null +++ b/actionpack/test/dispatch/session/abstract_store_test.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_dispatch/middleware/session/abstract_store" + +module ActionDispatch + module Session + class AbstractStoreTest < ActiveSupport::TestCase + class MemoryStore < AbstractStore + def initialize(app) + @sessions = {} + super + end + + def find_session(env, sid) + sid ||= 1 + session = @sessions[sid] ||= {} + [sid, session] + end + + def write_session(env, sid, session, options) + @sessions[sid] = session + end + end + + def test_session_is_set + env = {} + as = MemoryStore.new app + as.call(env) + + assert @env + assert Request::Session.find ActionDispatch::Request.new @env + end + + def test_new_session_object_is_merged_with_old + env = {} + as = MemoryStore.new app + as.call(env) + + assert @env + session = Request::Session.find ActionDispatch::Request.new @env + session["foo"] = "bar" + + as.call(@env) + session1 = Request::Session.find ActionDispatch::Request.new @env + + assert_not_equal session, session1 + assert_equal session.to_hash, session1.to_hash + end + + private + def app(&block) + @env = nil + lambda { |env| @env = env } + end + end + end +end diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb new file mode 100644 index 0000000000..06e67fac9f --- /dev/null +++ b/actionpack/test/dispatch/session/cache_store_test.rb @@ -0,0 +1,183 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "fixtures/session_autoload_test/session_autoload_test/foo" + +class CacheStoreTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + def no_session_access + head :ok + end + + def set_session_value + session[:foo] = "bar" + head :ok + end + + def set_serialized_session_value + session[:foo] = SessionAutoloadTest::Foo.new + head :ok + end + + def get_session_value + render plain: "foo: #{session[:foo].inspect}" + end + + def get_session_id + render plain: "#{request.session.id}" + end + + def call_reset_session + session[:bar] + reset_session + session[:bar] = "baz" + head :ok + end + end + + def test_setting_and_getting_session_value + with_test_route_set do + get "/set_session_value" + assert_response :success + assert cookies["_session_id"] + + get "/get_session_value" + assert_response :success + assert_equal 'foo: "bar"', response.body + end + end + + def test_getting_nil_session_value + with_test_route_set do + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body + end + end + + def test_getting_session_value_after_session_reset + with_test_route_set do + get "/set_session_value" + assert_response :success + assert cookies["_session_id"] + session_cookie = cookies.send(:hash_for)["_session_id"] + + get "/call_reset_session" + assert_response :success + assert_not_equal [], headers["Set-Cookie"] + + cookies << session_cookie # replace our new session_id with our old, pre-reset session_id + + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body, "data for this session should have been obliterated from cache" + end + end + + def test_getting_from_nonexistent_session + with_test_route_set do + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body + assert_nil cookies["_session_id"], "should only create session on write, not read" + end + end + + def test_setting_session_value_after_session_reset + with_test_route_set do + get "/set_session_value" + assert_response :success + assert cookies["_session_id"] + session_id = cookies["_session_id"] + + get "/call_reset_session" + assert_response :success + assert_not_equal [], headers["Set-Cookie"] + + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body + + get "/get_session_id" + assert_response :success + assert_not_equal session_id, response.body + end + end + + def test_getting_session_id + with_test_route_set do + get "/set_session_value" + assert_response :success + assert cookies["_session_id"] + session_id = cookies["_session_id"] + + get "/get_session_id" + assert_response :success + assert_equal session_id, response.body, "should be able to read session id without accessing the session hash" + end + end + + def test_deserializes_unloaded_class + with_test_route_set do + with_autoload_path "session_autoload_test" do + get "/set_serialized_session_value" + assert_response :success + assert cookies["_session_id"] + end + with_autoload_path "session_autoload_test" do + get "/get_session_id" + assert_response :success + end + with_autoload_path "session_autoload_test" do + get "/get_session_value" + assert_response :success + assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class" + end + end + end + + def test_doesnt_write_session_cookie_if_session_id_is_already_exists + with_test_route_set do + get "/set_session_value" + assert_response :success + assert cookies["_session_id"] + + get "/get_session_value" + assert_response :success + assert_nil headers["Set-Cookie"], "should not resend the cookie again if session_id cookie is already exists" + end + end + + def test_prevents_session_fixation + with_test_route_set do + assert_nil @cache.read("_session_id:0xhax") + + cookies["_session_id"] = "0xhax" + get "/set_session_value" + + assert_response :success + assert_not_equal "0xhax", cookies["_session_id"] + assert_nil @cache.read("_session_id:0xhax") + assert_equal({ "foo" => "bar" }, @cache.read("_session_id:#{cookies['_session_id']}")) + end + end + + private + def with_test_route_set + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + get ":action", to: ::CacheStoreTest::TestController + end + end + + @app = self.class.build_app(set) do |middleware| + @cache = ActiveSupport::Cache::MemoryStore.new + middleware.use ActionDispatch::Session::CacheStore, key: "_session_id", cache: @cache + middleware.delete ActionDispatch::ShowExceptions + end + + yield + end + end +end diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb new file mode 100644 index 0000000000..e34426a471 --- /dev/null +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -0,0 +1,417 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "stringio" +require "active_support/key_generator" +require "active_support/messages/rotation_configuration" + +class CookieStoreTest < ActionDispatch::IntegrationTest + SessionKey = "_myapp_session" + SessionSecret = "b3c631c314c0bbca50c1b2843150fe33" + SessionSalt = "authenticated encrypted cookie" + + Generator = ActiveSupport::KeyGenerator.new(SessionSecret, iterations: 1000) + Rotations = ActiveSupport::Messages::RotationConfiguration.new + + Encryptor = ActiveSupport::MessageEncryptor.new( + Generator.generate_key(SessionSalt, 32), cipher: "aes-256-gcm", serializer: Marshal + ) + + class TestController < ActionController::Base + def no_session_access + head :ok + end + + def persistent_session_id + render plain: session[:session_id] + end + + def set_session_value + session[:foo] = "bar" + render body: nil + end + + def get_session_value + render plain: "foo: #{session[:foo].inspect}" + end + + def get_session_id + render plain: "id: #{request.session.id}" + end + + def get_class_after_reset_session + reset_session + render plain: "class: #{session.class}" + end + + def call_session_clear + session.clear + head :ok + end + + def call_reset_session + reset_session + head :ok + end + + def raise_data_overflow + session[:foo] = "bye!" * 1024 + head :ok + end + + def change_session_id + request.session.options[:id] = nil + get_session_id + end + + def renew_session_id + request.session_options[:renew] = true + head :ok + end + end + + def parse_cookie_from_header + cookie_matches = headers["Set-Cookie"].match(/#{SessionKey}=([^;]+)/) + cookie_matches && cookie_matches[1] + end + + def assert_session_cookie(cookie_string, contents) + assert_includes headers["Set-Cookie"], cookie_string + + session_value = parse_cookie_from_header + session_data = Encryptor.decrypt_and_verify(Rack::Utils.unescape(session_value)) rescue nil + + assert_not_nil session_data, "session failed to decrypt" + assert_equal session_data.slice(*contents.keys), contents + end + + def test_setting_session_value + with_test_route_set do + get "/set_session_value" + + assert_response :success + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" + end + end + + def test_getting_session_value + with_test_route_set do + get "/set_session_value" + get "/get_session_value" + + assert_response :success + assert_equal 'foo: "bar"', response.body + end + end + + def test_getting_session_id + with_test_route_set do + get "/set_session_value" + get "/persistent_session_id" + + assert_response :success + assert_equal 32, response.body.size + session_id = response.body + + get "/get_session_id" + assert_response :success + assert_equal "id: #{session_id}", response.body, "should be able to read session id without accessing the session hash" + end + end + + def test_disregards_tampered_sessions + with_test_route_set do + encryptor = ActiveSupport::MessageEncryptor.new("A" * 32, cipher: "aes-256-gcm", serializer: Marshal) + + cookies[SessionKey] = encryptor.encrypt_and_sign("foo" => "bar", "session_id" => "abc") + + get "/get_session_value" + + assert_response :success + assert_equal "foo: nil", response.body + end + end + + def test_does_not_set_secure_cookies_over_http + with_test_route_set(secure: true) do + get "/set_session_value" + assert_response :success + assert_nil headers["Set-Cookie"] + end + end + + def test_properly_renew_cookies + with_test_route_set do + get "/set_session_value" + get "/persistent_session_id" + session_id = response.body + get "/renew_session_id" + get "/persistent_session_id" + assert_not_equal response.body, session_id + end + end + + def test_does_set_secure_cookies_over_https + with_test_route_set(secure: true) do + get "/set_session_value", headers: { "HTTPS" => "on" } + + assert_response :success + assert_session_cookie "path=/; secure; HttpOnly", "foo" => "bar" + end + end + + # {:foo=>#<SessionAutoloadTest::Foo bar:"baz">, :session_id=>"ce8b0752a6ab7c7af3cdb8a80e6b9e46"} + EncryptedSerializedCookie = "9RZ2Fij0qLveUwM4s+CCjGqhpjyUC8jiBIf/AiBr9M3TB8xh2vQZtvSOMfN3uf6oYbbpIDHAcOFIEl69FcW1ozQYeSrCLonYCazoh34ZdYskIQfGwCiSYleVXG1OD9Z4jFqeVArw4Ewm0paOOPLbN1rc6A==--I359v/KWdZ1ok0ey--JFFhuPOY7WUo6tB/eP05Aw==" + + def test_deserializes_unloaded_classes_on_get_id + with_test_route_set do + with_autoload_path "session_autoload_test" do + cookies[SessionKey] = EncryptedSerializedCookie + get "/get_session_id" + assert_response :success + assert_equal "id: ce8b0752a6ab7c7af3cdb8a80e6b9e46", response.body, "should auto-load unloaded class" + end + end + end + + def test_deserializes_unloaded_classes_on_get_value + with_test_route_set do + with_autoload_path "session_autoload_test" do + cookies[SessionKey] = EncryptedSerializedCookie + get "/get_session_value" + assert_response :success + assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class" + end + end + end + + def test_close_raises_when_data_overflows + with_test_route_set do + assert_raise(ActionDispatch::Cookies::CookieOverflow) { + get "/raise_data_overflow" + } + end + end + + def test_doesnt_write_session_cookie_if_session_is_not_accessed + with_test_route_set do + get "/no_session_access" + assert_response :success + assert_nil headers["Set-Cookie"] + end + end + + def test_doesnt_write_session_cookie_if_session_is_unchanged + with_test_route_set do + cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--" \ + "fef868465920f415f2c0652d6910d3af288a0367" + get "/no_session_access" + assert_response :success + assert_nil headers["Set-Cookie"] + end + end + + def test_setting_session_value_after_session_reset + with_test_route_set do + get "/set_session_value" + assert_response :success + session_payload = response.body + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" + + get "/call_reset_session" + assert_response :success + assert_not_equal [], headers["Set-Cookie"] + assert_not_nil session_payload + assert_not_equal session_payload, cookies[SessionKey] + + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body + end + end + + def test_class_type_after_session_reset + with_test_route_set do + get "/set_session_value" + assert_response :success + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" + + get "/get_class_after_reset_session" + assert_response :success + assert_not_equal [], headers["Set-Cookie"] + assert_equal "class: ActionDispatch::Request::Session", response.body + end + end + + def test_getting_from_nonexistent_session + with_test_route_set do + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body + assert_nil headers["Set-Cookie"], "should only create session on write, not read" + end + end + + def test_setting_session_value_after_session_clear + with_test_route_set do + get "/set_session_value" + assert_response :success + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" + + get "/call_session_clear" + assert_response :success + + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body + end + end + + def test_persistent_session_id + with_test_route_set do + get "/set_session_value" + get "/persistent_session_id" + assert_response :success + assert_equal 32, response.body.size + session_id = response.body + get "/persistent_session_id" + assert_equal session_id, response.body + reset! + get "/persistent_session_id" + assert_not_equal session_id, response.body + end + end + + def test_setting_session_id_to_nil_is_respected + with_test_route_set do + get "/set_session_value" + get "/get_session_id" + sid = response.body + assert_equal 36, sid.size + + get "/change_session_id" + assert_not_equal sid, response.body + end + end + + def test_session_store_with_expire_after + with_test_route_set(expire_after: 5.hours) do + # First request accesses the session + time = Time.local(2008, 4, 24) + + Time.stub :now, time do + expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") + + get "/set_session_value" + + assert_response :success + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" + end + + # Second request does not access the session + time = time + 3.hours + Time.stub :now, time do + expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") + + get "/no_session_access" + + assert_response :success + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" + end + end + end + + def test_session_store_with_expire_after_does_not_accept_expired_session + with_test_route_set(expire_after: 5.hours) do + # First request accesses the session + time = Time.local(2017, 11, 12) + + Time.stub :now, time do + expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") + + get "/set_session_value" + get "/get_session_value" + + assert_response :success + assert_equal 'foo: "bar"', response.body + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" + end + + # Second request is beyond the expiry time and the session is invalidated + time += 5.hours + 1.minute + + Time.stub :now, time do + get "/get_session_value" + + assert_response :success + assert_equal "foo: nil", response.body + end + end + end + + def test_session_store_with_explicit_domain + with_test_route_set(domain: "example.es") do + get "/set_session_value" + assert_match(/domain=example\.es/, headers["Set-Cookie"]) + headers["Set-Cookie"] + end + end + + def test_session_store_without_domain + with_test_route_set do + get "/set_session_value" + assert_no_match(/domain\=/, headers["Set-Cookie"]) + end + end + + def test_session_store_with_nil_domain + with_test_route_set(domain: nil) do + get "/set_session_value" + assert_no_match(/domain\=/, headers["Set-Cookie"]) + end + end + + def test_session_store_with_all_domains + with_test_route_set(domain: :all) do + get "/set_session_value" + assert_match(/domain=\.example\.com/, headers["Set-Cookie"]) + end + end + + private + + # Overwrite get to send SessionSecret in env hash + def get(path, *args) + args[0] ||= {} + args[0][:headers] ||= {} + args[0][:headers].tap do |config| + config["action_dispatch.secret_key_base"] = SessionSecret + config["action_dispatch.authenticated_encrypted_cookie_salt"] = SessionSalt + config["action_dispatch.use_authenticated_cookie_encryption"] = true + + config["action_dispatch.key_generator"] ||= Generator + config["action_dispatch.cookies_rotations"] ||= Rotations + end + + super(path, *args) + end + + def with_test_route_set(options = {}) + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + get ":action", to: ::CookieStoreTest::TestController + end + end + + options = { key: SessionKey }.merge!(options) + + @app = self.class.build_app(set) do |middleware| + middleware.use ActionDispatch::Session::CookieStore, options + middleware.delete ActionDispatch::ShowExceptions + end + + yield + end + end +end diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb new file mode 100644 index 0000000000..9b51ee1cad --- /dev/null +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "securerandom" + +# You need to start a memcached server inorder to run these tests +class MemCacheStoreTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + def no_session_access + head :ok + end + + def set_session_value + session[:foo] = "bar" + head :ok + end + + def set_serialized_session_value + session[:foo] = SessionAutoloadTest::Foo.new + head :ok + end + + def get_session_value + render plain: "foo: #{session[:foo].inspect}" + end + + def get_session_id + render plain: "#{request.session.id}" + end + + def call_reset_session + session[:bar] + reset_session + session[:bar] = "baz" + head :ok + end + end + + begin + require "dalli" + ss = Dalli::Client.new("localhost:11211").stats + raise Dalli::DalliError unless ss["localhost:11211"] + + def test_setting_and_getting_session_value + with_test_route_set do + get "/set_session_value" + assert_response :success + assert cookies["_session_id"] + + get "/get_session_value" + assert_response :success + assert_equal 'foo: "bar"', response.body + end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace + end + + def test_getting_nil_session_value + with_test_route_set do + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body + end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace + end + + def test_getting_session_value_after_session_reset + with_test_route_set do + get "/set_session_value" + assert_response :success + assert cookies["_session_id"] + session_cookie = cookies.send(:hash_for)["_session_id"] + + get "/call_reset_session" + assert_response :success + assert_not_equal [], headers["Set-Cookie"] + + cookies << session_cookie # replace our new session_id with our old, pre-reset session_id + + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body, "data for this session should have been obliterated from memcached" + end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace + end + + def test_getting_from_nonexistent_session + with_test_route_set do + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body + assert_nil cookies["_session_id"], "should only create session on write, not read" + end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace + end + + def test_setting_session_value_after_session_reset + with_test_route_set do + get "/set_session_value" + assert_response :success + assert cookies["_session_id"] + session_id = cookies["_session_id"] + + get "/call_reset_session" + assert_response :success + assert_not_equal [], headers["Set-Cookie"] + + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body + + get "/get_session_id" + assert_response :success + assert_not_equal session_id, response.body + end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace + end + + def test_getting_session_id + with_test_route_set do + get "/set_session_value" + assert_response :success + assert cookies["_session_id"] + session_id = cookies["_session_id"] + + get "/get_session_id" + assert_response :success + assert_equal session_id, response.body, "should be able to read session id without accessing the session hash" + end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace + end + + def test_deserializes_unloaded_class + with_test_route_set do + with_autoload_path "session_autoload_test" do + get "/set_serialized_session_value" + assert_response :success + assert cookies["_session_id"] + end + with_autoload_path "session_autoload_test" do + get "/get_session_id" + assert_response :success + end + end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace + end + + def test_doesnt_write_session_cookie_if_session_id_is_already_exists + with_test_route_set do + get "/set_session_value" + assert_response :success + assert cookies["_session_id"] + + get "/get_session_value" + assert_response :success + assert_nil headers["Set-Cookie"], "should not resend the cookie again if session_id cookie is already exists" + end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace + end + + def test_prevents_session_fixation + with_test_route_set do + get "/get_session_value" + assert_response :success + assert_equal "foo: nil", response.body + session_id = cookies["_session_id"] + + reset! + + get "/set_session_value", params: { _session_id: session_id } + assert_response :success + assert_not_equal session_id, cookies["_session_id"] + end + rescue Dalli::RingError => ex + skip ex.message, ex.backtrace + end + rescue LoadError, RuntimeError, Dalli::DalliError + $stderr.puts "Skipping MemCacheStoreTest tests. Start memcached and try again." + end + + private + def with_test_route_set + with_routing do |set| + set.draw do + ActiveSupport::Deprecation.silence do + get ":action", to: ::MemCacheStoreTest::TestController + end + end + + @app = self.class.build_app(set) do |middleware| + middleware.use ActionDispatch::Session::MemCacheStore, key: "_session_id", namespace: "mem_cache_store_test:#{SecureRandom.hex(10)}" + middleware.delete ActionDispatch::ShowExceptions + end + + yield + end + end +end diff --git a/actionpack/test/dispatch/session/test_session_test.rb b/actionpack/test/dispatch/session/test_session_test.rb new file mode 100644 index 0000000000..e90162a5fe --- /dev/null +++ b/actionpack/test/dispatch/session/test_session_test.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "stringio" + +class ActionController::TestSessionTest < ActiveSupport::TestCase + def test_initialize_with_values + session = ActionController::TestSession.new(one: "one", two: "two") + assert_equal("one", session[:one]) + assert_equal("two", session[:two]) + end + + def test_setting_session_item_sets_item + session = ActionController::TestSession.new + session[:key] = "value" + assert_equal("value", session[:key]) + end + + def test_calling_delete_removes_item_and_returns_its_value + session = ActionController::TestSession.new + session[:key] = "value" + assert_equal("value", session[:key]) + assert_equal("value", session.delete(:key)) + assert_nil(session[:key]) + end + + def test_calling_update_with_params_passes_to_attributes + session = ActionController::TestSession.new + session.update("key" => "value") + assert_equal("value", session[:key]) + end + + def test_clear_empties_session + session = ActionController::TestSession.new(one: "one", two: "two") + session.clear + assert_nil(session[:one]) + assert_nil(session[:two]) + end + + def test_keys_and_values + session = ActionController::TestSession.new(one: "1", two: "2") + assert_equal %w(one two), session.keys + assert_equal %w(1 2), session.values + end + + def test_fetch_returns_default + session = ActionController::TestSession.new(one: "1") + assert_equal("2", session.fetch(:two, "2")) + end + + def test_fetch_on_symbol_returns_value + session = ActionController::TestSession.new(one: "1") + assert_equal("1", session.fetch(:one)) + end + + def test_fetch_on_string_returns_value + session = ActionController::TestSession.new(one: "1") + assert_equal("1", session.fetch("one")) + end + + def test_fetch_returns_block_value + session = ActionController::TestSession.new(one: "1") + assert_equal(2, session.fetch("2") { |key| key.to_i }) + end +end diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb new file mode 100644 index 0000000000..f802abc653 --- /dev/null +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class ShowExceptionsTest < ActionDispatch::IntegrationTest + class Boomer + def call(env) + req = ActionDispatch::Request.new(env) + case req.path + when "/not_found" + raise AbstractController::ActionNotFound + when "/bad_params", "/bad_params.json" + begin + raise StandardError.new + rescue + raise ActionDispatch::Http::Parameters::ParseError + end + when "/method_not_allowed" + raise ActionController::MethodNotAllowed, "PUT" + when "/unknown_http_method" + raise ActionController::UnknownHttpMethod + when "/not_found_original_exception" + begin + raise AbstractController::ActionNotFound.new + rescue + raise ActionView::Template::Error.new("template") + end + else + raise "puke!" + end + end + end + + ProductionApp = ActionDispatch::ShowExceptions.new(Boomer.new, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")) + + test "skip exceptions app if not showing exceptions" do + @app = ProductionApp + assert_raise RuntimeError do + get "/", env: { "action_dispatch.show_exceptions" => false } + end + end + + test "rescue with error page" do + @app = ProductionApp + + get "/", env: { "action_dispatch.show_exceptions" => true } + assert_response 500 + assert_equal "500 error fixture\n", body + + get "/bad_params", env: { "action_dispatch.show_exceptions" => true } + assert_response 400 + assert_equal "400 error fixture\n", body + + get "/not_found", env: { "action_dispatch.show_exceptions" => true } + assert_response 404 + assert_equal "404 error fixture\n", body + + get "/method_not_allowed", env: { "action_dispatch.show_exceptions" => true } + assert_response 405 + assert_equal "", body + + get "/unknown_http_method", env: { "action_dispatch.show_exceptions" => true } + assert_response 405 + assert_equal "", body + end + + test "localize rescue error page" do + old_locale, I18n.locale = I18n.locale, :da + + begin + @app = ProductionApp + + get "/", env: { "action_dispatch.show_exceptions" => true } + assert_response 500 + assert_equal "500 localized error fixture\n", body + + get "/not_found", env: { "action_dispatch.show_exceptions" => true } + assert_response 404 + assert_equal "404 error fixture\n", body + ensure + I18n.locale = old_locale + end + end + + test "sets the HTTP charset parameter" do + @app = ProductionApp + + get "/", env: { "action_dispatch.show_exceptions" => true } + assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] + end + + test "show registered original exception for wrapped exceptions" do + @app = ProductionApp + + get "/not_found_original_exception", env: { "action_dispatch.show_exceptions" => true } + assert_response 404 + assert_match(/404 error/, body) + end + + test "calls custom exceptions app" do + exceptions_app = lambda do |env| + assert_kind_of AbstractController::ActionNotFound, env["action_dispatch.exception"] + assert_equal "/404", env["PATH_INFO"] + assert_equal "/not_found_original_exception", env["action_dispatch.original_path"] + [404, { "Content-Type" => "text/plain" }, ["YOU FAILED"]] + end + + @app = ActionDispatch::ShowExceptions.new(Boomer.new, exceptions_app) + get "/not_found_original_exception", env: { "action_dispatch.show_exceptions" => true } + assert_response 404 + assert_equal "YOU FAILED", body + end + + test "returns an empty response if custom exceptions app returns X-Cascade pass" do + exceptions_app = lambda do |env| + [404, { "X-Cascade" => "pass" }, []] + end + + @app = ActionDispatch::ShowExceptions.new(Boomer.new, exceptions_app) + get "/method_not_allowed", env: { "action_dispatch.show_exceptions" => true } + assert_response 405 + assert_equal "", body + end + + test "bad params exception is returned in the correct format" do + @app = ProductionApp + + get "/bad_params", env: { "action_dispatch.show_exceptions" => true } + assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] + assert_response 400 + assert_match(/400 error/, body) + + get "/bad_params.json", env: { "action_dispatch.show_exceptions" => true } + assert_equal "application/json; charset=utf-8", response.headers["Content-Type"] + assert_response 400 + assert_equal("{\"status\":400,\"error\":\"Bad Request\"}", body) + end +end diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb new file mode 100644 index 0000000000..baf46e7c7e --- /dev/null +++ b/actionpack/test/dispatch/ssl_test.rb @@ -0,0 +1,228 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class SSLTest < ActionDispatch::IntegrationTest + HEADERS = Rack::Utils::HeaderHash.new "Content-Type" => "text/html" + + attr_accessor :app + + def build_app(headers: {}, ssl_options: {}) + headers = HEADERS.merge(headers) + ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options.reverse_merge(hsts: { subdomains: true }) + end +end + +class RedirectSSLTest < SSLTest + def assert_not_redirected(url, headers: {}, redirect: {}) + self.app = build_app ssl_options: { redirect: redirect } + get url, headers: headers + assert_response :ok + end + + def assert_redirected(redirect: {}, from: "http://a/b?c=d", to: from.sub("http", "https")) + redirect = { status: 301, body: [] }.merge(redirect) + + self.app = build_app ssl_options: { redirect: redirect } + + get from + assert_response redirect[:status] || 301 + assert_redirected_to to + assert_equal redirect[:body].join, @response.body + end + + def assert_post_redirected(redirect: {}, from: "http://a/b?c=d", + to: from.sub("http", "https")) + + self.app = build_app ssl_options: { redirect: redirect } + + post from + assert_response redirect[:status] || 307 + assert_redirected_to to + end + + test "exclude can avoid redirect" do + excluding = { exclude: -> request { request.path =~ /healthcheck/ } } + + assert_not_redirected "http://example.org/healthcheck", redirect: excluding + assert_redirected from: "http://example.org/", redirect: excluding + end + + test "https is not redirected" do + assert_not_redirected "https://example.org" + end + + test "proxied https is not redirected" do + assert_not_redirected "http://example.org", headers: { "HTTP_X_FORWARDED_PROTO" => "https" } + end + + test "http is redirected to https" do + assert_redirected + end + + test "http POST is redirected to https with status 307" do + assert_post_redirected + end + + test "redirect with non-301 status" do + assert_redirected redirect: { status: 307 } + end + + test "redirect with custom body" do + assert_redirected redirect: { body: ["foo"] } + end + + test "redirect to specific host" do + assert_redirected redirect: { host: "ssl" }, to: "https://ssl/b?c=d" + end + + test "redirect to default port" do + assert_redirected redirect: { port: 443 } + end + + test "redirect to non-default port" do + assert_redirected redirect: { port: 8443 }, to: "https://a:8443/b?c=d" + end + + test "redirect to different host and non-default port" do + assert_redirected redirect: { host: "ssl", port: 8443 }, to: "https://ssl:8443/b?c=d" + end + + test "redirect to different host including port" do + assert_redirected redirect: { host: "ssl:443" }, to: "https://ssl:443/b?c=d" + end + + test "no redirect with redirect set to false" do + assert_not_redirected "http://example.org", redirect: false + end +end + +class StrictTransportSecurityTest < SSLTest + EXPECTED = "max-age=31536000" + EXPECTED_WITH_SUBDOMAINS = "max-age=31536000; includeSubDomains" + + def assert_hsts(expected, url: "https://example.org", hsts: { subdomains: true }, headers: {}) + self.app = build_app ssl_options: { hsts: hsts }, headers: headers + get url + if expected.nil? + assert_nil response.headers["Strict-Transport-Security"] + else + assert_equal expected, response.headers["Strict-Transport-Security"] + end + end + + test "enabled by default" do + assert_hsts EXPECTED_WITH_SUBDOMAINS + end + + test "not sent with http:// responses" do + assert_hsts nil, url: "http://example.org" + end + + test "defers to app-provided header" do + assert_hsts "app-provided", headers: { "Strict-Transport-Security" => "app-provided" } + end + + test "hsts: true enables default settings" do + assert_hsts EXPECTED_WITH_SUBDOMAINS, hsts: true + end + + test "hsts: false sets max-age to zero, clearing browser HSTS settings" do + assert_hsts "max-age=0; includeSubDomains", hsts: false + end + + test ":expires sets max-age" do + assert_hsts "max-age=500; includeSubDomains", hsts: { expires: 500 } + end + + test ":expires supports AS::Duration arguments" do + assert_hsts "max-age=31556952; includeSubDomains", hsts: { expires: 1.year } + end + + test "include subdomains" do + assert_hsts "#{EXPECTED}; includeSubDomains", hsts: { subdomains: true } + end + + test "exclude subdomains" do + assert_hsts EXPECTED, hsts: { subdomains: false } + end + + test "opt in to browser preload lists" do + assert_hsts "#{EXPECTED_WITH_SUBDOMAINS}; preload", hsts: { preload: true } + end + + test "opt out of browser preload lists" do + assert_hsts EXPECTED_WITH_SUBDOMAINS, hsts: { preload: false } + end +end + +class SecureCookiesTest < SSLTest + DEFAULT = %(id=1; path=/\ntoken=abc; path=/; secure; HttpOnly) + + def get(**options) + self.app = build_app(**options) + super "https://example.org" + end + + def assert_cookies(*expected) + assert_equal expected, response.headers["Set-Cookie"].split("\n") + end + + def test_flag_cookies_as_secure + get headers: { "Set-Cookie" => DEFAULT } + assert_cookies "id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly" + end + + def test_flag_cookies_as_secure_at_end_of_line + get headers: { "Set-Cookie" => "problem=def; path=/; HttpOnly; secure" } + assert_cookies "problem=def; path=/; HttpOnly; secure" + end + + def test_flag_cookies_as_secure_with_more_spaces_before + get headers: { "Set-Cookie" => "problem=def; path=/; HttpOnly; secure" } + assert_cookies "problem=def; path=/; HttpOnly; secure" + end + + def test_flag_cookies_as_secure_with_more_spaces_after + get headers: { "Set-Cookie" => "problem=def; path=/; secure; HttpOnly" } + assert_cookies "problem=def; path=/; secure; HttpOnly" + end + + def test_flag_cookies_as_secure_with_has_not_spaces_before + get headers: { "Set-Cookie" => "problem=def; path=/;secure; HttpOnly" } + assert_cookies "problem=def; path=/;secure; HttpOnly" + end + + def test_flag_cookies_as_secure_with_has_not_spaces_after + get headers: { "Set-Cookie" => "problem=def; path=/; secure;HttpOnly" } + assert_cookies "problem=def; path=/; secure;HttpOnly" + end + + def test_flag_cookies_as_secure_with_ignore_case + get headers: { "Set-Cookie" => "problem=def; path=/; Secure; HttpOnly" } + assert_cookies "problem=def; path=/; Secure; HttpOnly" + end + + def test_cookies_as_not_secure_with_secure_cookies_disabled + get headers: { "Set-Cookie" => DEFAULT }, ssl_options: { secure_cookies: false } + assert_cookies(*DEFAULT.split("\n")) + end + + def test_cookies_as_not_secure_with_exclude + excluding = { exclude: -> request { request.domain =~ /example/ } } + get headers: { "Set-Cookie" => DEFAULT }, ssl_options: { redirect: excluding } + + assert_cookies(*DEFAULT.split("\n")) + assert_response :ok + end + + def test_no_cookies + get + assert_nil response.headers["Set-Cookie"] + end + + def test_keeps_original_headers_behavior + get headers: { "Connection" => %w[close] } + assert_equal "close", response.headers["Connection"] + end +end diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb new file mode 100644 index 0000000000..d44aa00122 --- /dev/null +++ b/actionpack/test/dispatch/static_test.rb @@ -0,0 +1,318 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "zlib" + +module StaticTests + DummyApp = lambda { |env| + [200, { "Content-Type" => "text/plain" }, ["Hello, World!"]] + } + + def setup + silence_warnings do + @default_internal_encoding = Encoding.default_internal + @default_external_encoding = Encoding.default_external + end + end + + def teardown + silence_warnings do + Encoding.default_internal = @default_internal_encoding + Encoding.default_external = @default_external_encoding + end + end + + def test_serves_dynamic_content + assert_equal "Hello, World!", get("/nofile").body + end + + def test_handles_urls_with_bad_encoding + assert_equal "Hello, World!", get("/doorkeeper%E3E4").body + end + + def test_handles_urls_with_ascii_8bit + assert_equal "Hello, World!", get((+"/doorkeeper%E3E4").force_encoding("ASCII-8BIT")).body + end + + def test_handles_urls_with_ascii_8bit_on_win_31j + silence_warnings do + Encoding.default_internal = "Windows-31J" + Encoding.default_external = "Windows-31J" + end + assert_equal "Hello, World!", get((+"/doorkeeper%E3E4").force_encoding("ASCII-8BIT")).body + end + + def test_handles_urls_with_null_byte + assert_equal "Hello, World!", get("/doorkeeper%00").body + end + + def test_serves_static_index_at_root + assert_html "/index.html", get("/index.html") + assert_html "/index.html", get("/index") + assert_html "/index.html", get("/") + assert_html "/index.html", get("") + end + + def test_serves_static_file_in_directory + assert_html "/foo/bar.html", get("/foo/bar.html") + assert_html "/foo/bar.html", get("/foo/bar/") + assert_html "/foo/bar.html", get("/foo/bar") + end + + def test_serves_static_index_file_in_directory + assert_html "/foo/index.html", get("/foo/index.html") + assert_html "/foo/index.html", get("/foo/index") + assert_html "/foo/index.html", get("/foo/") + assert_html "/foo/index.html", get("/foo") + end + + def test_serves_file_with_same_name_before_index_in_directory + assert_html "/bar.html", get("/bar") + end + + def test_served_static_file_with_non_english_filename + assert_html "means hello in Japanese\n", get("/foo/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF.html") + end + + def test_served_gzipped_static_file_with_non_english_filename + response = get("/foo/%E3%81%95%E3%82%88%E3%81%86%E3%81%AA%E3%82%89.html", "HTTP_ACCEPT_ENCODING" => "gzip") + + assert_gzip "/foo/さようなら.html", response + assert_equal "text/html", response.headers["Content-Type"] + assert_equal "Accept-Encoding", response.headers["Vary"] + assert_equal "gzip", response.headers["Content-Encoding"] + end + + def test_serves_static_file_with_exclamation_mark_in_filename + with_static_file "/foo/foo!bar.html" do |file| + assert_html file, get("/foo/foo%21bar.html") + assert_html file, get("/foo/foo!bar.html") + end + end + + def test_serves_static_file_with_dollar_sign_in_filename + with_static_file "/foo/foo$bar.html" do |file| + assert_html file, get("/foo/foo%24bar.html") + assert_html file, get("/foo/foo$bar.html") + end + end + + def test_serves_static_file_with_ampersand_in_filename + with_static_file "/foo/foo&bar.html" do |file| + assert_html file, get("/foo/foo%26bar.html") + assert_html file, get("/foo/foo&bar.html") + end + end + + def test_serves_static_file_with_apostrophe_in_filename + with_static_file "/foo/foo'bar.html" do |file| + assert_html file, get("/foo/foo%27bar.html") + assert_html file, get("/foo/foo'bar.html") + end + end + + def test_serves_static_file_with_parentheses_in_filename + with_static_file "/foo/foo(bar).html" do |file| + assert_html file, get("/foo/foo%28bar%29.html") + assert_html file, get("/foo/foo(bar).html") + end + end + + def test_serves_static_file_with_plus_sign_in_filename + with_static_file "/foo/foo+bar.html" do |file| + assert_html file, get("/foo/foo%2Bbar.html") + assert_html file, get("/foo/foo+bar.html") + end + end + + def test_serves_static_file_with_comma_in_filename + with_static_file "/foo/foo,bar.html" do |file| + assert_html file, get("/foo/foo%2Cbar.html") + assert_html file, get("/foo/foo,bar.html") + end + end + + def test_serves_static_file_with_semi_colon_in_filename + with_static_file "/foo/foo;bar.html" do |file| + assert_html file, get("/foo/foo%3Bbar.html") + assert_html file, get("/foo/foo;bar.html") + end + end + + def test_serves_static_file_with_at_symbol_in_filename + with_static_file "/foo/foo@bar.html" do |file| + assert_html file, get("/foo/foo%40bar.html") + assert_html file, get("/foo/foo@bar.html") + end + end + + def test_serves_gzip_files_when_header_set + file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js" + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "gzip") + assert_gzip file_name, response + assert_equal "application/javascript", response.headers["Content-Type"] + assert_equal "Accept-Encoding", response.headers["Vary"] + assert_equal "gzip", response.headers["Content-Encoding"] + + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "Gzip") + assert_gzip file_name, response + + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "GZIP") + assert_gzip file_name, response + + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "compress;q=0.5, gzip;q=1.0") + assert_gzip file_name, response + + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "") + assert_not_equal "gzip", response.headers["Content-Encoding"] + end + + def test_does_not_modify_path_info + file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js" + env = { "PATH_INFO" => file_name, "HTTP_ACCEPT_ENCODING" => "gzip", "REQUEST_METHOD" => "POST" } + @app.call(env) + assert_equal file_name, env["PATH_INFO"] + end + + def test_serves_gzip_with_proper_content_type_fallback + file_name = "/gzip/foo.zoo" + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "gzip") + assert_gzip file_name, response + + default_response = get(file_name) # no gzip + assert_equal default_response.headers["Content-Type"], response.headers["Content-Type"] + end + + def test_serves_gzip_files_with_not_modified + file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js" + last_modified = File.mtime(File.join(@root, "#{file_name}.gz")) + response = get(file_name, "HTTP_ACCEPT_ENCODING" => "gzip", "HTTP_IF_MODIFIED_SINCE" => last_modified.httpdate) + assert_equal 304, response.status + assert_nil response.headers["Content-Type"] + assert_nil response.headers["Content-Encoding"] + assert_nil response.headers["Vary"] + end + + def test_serves_files_with_headers + headers = { + "Access-Control-Allow-Origin" => "http://rubyonrails.org", + "Cache-Control" => "public, max-age=60", + "X-Custom-Header" => "I'm a teapot" + } + + app = ActionDispatch::Static.new(DummyApp, @root, headers: headers) + response = Rack::MockRequest.new(app).request("GET", "/foo/bar.html") + + assert_equal "http://rubyonrails.org", response.headers["Access-Control-Allow-Origin"] + assert_equal "public, max-age=60", response.headers["Cache-Control"] + assert_equal "I'm a teapot", response.headers["X-Custom-Header"] + end + + def test_ignores_unknown_http_methods + app = ActionDispatch::Static.new(DummyApp, @root) + + assert_nothing_raised { Rack::MockRequest.new(app).request("BAD_METHOD", "/foo/bar.html") } + end + + # Windows doesn't allow \ / : * ? " < > | in filenames + unless Gem.win_platform? + def test_serves_static_file_with_colon + with_static_file "/foo/foo:bar.html" do |file| + assert_html file, get("/foo/foo%3Abar.html") + assert_html file, get("/foo/foo:bar.html") + end + end + + def test_serves_static_file_with_asterisk + with_static_file "/foo/foo*bar.html" do |file| + assert_html file, get("/foo/foo%2Abar.html") + assert_html file, get("/foo/foo*bar.html") + end + end + end + + private + + def assert_gzip(file_name, response) + expected = File.read("#{FIXTURE_LOAD_PATH}/#{public_path}" + file_name) + actual = ActiveSupport::Gzip.decompress(response.body) + assert_equal expected, actual + end + + def assert_html(body, response) + assert_equal body, response.body + assert_equal "text/html", response.headers["Content-Type"] + assert_nil response.headers["Vary"] + end + + def get(path, headers = {}) + Rack::MockRequest.new(@app).request("GET", path, headers) + end + + def with_static_file(file) + path = "#{FIXTURE_LOAD_PATH}/#{public_path}" + file + begin + File.open(path, "wb+") { |f| f.write(file) } + rescue Errno::EPROTO + skip "Couldn't create a file #{path}" + end + + yield file + ensure + File.delete(path) if File.exist? path + end +end + +class StaticTest < ActiveSupport::TestCase + def setup + super + @root = "#{FIXTURE_LOAD_PATH}/public" + @app = ActionDispatch::Static.new(DummyApp, @root, headers: { "Cache-Control" => "public, max-age=60" }) + end + + def public_path + "public" + end + + include StaticTests + + def test_custom_handler_called_when_file_is_outside_root + filename = "shared.html.erb" + assert File.exist?(File.join(@root, "..", filename)) + env = { + "REQUEST_METHOD" => "GET", + "REQUEST_PATH" => "/..%2F#{filename}", + "PATH_INFO" => "/..%2F#{filename}", + "REQUEST_URI" => "/..%2F#{filename}", + "HTTP_VERSION" => "HTTP/1.1", + "SERVER_NAME" => "localhost", + "SERVER_PORT" => "8080", + "QUERY_STRING" => "" + } + assert_equal(DummyApp.call(nil), @app.call(env)) + end + + def test_non_default_static_index + @app = ActionDispatch::Static.new(DummyApp, @root, index: "other-index") + assert_html "/other-index.html", get("/other-index.html") + assert_html "/other-index.html", get("/other-index") + assert_html "/other-index.html", get("/") + assert_html "/other-index.html", get("") + assert_html "/foo/other-index.html", get("/foo/other-index.html") + assert_html "/foo/other-index.html", get("/foo/other-index") + assert_html "/foo/other-index.html", get("/foo/") + assert_html "/foo/other-index.html", get("/foo") + end +end + +class StaticEncodingTest < StaticTest + def setup + super + @root = "#{FIXTURE_LOAD_PATH}/公共" + @app = ActionDispatch::Static.new(DummyApp, @root, headers: { "Cache-Control" => "public, max-age=60" }) + end + + def public_path + "公共" + end +end diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb new file mode 100644 index 0000000000..a824ee0c84 --- /dev/null +++ b/actionpack/test/dispatch/system_testing/driver_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_dispatch/system_testing/driver" + +class DriverTest < ActiveSupport::TestCase + test "initializing the driver" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium) + assert_equal :selenium, driver.instance_variable_get(:@name) + end + + test "initializing the driver with a browser" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) + assert_equal :selenium, driver.instance_variable_get(:@name) + assert_equal :chrome, driver.instance_variable_get(:@browser).name + assert_nil driver.instance_variable_get(:@browser).options + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) + end + + test "initializing the driver with a headless chrome" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) + assert_equal :selenium, driver.instance_variable_get(:@name) + assert_equal :headless_chrome, driver.instance_variable_get(:@browser).name + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) + end + + test "initializing the driver with a headless firefox" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_firefox, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) + assert_equal :selenium, driver.instance_variable_get(:@name) + assert_equal :headless_firefox, driver.instance_variable_get(:@browser).name + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) + end + + test "initializing the driver with a poltergeist" do + driver = ActionDispatch::SystemTesting::Driver.new(:poltergeist, screen_size: [1400, 1400], options: { js_errors: false }) + assert_equal :poltergeist, driver.instance_variable_get(:@name) + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ js_errors: false }), driver.instance_variable_get(:@options) + end + + test "initializing the driver with a webkit" do + driver = ActionDispatch::SystemTesting::Driver.new(:webkit, screen_size: [1400, 1400], options: { skip_image_loading: true }) + assert_equal :webkit, driver.instance_variable_get(:@name) + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ skip_image_loading: true }), driver.instance_variable_get(:@options) + end + + test "registerable? returns false if driver is rack_test" do + assert_not ActionDispatch::SystemTesting::Driver.new(:rack_test).send(:registerable?) + end +end diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb new file mode 100644 index 0000000000..097ef8af29 --- /dev/null +++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_dispatch/system_testing/test_helpers/screenshot_helper" +require "capybara/dsl" + +class ScreenshotHelperTest < ActiveSupport::TestCase + test "image path is saved in tmp directory" do + new_test = DrivenBySeleniumWithChrome.new("x") + + Rails.stub :root, Pathname.getwd do + assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path) + end + end + + test "image path includes failures text if test did not pass" do + new_test = DrivenBySeleniumWithChrome.new("x") + + Rails.stub :root, Pathname.getwd do + new_test.stub :passed?, false do + assert_equal Rails.root.join("tmp/screenshots/failures_x.png").to_s, new_test.send(:image_path) + end + end + end + + test "image path does not include failures text if test skipped" do + new_test = DrivenBySeleniumWithChrome.new("x") + + Rails.stub :root, Pathname.getwd do + new_test.stub :passed?, false do + new_test.stub :skipped?, true do + assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path) + end + end + end + end + + test "defaults to simple output for the screenshot" do + new_test = DrivenBySeleniumWithChrome.new("x") + assert_equal "simple", new_test.send(:output_type) + end + + test "display_image return artifact format when specify RAILS_SYSTEM_TESTING_SCREENSHOT environment" do + original_output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] + ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] = "artifact" + + new_test = DrivenBySeleniumWithChrome.new("x") + + assert_equal "artifact", new_test.send(:output_type) + + Rails.stub :root, Pathname.getwd do + new_test.stub :passed?, false do + assert_match %r|url=artifact://.+?tmp/screenshots/failures_x\.png|, new_test.send(:display_image) + end + end + ensure + ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] = original_output_type + end + + test "image path returns the absolute path from root" do + new_test = DrivenBySeleniumWithChrome.new("x") + + Rails.stub :root, Pathname.getwd.join("..") do + assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path) + end + end +end + +class RackTestScreenshotsTest < DrivenByRackTest + test "rack_test driver does not support screenshot" do + assert_not self.send(:supports_screenshot?) + end +end + +class SeleniumScreenshotsTest < DrivenBySeleniumWithChrome + test "selenium driver supports screenshot" do + assert self.send(:supports_screenshot?) + end +end diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb new file mode 100644 index 0000000000..740e90a4da --- /dev/null +++ b/actionpack/test/dispatch/system_testing/server_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "capybara/dsl" +require "action_dispatch/system_testing/server" + +class ServerTest < ActiveSupport::TestCase + setup do + @old_capybara_server = Capybara.server + end + + test "port is always included" do + ActionDispatch::SystemTesting::Server.new.run + assert Capybara.always_include_port, "expected Capybara.always_include_port to be true" + end + + test "server is changed from `default` to `puma`" do + Capybara.server = :default + ActionDispatch::SystemTesting::Server.new.run + assert_not_equal Capybara.server, Capybara.servers[:default] + end + + test "server is not changed to `puma` when is different than default" do + Capybara.server = :webrick + ActionDispatch::SystemTesting::Server.new.run + assert_equal Capybara.server, Capybara.servers[:webrick] + end + + teardown do + Capybara.server = @old_capybara_server + end +end diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb new file mode 100644 index 0000000000..b078a5abc5 --- /dev/null +++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class SetDriverToRackTestTest < DrivenByRackTest + test "uses rack_test" do + assert_equal :rack_test, Capybara.current_driver + end +end + +class OverrideSeleniumSubclassToRackTestTest < DrivenBySeleniumWithChrome + driven_by :rack_test + + test "uses rack_test" do + assert_equal :rack_test, Capybara.current_driver + end +end + +class SetDriverToSeleniumTest < DrivenBySeleniumWithChrome + test "uses selenium" do + assert_equal :selenium, Capybara.current_driver + end +end + +class SetDriverToSeleniumHeadlessChromeTest < DrivenBySeleniumWithHeadlessChrome + test "uses selenium headless chrome" do + assert_equal :selenium, Capybara.current_driver + end +end + +class SetDriverToSeleniumHeadlessFirefoxTest < DrivenBySeleniumWithHeadlessFirefox + test "uses selenium headless firefox" do + assert_equal :selenium, Capybara.current_driver + end +end + +class SetHostTest < DrivenByRackTest + test "sets default host" do + assert_equal "http://127.0.0.1", Capybara.app_host + end + + test "overrides host" do + host! "http://example.com" + + assert_equal "http://example.com", Capybara.app_host + end +end + +class UndefMethodsTest < DrivenBySeleniumWithChrome + test "get" do + exception = assert_raise NoMethodError do + get "http://example.com" + end + assert_equal "System tests cannot make direct requests via #get; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message + end + + test "post" do + exception = assert_raise NoMethodError do + post "http://example.com" + end + assert_equal "System tests cannot make direct requests via #post; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message + end + + test "put" do + exception = assert_raise NoMethodError do + put "http://example.com" + end + assert_equal "System tests cannot make direct requests via #put; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message + end + + test "patch" do + exception = assert_raise NoMethodError do + patch "http://example.com" + end + assert_equal "System tests cannot make direct requests via #patch; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message + end + + test "delete" do + exception = assert_raise NoMethodError do + delete "http://example.com" + end + assert_equal "System tests cannot make direct requests via #delete; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information.", exception.message + end +end diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb new file mode 100644 index 0000000000..e56537d80b --- /dev/null +++ b/actionpack/test/dispatch/test_request_test.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class TestRequestTest < ActiveSupport::TestCase + test "sane defaults" do + env = ActionDispatch::TestRequest.create.env + + assert_equal "GET", env.delete("REQUEST_METHOD") + assert_equal "off", env.delete("HTTPS") + assert_equal "http", env.delete("rack.url_scheme") + assert_equal "example.org", env.delete("SERVER_NAME") + assert_equal "80", env.delete("SERVER_PORT") + assert_equal "/", env.delete("PATH_INFO") + assert_equal "", env.delete("SCRIPT_NAME") + assert_equal "", env.delete("QUERY_STRING") + assert_equal "0", env.delete("CONTENT_LENGTH") + + assert_equal "test.host", env.delete("HTTP_HOST") + assert_equal "0.0.0.0", env.delete("REMOTE_ADDR") + assert_equal "Rails Testing", env.delete("HTTP_USER_AGENT") + + assert_equal [1, 3], env.delete("rack.version") + assert_equal "", env.delete("rack.input").string + assert_kind_of StringIO, env.delete("rack.errors") + assert_equal true, env.delete("rack.multithread") + assert_equal true, env.delete("rack.multiprocess") + assert_equal false, env.delete("rack.run_once") + end + + test "cookie jar" do + req = ActionDispatch::TestRequest.create({}) + + assert_equal({}, req.cookies) + assert_nil req.env["HTTP_COOKIE"] + + req.cookie_jar["user_name"] = "david" + assert_cookies({ "user_name" => "david" }, req.cookie_jar) + + req.cookie_jar["login"] = "XJ-122" + assert_cookies({ "user_name" => "david", "login" => "XJ-122" }, req.cookie_jar) + + assert_nothing_raised do + req.cookie_jar["login"] = nil + assert_cookies({ "user_name" => "david", "login" => nil }, req.cookie_jar) + end + + req.cookie_jar.delete(:login) + assert_cookies({ "user_name" => "david" }, req.cookie_jar) + + req.cookie_jar.clear + assert_cookies({}, req.cookie_jar) + + req.cookie_jar.update(user_name: "david") + assert_cookies({ "user_name" => "david" }, req.cookie_jar) + end + + test "does not complain when there is no application config" do + req = ActionDispatch::TestRequest.create({}) + assert_equal false, req.env.empty? + end + + test "default remote address is 0.0.0.0" do + req = ActionDispatch::TestRequest.create({}) + assert_equal "0.0.0.0", req.remote_addr + end + + test "allows remote address to be overridden" do + req = ActionDispatch::TestRequest.create("REMOTE_ADDR" => "127.0.0.1") + assert_equal "127.0.0.1", req.remote_addr + end + + test "default host is test.host" do + req = ActionDispatch::TestRequest.create({}) + assert_equal "test.host", req.host + end + + test "allows host to be overridden" do + req = ActionDispatch::TestRequest.create("HTTP_HOST" => "www.example.com") + assert_equal "www.example.com", req.host + end + + test "default user agent is 'Rails Testing'" do + req = ActionDispatch::TestRequest.create({}) + assert_equal "Rails Testing", req.user_agent + end + + test "allows user agent to be overridden" do + req = ActionDispatch::TestRequest.create("HTTP_USER_AGENT" => "GoogleBot") + assert_equal "GoogleBot", req.user_agent + end + + test "request_method getter and setter" do + req = ActionDispatch::TestRequest.create + req.request_method # to reproduce bug caused by memoization + req.request_method = "POST" + assert_equal "POST", req.request_method + end + + test "setter methods" do + req = ActionDispatch::TestRequest.create({}) + get = "GET" + + [ + "request_method=", "host=", "request_uri=", "path=", "if_modified_since=", "if_none_match=", + "remote_addr=", "user_agent=", "accept=" + ].each do |method| + req.send(method, get) + end + + req.port = 8080 + req.accept = "hello goodbye" + + assert_equal(get, req.get_header("REQUEST_METHOD")) + assert_equal(get, req.get_header("HTTP_HOST")) + assert_equal(8080, req.get_header("SERVER_PORT")) + assert_equal(get, req.get_header("REQUEST_URI")) + assert_equal(get, req.get_header("PATH_INFO")) + assert_equal(get, req.get_header("HTTP_IF_MODIFIED_SINCE")) + assert_equal(get, req.get_header("HTTP_IF_NONE_MATCH")) + assert_equal(get, req.get_header("REMOTE_ADDR")) + assert_equal(get, req.get_header("HTTP_USER_AGENT")) + assert_nil(req.get_header("action_dispatch.request.accepts")) + assert_equal("hello goodbye", req.get_header("HTTP_ACCEPT")) + end + + private + def assert_cookies(expected, cookie_jar) + assert_equal(expected, cookie_jar.instance_variable_get("@cookies")) + end +end diff --git a/actionpack/test/dispatch/test_response_test.rb b/actionpack/test/dispatch/test_response_test.rb new file mode 100644 index 0000000000..f0b8f7785d --- /dev/null +++ b/actionpack/test/dispatch/test_response_test.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class TestResponseTest < ActiveSupport::TestCase + def assert_response_code_range(range, predicate) + response = ActionDispatch::TestResponse.new + (0..599).each do |status| + response.status = status + assert_equal range.include?(status), response.send(predicate), + "ActionDispatch::TestResponse.new(#{status}).#{predicate}" + end + end + + test "helpers" do + assert_response_code_range 200..299, :successful? + assert_response_code_range [404], :not_found? + assert_response_code_range 300..399, :redirection? + assert_response_code_range 500..599, :server_error? + assert_response_code_range 400..499, :client_error? + end + + test "response parsing" do + response = ActionDispatch::TestResponse.create(200, {}, "") + assert_equal response.body, response.parsed_body + + response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "application/json" }, '{ "foo": "fighters" }') + assert_equal({ "foo" => "fighters" }, response.parsed_body) + end + + test "response status aliases deprecated" do + response = ActionDispatch::TestResponse.create + assert_deprecated { response.success? } + assert_deprecated { response.missing? } + assert_deprecated { response.error? } + end +end diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb new file mode 100644 index 0000000000..21169fcb5c --- /dev/null +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + class UploadedFileTest < ActiveSupport::TestCase + def test_constructor_with_argument_error + assert_raises(ArgumentError) do + Http::UploadedFile.new({}) + end + end + + def test_original_filename + uf = Http::UploadedFile.new(filename: "foo", tempfile: Object.new) + assert_equal "foo", uf.original_filename + end + + def test_filename_is_different_object + file_str = "foo" + uf = Http::UploadedFile.new(filename: file_str, tempfile: Object.new) + assert_not_equal file_str.object_id, uf.original_filename.object_id + end + + def test_filename_should_be_in_utf_8 + uf = Http::UploadedFile.new(filename: "foo", tempfile: Object.new) + assert_equal "UTF-8", uf.original_filename.encoding.to_s + end + + def test_filename_should_always_be_in_utf_8 + uf = Http::UploadedFile.new(filename: "foo".encode(Encoding::SHIFT_JIS), + tempfile: Object.new) + assert_equal "UTF-8", uf.original_filename.encoding.to_s + end + + def test_content_type + uf = Http::UploadedFile.new(type: "foo", tempfile: Object.new) + assert_equal "foo", uf.content_type + end + + def test_headers + uf = Http::UploadedFile.new(head: "foo", tempfile: Object.new) + assert_equal "foo", uf.headers + end + + def test_tempfile + uf = Http::UploadedFile.new(tempfile: "foo") + assert_equal "foo", uf.tempfile + end + + def test_to_io_returns_the_tempfile + tf = Object.new + uf = Http::UploadedFile.new(tempfile: tf) + assert_equal tf, uf.to_io + end + + def test_delegates_path_to_tempfile + tf = Class.new { def path; "thunderhorse" end } + uf = Http::UploadedFile.new(tempfile: tf.new) + assert_equal "thunderhorse", uf.path + end + + def test_delegates_open_to_tempfile + tf = Class.new { def open; "thunderhorse" end } + uf = Http::UploadedFile.new(tempfile: tf.new) + assert_equal "thunderhorse", uf.open + end + + def test_delegates_close_to_tempfile + tf = Class.new { def close(unlink_now = false); "thunderhorse" end } + uf = Http::UploadedFile.new(tempfile: tf.new) + assert_equal "thunderhorse", uf.close + end + + def test_close_accepts_parameter + tf = Class.new { def close(unlink_now = false); "thunderhorse: #{unlink_now}" end } + uf = Http::UploadedFile.new(tempfile: tf.new) + assert_equal "thunderhorse: true", uf.close(true) + end + + def test_delegates_read_to_tempfile + tf = Class.new { def read(length = nil, buffer = nil); "thunderhorse" end } + uf = Http::UploadedFile.new(tempfile: tf.new) + assert_equal "thunderhorse", uf.read + end + + def test_delegates_read_to_tempfile_with_params + tf = Class.new { def read(length = nil, buffer = nil); [length, buffer] end } + uf = Http::UploadedFile.new(tempfile: tf.new) + assert_equal %w{ thunder horse }, uf.read(*%w{ thunder horse }) + end + + def test_delegate_respects_respond_to? + tf = Class.new { def read; yield end; private :read } + uf = Http::UploadedFile.new(tempfile: tf.new) + assert_raises(NoMethodError) do + uf.read + end + end + + def test_delegate_eof_to_tempfile + tf = Class.new { def eof?; true end; } + uf = Http::UploadedFile.new(tempfile: tf.new) + assert_predicate uf, :eof? + end + + def test_delegate_to_path_to_tempfile + tf = Class.new { def to_path; "/any/file/path" end; } + uf = Http::UploadedFile.new(tempfile: tf.new) + assert_equal "/any/file/path", uf.to_path + end + + def test_respond_to? + tf = Class.new { def read; yield end } + uf = Http::UploadedFile.new(tempfile: tf.new) + assert_respond_to uf, :headers + assert_respond_to uf, :read + end + end +end diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb new file mode 100644 index 0000000000..aef9351de1 --- /dev/null +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module TestUrlGeneration + class WithMountPoint < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new + include Routes.url_helpers + + class ::MyRouteGeneratingController < ActionController::Base + include Routes.url_helpers + def index + render plain: foo_path + end + end + + Routes.draw do + get "/foo", to: "my_route_generating#index", as: :foo + + resources :bars + + mount MyRouteGeneratingController.action(:index), at: "/bar" + end + + APP = build_app Routes + + def _routes + Routes + end + + def app + APP + end + + test "generating URLS normally" do + assert_equal "/foo", foo_path + end + + test "accepting a :script_name option" do + assert_equal "/bar/foo", foo_path(script_name: "/bar") + end + + test "the request's SCRIPT_NAME takes precedence over the route" do + get "/foo", headers: { "SCRIPT_NAME" => "/new", "action_dispatch.routes" => Routes } + assert_equal "/new/foo", response.body + end + + test "the request's SCRIPT_NAME wraps the mounted app's" do + get "/new/bar/foo", headers: { "SCRIPT_NAME" => "/new", "PATH_INFO" => "/bar/foo", "action_dispatch.routes" => Routes } + assert_equal "/new/bar/foo", response.body + end + + test "handling http protocol with https set" do + https! + assert_equal "http://www.example.com/foo", foo_url(protocol: "http") + end + + test "extracting protocol from host when protocol not present" do + assert_equal "httpz://www.example.com/foo", foo_url(host: "httpz://www.example.com", protocol: nil) + end + + test "formatting host when protocol is present" do + assert_equal "http://www.example.com/foo", foo_url(host: "httpz://www.example.com", protocol: "http://") + end + + test "default ports are removed from the host" do + assert_equal "http://www.example.com/foo", foo_url(host: "www.example.com:80", protocol: "http://") + assert_equal "https://www.example.com/foo", foo_url(host: "www.example.com:443", protocol: "https://") + end + + test "port is extracted from the host" do + assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "http://") + assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "//") + assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com:80", protocol: "//") + end + + test "port option is used" do + assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com", protocol: "http://", port: 8080) + assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com", protocol: "//", port: 8080) + assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com", protocol: "//", port: 80) + end + + test "port option overrides the host" do + assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: 8080) + assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: 8080) + assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com:443", protocol: "//", port: 80) + end + + test "port option disables the host when set to nil" do + assert_equal "http://www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: nil) + assert_equal "//www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: nil) + end + + test "port option disables the host when set to false" do + assert_equal "http://www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: false) + assert_equal "//www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: false) + end + + test "keep subdomain when key is true" do + assert_equal "http://www.example.com/foo", foo_url(subdomain: true) + end + + test "keep subdomain when key is missing" do + assert_equal "http://www.example.com/foo", foo_url + end + + test "omit subdomain when key is nil" do + assert_equal "http://example.com/foo", foo_url(subdomain: nil) + end + + test "omit subdomain when key is false" do + assert_equal "http://example.com/foo", foo_url(subdomain: false) + end + + test "omit subdomain when key is blank" do + assert_equal "http://example.com/foo", foo_url(subdomain: "") + end + + test "generating URLs with trailing slashes" do + assert_equal "/bars.json", bars_path( + trailing_slash: true, + format: "json" + ) + end + + test "generating URLS with querystring and trailing slashes" do + assert_equal "/bars.json?a=b", bars_path( + trailing_slash: true, + a: "b", + format: "json" + ) + end + + test "generating URLS with empty querystring" do + assert_equal "/bars.json", bars_path( + a: {}, + format: "json" + ) + end + end +end diff --git a/actionpack/test/fixtures/_top_level_partial_only.erb b/actionpack/test/fixtures/_top_level_partial_only.erb new file mode 100644 index 0000000000..44f25b61d0 --- /dev/null +++ b/actionpack/test/fixtures/_top_level_partial_only.erb @@ -0,0 +1 @@ +top level partial
\ No newline at end of file diff --git a/actionpack/test/fixtures/alternate_helpers/foo_helper.rb b/actionpack/test/fixtures/alternate_helpers/foo_helper.rb new file mode 100644 index 0000000000..c1a995af5f --- /dev/null +++ b/actionpack/test/fixtures/alternate_helpers/foo_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module FooHelper + redefine_method(:baz) { } +end diff --git a/actionpack/test/fixtures/bad_customers/_bad_customer.html.erb b/actionpack/test/fixtures/bad_customers/_bad_customer.html.erb new file mode 100644 index 0000000000..d22af431ec --- /dev/null +++ b/actionpack/test/fixtures/bad_customers/_bad_customer.html.erb @@ -0,0 +1 @@ +<%= greeting %> bad customer: <%= bad_customer.name %><%= bad_customer_counter %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/collection_cache/index.html.erb b/actionpack/test/fixtures/collection_cache/index.html.erb new file mode 100644 index 0000000000..853e501ab4 --- /dev/null +++ b/actionpack/test/fixtures/collection_cache/index.html.erb @@ -0,0 +1 @@ +<%= render partial: 'customers/customer', collection: @customers, cached: true %> diff --git a/actionpack/test/fixtures/company.rb b/actionpack/test/fixtures/company.rb new file mode 100644 index 0000000000..93afdd5472 --- /dev/null +++ b/actionpack/test/fixtures/company.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Company < ActiveRecord::Base + has_one :mascot + self.sequence_name = :companies_nonstd_seq + + validates_presence_of :name + def validate + errors.add("rating", "rating should not be 2") if rating == 2 + end +end diff --git a/actionpack/test/fixtures/customers/_commented_customer.html.erb b/actionpack/test/fixtures/customers/_commented_customer.html.erb new file mode 100644 index 0000000000..8cc9c1ec13 --- /dev/null +++ b/actionpack/test/fixtures/customers/_commented_customer.html.erb @@ -0,0 +1,5 @@ +<%# I'm a comment %> +<% cache customer do %> + <% controller.partial_rendered_times += 1 %> + <%= customer.name %>, <%= customer.id %> +<% end %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/customers/_customer.html.erb b/actionpack/test/fixtures/customers/_customer.html.erb new file mode 100644 index 0000000000..5105090d4b --- /dev/null +++ b/actionpack/test/fixtures/customers/_customer.html.erb @@ -0,0 +1,4 @@ +<% cache customer do %> + <% controller.partial_rendered_times += 1 %> + <%= customer.name %>, <%= customer.id %> +<% end %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/filter_test/implicit_actions/edit.html.erb b/actionpack/test/fixtures/filter_test/implicit_actions/edit.html.erb new file mode 100644 index 0000000000..8491ab9f80 --- /dev/null +++ b/actionpack/test/fixtures/filter_test/implicit_actions/edit.html.erb @@ -0,0 +1 @@ +edit
\ No newline at end of file diff --git a/actionpack/test/fixtures/filter_test/implicit_actions/show.html.erb b/actionpack/test/fixtures/filter_test/implicit_actions/show.html.erb new file mode 100644 index 0000000000..0a89cecf05 --- /dev/null +++ b/actionpack/test/fixtures/filter_test/implicit_actions/show.html.erb @@ -0,0 +1 @@ +show
\ No newline at end of file diff --git a/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb b/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb new file mode 100644 index 0000000000..aad73c0d6b --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb @@ -0,0 +1 @@ +<p>Hello!</p> diff --git a/actionpack/test/fixtures/functional_caching/_partial.erb b/actionpack/test/fixtures/functional_caching/_partial.erb new file mode 100644 index 0000000000..ec0da7cf50 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/_partial.erb @@ -0,0 +1,3 @@ +<% cache do %> +Old fragment caching in a partial +<% end %> diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb new file mode 100644 index 0000000000..dfcd423978 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb @@ -0,0 +1,3 @@ +<body> +<%= cache("fragment") do %><p>ERB</p><% end %> +</body> diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder new file mode 100644 index 0000000000..6599579740 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder @@ -0,0 +1,5 @@ +xml.body do + cache("fragment") do + xml.p "Builder" + end +end diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb new file mode 100644 index 0000000000..abf7017ce6 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb @@ -0,0 +1,3 @@ +<body> +<%= cache("fragment") do %><p>PHONE</p><% end %> +</body> diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb new file mode 100644 index 0000000000..1148d83ad7 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb @@ -0,0 +1,3 @@ +Hello +<%= cache "fragment" do %>This bit's fragment cached<% end %> +<%= 'Ciao' %> diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb new file mode 100644 index 0000000000..951c761995 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb @@ -0,0 +1,3 @@ +<body> +<%= cache 'with_options', skip_digest: true, expires_in: 10 do %><p>ERB</p><% end %> +</body> diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb new file mode 100644 index 0000000000..3125583a28 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb @@ -0,0 +1,3 @@ +<body> +<%= cache 'nodigest', skip_digest: true do %><p>ERB</p><% end %> +</body> diff --git a/actionpack/test/fixtures/functional_caching/html_fragment_cached_with_partial.html.erb b/actionpack/test/fixtures/functional_caching/html_fragment_cached_with_partial.html.erb new file mode 100644 index 0000000000..a9462d3499 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/html_fragment_cached_with_partial.html.erb @@ -0,0 +1 @@ +<%= render :partial => 'partial' %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/functional_caching/inline_fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/inline_fragment_cached.html.erb new file mode 100644 index 0000000000..41647f1404 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/inline_fragment_cached.html.erb @@ -0,0 +1,2 @@ +<%= render :inline => 'Some inline content' %> +<%= cache do %>Some cached content<% end %> diff --git a/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder b/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder new file mode 100644 index 0000000000..2bdda3af18 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder @@ -0,0 +1,5 @@ +cache do + xml.title "Hello!" +end + +xml.body cdata_section(render("formatted_partial")) diff --git a/actionpack/test/fixtures/helpers/abc_helper.rb b/actionpack/test/fixtures/helpers/abc_helper.rb new file mode 100644 index 0000000000..999b9b5c6e --- /dev/null +++ b/actionpack/test/fixtures/helpers/abc_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module AbcHelper + def bare_a() end +end diff --git a/actionpack/test/fixtures/helpers/fun/games_helper.rb b/actionpack/test/fixtures/helpers/fun/games_helper.rb new file mode 100644 index 0000000000..8b325927f3 --- /dev/null +++ b/actionpack/test/fixtures/helpers/fun/games_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Fun + module GamesHelper + def stratego() "Iz guuut!" end + end +end diff --git a/actionpack/test/fixtures/helpers/fun/pdf_helper.rb b/actionpack/test/fixtures/helpers/fun/pdf_helper.rb new file mode 100644 index 0000000000..7ce6591de3 --- /dev/null +++ b/actionpack/test/fixtures/helpers/fun/pdf_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Fun + module PdfHelper + def foobar() "baz" end + end +end diff --git a/actionpack/test/fixtures/helpers/just_me_helper.rb b/actionpack/test/fixtures/helpers/just_me_helper.rb new file mode 100644 index 0000000000..bd977a22d9 --- /dev/null +++ b/actionpack/test/fixtures/helpers/just_me_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module JustMeHelper + def me() "mine!" end +end diff --git a/actionpack/test/fixtures/helpers/me_too_helper.rb b/actionpack/test/fixtures/helpers/me_too_helper.rb new file mode 100644 index 0000000000..c6fc053dee --- /dev/null +++ b/actionpack/test/fixtures/helpers/me_too_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module MeTooHelper + def me() "me too!" end +end diff --git a/actionpack/test/fixtures/helpers1_pack/pack1_helper.rb b/actionpack/test/fixtures/helpers1_pack/pack1_helper.rb new file mode 100644 index 0000000000..cf75b6875e --- /dev/null +++ b/actionpack/test/fixtures/helpers1_pack/pack1_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Pack1Helper + def conflicting_helper + "pack1" + end +end diff --git a/actionpack/test/fixtures/helpers2_pack/pack2_helper.rb b/actionpack/test/fixtures/helpers2_pack/pack2_helper.rb new file mode 100644 index 0000000000..c8e51d40a2 --- /dev/null +++ b/actionpack/test/fixtures/helpers2_pack/pack2_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Pack2Helper + def conflicting_helper + "pack2" + end +end diff --git a/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb b/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb new file mode 100644 index 0000000000..0455e26b93 --- /dev/null +++ b/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Admin + module UsersHelpeR + end +end diff --git a/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb b/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb new file mode 100644 index 0000000000..ded99ba52d --- /dev/null +++ b/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb @@ -0,0 +1 @@ +mobile diff --git a/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb b/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb new file mode 100644 index 0000000000..dd294f8cf6 --- /dev/null +++ b/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb @@ -0,0 +1 @@ +<h1>Empty action rendered this implicitly.</h1> diff --git a/actionpack/test/fixtures/layouts/_customers.erb b/actionpack/test/fixtures/layouts/_customers.erb new file mode 100644 index 0000000000..ae63f13cd3 --- /dev/null +++ b/actionpack/test/fixtures/layouts/_customers.erb @@ -0,0 +1 @@ +<title><%= yield Struct.new(:name).new("David") %></title>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layouts/block_with_layout.erb b/actionpack/test/fixtures/layouts/block_with_layout.erb new file mode 100644 index 0000000000..73ac833e52 --- /dev/null +++ b/actionpack/test/fixtures/layouts/block_with_layout.erb @@ -0,0 +1,3 @@ +<%= render(:layout => "layout_for_partial", :locals => { :name => "Anthony" }) do %>Inside from first block in layout<% "Return value should be discarded" %><% end %> +<%= yield %> +<%= render(:layout => "layout_for_partial", :locals => { :name => "Ramm" }) do %>Inside from second block in layout<% end %> diff --git a/actionpack/test/fixtures/layouts/builder.builder b/actionpack/test/fixtures/layouts/builder.builder new file mode 100644 index 0000000000..c55488edd0 --- /dev/null +++ b/actionpack/test/fixtures/layouts/builder.builder @@ -0,0 +1,3 @@ +xml.wrapper do + xml << yield +end diff --git a/actionpack/test/fixtures/layouts/partial_with_layout.erb b/actionpack/test/fixtures/layouts/partial_with_layout.erb new file mode 100644 index 0000000000..a0349d731e --- /dev/null +++ b/actionpack/test/fixtures/layouts/partial_with_layout.erb @@ -0,0 +1,3 @@ +<%= render( :layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => {:name => 'Anthony' } ) %> +<%= yield %> +<%= render( :layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => {:name => 'Ramm' } ) %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layouts/standard.html.erb b/actionpack/test/fixtures/layouts/standard.html.erb new file mode 100644 index 0000000000..48882dca35 --- /dev/null +++ b/actionpack/test/fixtures/layouts/standard.html.erb @@ -0,0 +1 @@ +<html><%= yield %><%= @variable_for_layout %></html> diff --git a/actionpack/test/fixtures/layouts/talk_from_action.erb b/actionpack/test/fixtures/layouts/talk_from_action.erb new file mode 100644 index 0000000000..bf53fdb785 --- /dev/null +++ b/actionpack/test/fixtures/layouts/talk_from_action.erb @@ -0,0 +1,2 @@ +<title><%= @title || yield(:title) %></title> +<%= yield -%>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layouts/with_html_partial.html.erb b/actionpack/test/fixtures/layouts/with_html_partial.html.erb new file mode 100644 index 0000000000..fd2896aeaa --- /dev/null +++ b/actionpack/test/fixtures/layouts/with_html_partial.html.erb @@ -0,0 +1 @@ +<%= render :partial => "partial_only_html" %><%= yield %> diff --git a/actionpack/test/fixtures/layouts/xhr.html.erb b/actionpack/test/fixtures/layouts/xhr.html.erb new file mode 100644 index 0000000000..85285324ec --- /dev/null +++ b/actionpack/test/fixtures/layouts/xhr.html.erb @@ -0,0 +1,2 @@ +XHR! +<%= yield %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/layouts/yield.erb b/actionpack/test/fixtures/layouts/yield.erb new file mode 100644 index 0000000000..482dc9022e --- /dev/null +++ b/actionpack/test/fixtures/layouts/yield.erb @@ -0,0 +1,2 @@ +<title><%= yield :title %></title> +<%= yield %> diff --git a/actionpack/test/fixtures/load_me.rb b/actionpack/test/fixtures/load_me.rb new file mode 100644 index 0000000000..efafe6898f --- /dev/null +++ b/actionpack/test/fixtures/load_me.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class LoadMe +end diff --git a/actionpack/test/fixtures/localized/hello_world.de.html b/actionpack/test/fixtures/localized/hello_world.de.html new file mode 100644 index 0000000000..a8fc612c60 --- /dev/null +++ b/actionpack/test/fixtures/localized/hello_world.de.html @@ -0,0 +1 @@ +Guten Tag
\ No newline at end of file diff --git a/actionpack/test/fixtures/localized/hello_world.en.html b/actionpack/test/fixtures/localized/hello_world.en.html new file mode 100644 index 0000000000..5e1c309dae --- /dev/null +++ b/actionpack/test/fixtures/localized/hello_world.en.html @@ -0,0 +1 @@ +Hello World
\ No newline at end of file diff --git a/actionpack/test/fixtures/localized/hello_world.it.erb b/actionpack/test/fixtures/localized/hello_world.it.erb new file mode 100644 index 0000000000..9191fdc187 --- /dev/null +++ b/actionpack/test/fixtures/localized/hello_world.it.erb @@ -0,0 +1 @@ +Ciao Mondo
\ No newline at end of file diff --git a/actionpack/test/fixtures/multipart/binary_file b/actionpack/test/fixtures/multipart/binary_file Binary files differnew file mode 100644 index 0000000000..556187ac1f --- /dev/null +++ b/actionpack/test/fixtures/multipart/binary_file diff --git a/actionpack/test/fixtures/multipart/boundary_problem_file b/actionpack/test/fixtures/multipart/boundary_problem_file new file mode 100644 index 0000000000..889c4aabe3 --- /dev/null +++ b/actionpack/test/fixtures/multipart/boundary_problem_file @@ -0,0 +1,10 @@ +--AaB03x
+Content-Disposition: form-data; name="file"; filename="file.txt"
+Content-Type: text/plain
+
+bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/bracketed_param b/actionpack/test/fixtures/multipart/bracketed_param new file mode 100644 index 0000000000..096bd8a192 --- /dev/null +++ b/actionpack/test/fixtures/multipart/bracketed_param @@ -0,0 +1,5 @@ +--AaB03x
+Content-Disposition: form-data; name="foo[baz]"
+
+bar
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/bracketed_utf8_param b/actionpack/test/fixtures/multipart/bracketed_utf8_param new file mode 100644 index 0000000000..976ca44a45 --- /dev/null +++ b/actionpack/test/fixtures/multipart/bracketed_utf8_param @@ -0,0 +1,5 @@ +--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name[Iñtërnâtiônàlizætiøn_nested_name]"
+
+Iñtërnâtiônàlizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/empty b/actionpack/test/fixtures/multipart/empty new file mode 100644 index 0000000000..f0f79835c9 --- /dev/null +++ b/actionpack/test/fixtures/multipart/empty @@ -0,0 +1,10 @@ +--AaB03x
+Content-Disposition: form-data; name="submit-name"
+
+Larry
+--AaB03x
+Content-Disposition: form-data; name="files"; filename="file1.txt"
+Content-Type: text/plain
+
+
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/hello.txt b/actionpack/test/fixtures/multipart/hello.txt new file mode 100644 index 0000000000..5ab2f8a432 --- /dev/null +++ b/actionpack/test/fixtures/multipart/hello.txt @@ -0,0 +1 @@ +Hello
\ No newline at end of file diff --git a/actionpack/test/fixtures/multipart/large_text_file b/actionpack/test/fixtures/multipart/large_text_file new file mode 100644 index 0000000000..7f97fb1d79 --- /dev/null +++ b/actionpack/test/fixtures/multipart/large_text_file @@ -0,0 +1,10 @@ +--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="file"; filename="file.txt"
+Content-Type: text/plain
+
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/mixed_files b/actionpack/test/fixtures/multipart/mixed_files Binary files differnew file mode 100644 index 0000000000..5eba7a6b48 --- /dev/null +++ b/actionpack/test/fixtures/multipart/mixed_files diff --git a/actionpack/test/fixtures/multipart/none b/actionpack/test/fixtures/multipart/none new file mode 100644 index 0000000000..d66f4730f1 --- /dev/null +++ b/actionpack/test/fixtures/multipart/none @@ -0,0 +1,9 @@ +--AaB03x
+Content-Disposition: form-data; name="submit-name"
+
+Larry
+--AaB03x
+Content-Disposition: form-data; name="files"; filename=""
+
+
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/ruby_on_rails.jpg b/actionpack/test/fixtures/multipart/ruby_on_rails.jpg Binary files differnew file mode 100644 index 0000000000..ed284ea0ba --- /dev/null +++ b/actionpack/test/fixtures/multipart/ruby_on_rails.jpg diff --git a/actionpack/test/fixtures/multipart/single_parameter b/actionpack/test/fixtures/multipart/single_parameter new file mode 100644 index 0000000000..8962c35430 --- /dev/null +++ b/actionpack/test/fixtures/multipart/single_parameter @@ -0,0 +1,5 @@ +--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/single_utf8_param b/actionpack/test/fixtures/multipart/single_utf8_param new file mode 100644 index 0000000000..b86f62d1e1 --- /dev/null +++ b/actionpack/test/fixtures/multipart/single_utf8_param @@ -0,0 +1,5 @@ +--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name"
+
+Iñtërnâtiônàlizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/text_file b/actionpack/test/fixtures/multipart/text_file new file mode 100644 index 0000000000..e0367d68c0 --- /dev/null +++ b/actionpack/test/fixtures/multipart/text_file @@ -0,0 +1,10 @@ +--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="file"; filename="file.txt"
+Content-Type: text/plain
+
+contents
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/utf8_filename b/actionpack/test/fixtures/multipart/utf8_filename new file mode 100644 index 0000000000..60738d53b0 --- /dev/null +++ b/actionpack/test/fixtures/multipart/utf8_filename @@ -0,0 +1,10 @@ +--AaB03x
+Content-Disposition: form-data; name="foo"
+
+bar
+--AaB03x
+Content-Disposition: form-data; name="file"; filename="ファイル%名.txt"
+Content-Type: text/plain
+
+contents
+--AaB03x--
diff --git a/actionpack/test/fixtures/namespaced/implicit_render_test/hello_world.erb b/actionpack/test/fixtures/namespaced/implicit_render_test/hello_world.erb new file mode 100644 index 0000000000..cd0875583a --- /dev/null +++ b/actionpack/test/fixtures/namespaced/implicit_render_test/hello_world.erb @@ -0,0 +1 @@ +Hello world! diff --git a/actionpack/test/fixtures/old_content_type/render_default_content_types_for_respond_to.xml.erb b/actionpack/test/fixtures/old_content_type/render_default_content_types_for_respond_to.xml.erb new file mode 100644 index 0000000000..25dc746886 --- /dev/null +++ b/actionpack/test/fixtures/old_content_type/render_default_content_types_for_respond_to.xml.erb @@ -0,0 +1 @@ +<hello>world</hello>
\ No newline at end of file diff --git a/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder b/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder new file mode 100644 index 0000000000..15c8a7f5cf --- /dev/null +++ b/actionpack/test/fixtures/old_content_type/render_default_for_builder.builder @@ -0,0 +1 @@ +xml.p "Hello world!" diff --git a/actionpack/test/fixtures/old_content_type/render_default_for_erb.erb b/actionpack/test/fixtures/old_content_type/render_default_for_erb.erb new file mode 100644 index 0000000000..c7926d48bb --- /dev/null +++ b/actionpack/test/fixtures/old_content_type/render_default_for_erb.erb @@ -0,0 +1 @@ +<%= 'hello world!' %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/post_test/layouts/post.html.erb b/actionpack/test/fixtures/post_test/layouts/post.html.erb new file mode 100644 index 0000000000..c6c1a586dd --- /dev/null +++ b/actionpack/test/fixtures/post_test/layouts/post.html.erb @@ -0,0 +1 @@ +<html><div id="html"><%= yield %></div></html>
\ No newline at end of file diff --git a/actionpack/test/fixtures/post_test/layouts/super_post.iphone.erb b/actionpack/test/fixtures/post_test/layouts/super_post.iphone.erb new file mode 100644 index 0000000000..db0e43694d --- /dev/null +++ b/actionpack/test/fixtures/post_test/layouts/super_post.iphone.erb @@ -0,0 +1 @@ +<html><div id="super_iphone"><%= yield %></div></html>
\ No newline at end of file diff --git a/actionpack/test/fixtures/post_test/post/index.html.erb b/actionpack/test/fixtures/post_test/post/index.html.erb new file mode 100644 index 0000000000..b349b25618 --- /dev/null +++ b/actionpack/test/fixtures/post_test/post/index.html.erb @@ -0,0 +1 @@ +Hello Firefox
\ No newline at end of file diff --git a/actionpack/test/fixtures/post_test/post/index.iphone.erb b/actionpack/test/fixtures/post_test/post/index.iphone.erb new file mode 100644 index 0000000000..d741e44351 --- /dev/null +++ b/actionpack/test/fixtures/post_test/post/index.iphone.erb @@ -0,0 +1 @@ +Hello iPhone
\ No newline at end of file diff --git a/actionpack/test/fixtures/post_test/super_post/index.html.erb b/actionpack/test/fixtures/post_test/super_post/index.html.erb new file mode 100644 index 0000000000..7fc2eb190a --- /dev/null +++ b/actionpack/test/fixtures/post_test/super_post/index.html.erb @@ -0,0 +1 @@ +Super Firefox
\ No newline at end of file diff --git a/actionpack/test/fixtures/post_test/super_post/index.iphone.erb b/actionpack/test/fixtures/post_test/super_post/index.iphone.erb new file mode 100644 index 0000000000..99063a8d8c --- /dev/null +++ b/actionpack/test/fixtures/post_test/super_post/index.iphone.erb @@ -0,0 +1 @@ +Super iPhone
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/400.html b/actionpack/test/fixtures/public/400.html new file mode 100644 index 0000000000..03be6bedaf --- /dev/null +++ b/actionpack/test/fixtures/public/400.html @@ -0,0 +1 @@ +400 error fixture diff --git a/actionpack/test/fixtures/public/404.html b/actionpack/test/fixtures/public/404.html new file mode 100644 index 0000000000..497397ccea --- /dev/null +++ b/actionpack/test/fixtures/public/404.html @@ -0,0 +1 @@ +404 error fixture diff --git a/actionpack/test/fixtures/public/500.da.html b/actionpack/test/fixtures/public/500.da.html new file mode 100644 index 0000000000..a497c13656 --- /dev/null +++ b/actionpack/test/fixtures/public/500.da.html @@ -0,0 +1 @@ +500 localized error fixture diff --git a/actionpack/test/fixtures/public/500.html b/actionpack/test/fixtures/public/500.html new file mode 100644 index 0000000000..7c66c7a943 --- /dev/null +++ b/actionpack/test/fixtures/public/500.html @@ -0,0 +1 @@ +500 error fixture diff --git a/actionpack/test/fixtures/public/bar.html b/actionpack/test/fixtures/public/bar.html new file mode 100644 index 0000000000..67fc57079b --- /dev/null +++ b/actionpack/test/fixtures/public/bar.html @@ -0,0 +1 @@ +/bar.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/bar/index.html b/actionpack/test/fixtures/public/bar/index.html new file mode 100644 index 0000000000..d5bb8f898d --- /dev/null +++ b/actionpack/test/fixtures/public/bar/index.html @@ -0,0 +1 @@ +/bar/index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/foo/bar.html b/actionpack/test/fixtures/public/foo/bar.html new file mode 100644 index 0000000000..9a35646205 --- /dev/null +++ b/actionpack/test/fixtures/public/foo/bar.html @@ -0,0 +1 @@ +/foo/bar.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/foo/baz.css b/actionpack/test/fixtures/public/foo/baz.css new file mode 100644 index 0000000000..b5173fbef2 --- /dev/null +++ b/actionpack/test/fixtures/public/foo/baz.css @@ -0,0 +1,3 @@ +body { +background: #000; +} diff --git a/actionpack/test/fixtures/public/foo/index.html b/actionpack/test/fixtures/public/foo/index.html new file mode 100644 index 0000000000..497a2e898f --- /dev/null +++ b/actionpack/test/fixtures/public/foo/index.html @@ -0,0 +1 @@ +/foo/index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/foo/other-index.html b/actionpack/test/fixtures/public/foo/other-index.html new file mode 100644 index 0000000000..51c90c26ea --- /dev/null +++ b/actionpack/test/fixtures/public/foo/other-index.html @@ -0,0 +1 @@ +/foo/other-index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/foo/こんにちは.html b/actionpack/test/fixtures/public/foo/こんにちは.html new file mode 100644 index 0000000000..1df9166522 --- /dev/null +++ b/actionpack/test/fixtures/public/foo/こんにちは.html @@ -0,0 +1 @@ +means hello in Japanese diff --git a/actionpack/test/fixtures/public/foo/さようなら.html b/actionpack/test/fixtures/public/foo/さようなら.html new file mode 100644 index 0000000000..627bb2469f --- /dev/null +++ b/actionpack/test/fixtures/public/foo/さようなら.html @@ -0,0 +1 @@ +means goodbye in Japanese diff --git a/actionpack/test/fixtures/public/foo/さようなら.html.gz b/actionpack/test/fixtures/public/foo/さようなら.html.gz Binary files differnew file mode 100644 index 0000000000..4f484cfe86 --- /dev/null +++ b/actionpack/test/fixtures/public/foo/さようなら.html.gz diff --git a/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js b/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js new file mode 100644 index 0000000000..1826a7660e --- /dev/null +++ b/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js @@ -0,0 +1,4 @@ +!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){function n(e){var t=e.length,n=it.type(e);return"function"===n||it.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e}function r(e,t,n){if(it.isFunction(t))return it.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return it.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ft.test(t))return it.filter(t,e,n);t=it.filter(t,e)}return it.grep(e,function(e){return it.inArray(e,t)>=0!==n})}function i(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function o(e){var t=xt[e]={};return it.each(e.match(bt)||[],function(e,n){t[n]=!0}),t}function a(){ht.addEventListener?(ht.removeEventListener("DOMContentLoaded",s,!1),e.removeEventListener("load",s,!1)):(ht.detachEvent("onreadystatechange",s),e.detachEvent("onload",s))}function s(){(ht.addEventListener||"load"===event.type||"complete"===ht.readyState)&&(a(),it.ready())}function l(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(Ct,"-$1").toLowerCase();if(n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:kt.test(n)?it.parseJSON(n):n}catch(i){}it.data(e,t,n)}else n=void 0}return n}function u(e){var t;for(t in e)if(("data"!==t||!it.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function c(e,t,n,r){if(it.acceptData(e)){var i,o,a=it.expando,s=e.nodeType,l=s?it.cache:e,u=s?e[a]:e[a]&&a;if(u&&l[u]&&(r||l[u].data)||void 0!==n||"string"!=typeof t)return u||(u=s?e[a]=G.pop()||it.guid++:a),l[u]||(l[u]=s?{}:{toJSON:it.noop}),("object"==typeof t||"function"==typeof t)&&(r?l[u]=it.extend(l[u],t):l[u].data=it.extend(l[u].data,t)),o=l[u],r||(o.data||(o.data={}),o=o.data),void 0!==n&&(o[it.camelCase(t)]=n),"string"==typeof t?(i=o[t],null==i&&(i=o[it.camelCase(t)])):i=o,i}}function d(e,t,n){if(it.acceptData(e)){var r,i,o=e.nodeType,a=o?it.cache:e,s=o?e[it.expando]:it.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){it.isArray(t)?t=t.concat(it.map(t,it.camelCase)):t in r?t=[t]:(t=it.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;for(;i--;)delete r[t[i]];if(n?!u(r):!it.isEmptyObject(r))return}(n||(delete a[s].data,u(a[s])))&&(o?it.cleanData([e],!0):nt.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}function f(){return!0}function p(){return!1}function h(){try{return ht.activeElement}catch(e){}}function m(e){var t=Mt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function g(e,t){var n,r,i=0,o=typeof e.getElementsByTagName!==Et?e.getElementsByTagName(t||"*"):typeof e.querySelectorAll!==Et?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||it.nodeName(r,t)?o.push(r):it.merge(o,g(r,t));return void 0===t||t&&it.nodeName(e,t)?it.merge([e],o):o}function v(e){Dt.test(e.type)&&(e.defaultChecked=e.checked)}function y(e,t){return it.nodeName(e,"table")&&it.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function b(e){return e.type=(null!==it.find.attr(e,"type"))+"/"+e.type,e}function x(e){var t=Vt.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function w(e,t){for(var n,r=0;null!=(n=e[r]);r++)it._data(n,"globalEval",!t||it._data(t[r],"globalEval"))}function T(e,t){if(1===t.nodeType&&it.hasData(e)){var n,r,i,o=it._data(e),a=it._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)it.event.add(t,n,s[n][r])}a.data&&(a.data=it.extend({},a.data))}}function E(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!nt.noCloneEvent&&t[it.expando]){i=it._data(t);for(r in i.events)it.removeEvent(t,r,i.handle);t.removeAttribute(it.expando)}"script"===n&&t.text!==e.text?(b(t).text=e.text,x(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),nt.html5Clone&&e.innerHTML&&!it.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Dt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function k(t,n){var r,i=it(n.createElement(t)).appendTo(n.body),o=e.getDefaultComputedStyle&&(r=e.getDefaultComputedStyle(i[0]))?r.display:it.css(i[0],"display");return i.detach(),o}function C(e){var t=ht,n=Zt[e];return n||(n=k(e,t),"none"!==n&&n||(Qt=(Qt||it("<iframe frameborder='0' width='0' height='0'/>")).appendTo(t.documentElement),t=(Qt[0].contentWindow||Qt[0].contentDocument).document,t.write(),t.close(),n=k(e,t),Qt.detach()),Zt[e]=n),n}function N(e,t){return{get:function(){var n=e();if(null!=n)return n?void delete this.get:(this.get=t).apply(this,arguments)}}}function S(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=pn.length;i--;)if(t=pn[i]+n,t in e)return t;return r}function j(e,t){for(var n,r,i,o=[],a=0,s=e.length;s>a;a++)r=e[a],r.style&&(o[a]=it._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&jt(r)&&(o[a]=it._data(r,"olddisplay",C(r.nodeName)))):(i=jt(r),(n&&"none"!==n||!i)&&it._data(r,"olddisplay",i?n:it.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}function A(e,t,n){var r=un.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function D(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=it.css(e,n+St[o],!0,i)),r?("content"===n&&(a-=it.css(e,"padding"+St[o],!0,i)),"margin"!==n&&(a-=it.css(e,"border"+St[o]+"Width",!0,i))):(a+=it.css(e,"padding"+St[o],!0,i),"padding"!==n&&(a+=it.css(e,"border"+St[o]+"Width",!0,i)));return a}function L(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=en(e),a=nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=tn(e,t,o),(0>i||null==i)&&(i=e.style[t]),rn.test(i))return i;r=a&&(nt.boxSizingReliable()||i===e.style[t]),i=parseFloat(i)||0}return i+D(e,t,n||(a?"border":"content"),r,o)+"px"}function H(e,t,n,r,i){return new H.prototype.init(e,t,n,r,i)}function _(){return setTimeout(function(){hn=void 0}),hn=it.now()}function q(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=St[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function F(e,t,n){for(var r,i=(xn[t]||[]).concat(xn["*"]),o=0,a=i.length;a>o;o++)if(r=i[o].call(n,t,e))return r}function M(e,t,n){var r,i,o,a,s,l,u,c,d=this,f={},p=e.style,h=e.nodeType&&jt(e),m=it._data(e,"fxshow");n.queue||(s=it._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,d.always(function(){d.always(function(){s.unqueued--,it.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],u=it.css(e,"display"),c="none"===u?it._data(e,"olddisplay")||C(e.nodeName):u,"inline"===c&&"none"===it.css(e,"float")&&(nt.inlineBlockNeedsLayout&&"inline"!==C(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",nt.shrinkWrapBlocks()||d.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],gn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(h?"hide":"show")){if("show"!==i||!m||void 0===m[r])continue;h=!0}f[r]=m&&m[r]||it.style(e,r)}else u=void 0;if(it.isEmptyObject(f))"inline"===("none"===u?C(e.nodeName):u)&&(p.display=u);else{m?"hidden"in m&&(h=m.hidden):m=it._data(e,"fxshow",{}),o&&(m.hidden=!h),h?it(e).show():d.done(function(){it(e).hide()}),d.done(function(){var t;it._removeData(e,"fxshow");for(t in f)it.style(e,t,f[t])});for(r in f)a=F(h?m[r]:0,r,d),r in m||(m[r]=a.start,h&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function O(e,t){var n,r,i,o,a;for(n in e)if(r=it.camelCase(n),i=t[r],o=e[n],it.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=it.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function R(e,t,n){var r,i,o=0,a=bn.length,s=it.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;for(var t=hn||_(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:it.extend({},t),opts:it.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:hn||_(),duration:n.duration,tweens:[],createTween:function(t,n){var r=it.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(O(c,u.opts.specialEasing);a>o;o++)if(r=bn[o].call(u,e,c,u.opts))return r;return it.map(c,F,u),it.isFunction(u.opts.start)&&u.opts.start.call(e,u),it.fx.timer(it.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function B(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(bt)||[];if(it.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function P(e,t,n,r){function i(s){var l;return o[s]=!0,it.each(e[s]||[],function(e,s){var u=s(t,n,r);return"string"!=typeof u||a||o[u]?a?!(l=u):void 0:(t.dataTypes.unshift(u),i(u),!1)}),l}var o={},a=e===zn;return i(t.dataTypes[0])||!o["*"]&&i("*")}function I(e,t){var n,r,i=it.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&it.extend(!0,e,n),e}function W(e,t,n){for(var r,i,o,a,s=e.contents,l=e.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){l.unshift(a);break}if(l[0]in n)o=l[0];else{for(a in n){if(!l[0]||e.converters[a+" "+l[0]]){o=a;break}r||(r=a)}o=o||r}return o?(o!==l[0]&&l.unshift(o),n[o]):void 0}function $(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(d){return{state:"parsererror",error:a?d:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}function z(e,t,n,r){var i;if(it.isArray(t))it.each(t,function(t,i){n||Gn.test(e)?r(e,i):z(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==it.type(t))r(e,t);else for(i in t)z(e+"["+i+"]",t[i],n,r)}function X(){try{return new e.XMLHttpRequest}catch(t){}}function U(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function V(e){return it.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}var G=[],Y=G.slice,J=G.concat,K=G.push,Q=G.indexOf,Z={},et=Z.toString,tt=Z.hasOwnProperty,nt={},rt="1.11.1",it=function(e,t){return new it.fn.init(e,t)},ot=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,at=/^-ms-/,st=/-([\da-z])/gi,lt=function(e,t){return t.toUpperCase()};it.fn=it.prototype={jquery:rt,constructor:it,selector:"",length:0,toArray:function(){return Y.call(this)},get:function(e){return null!=e?0>e?this[e+this.length]:this[e]:Y.call(this)},pushStack:function(e){var t=it.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return it.each(this,e,t)},map:function(e){return this.pushStack(it.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(Y.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:K,sort:G.sort,splice:G.splice},it.extend=it.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,l=arguments.length,u=!1;for("boolean"==typeof a&&(u=a,a=arguments[s]||{},s++),"object"==typeof a||it.isFunction(a)||(a={}),s===l&&(a=this,s--);l>s;s++)if(null!=(i=arguments[s]))for(r in i)e=a[r],n=i[r],a!==n&&(u&&n&&(it.isPlainObject(n)||(t=it.isArray(n)))?(t?(t=!1,o=e&&it.isArray(e)?e:[]):o=e&&it.isPlainObject(e)?e:{},a[r]=it.extend(u,o,n)):void 0!==n&&(a[r]=n));return a},it.extend({expando:"jQuery"+(rt+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isFunction:function(e){return"function"===it.type(e)},isArray:Array.isArray||function(e){return"array"===it.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!it.isArray(e)&&e-parseFloat(e)>=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==it.type(e)||e.nodeType||it.isWindow(e))return!1;try{if(e.constructor&&!tt.call(e,"constructor")&&!tt.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}if(nt.ownLast)for(t in e)return tt.call(e,t);for(t in e);return void 0===t||tt.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?Z[et.call(e)]||"object":typeof e},globalEval:function(t){t&&it.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(at,"ms-").replace(st,lt)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,r){var i,o=0,a=e.length,s=n(e);if(r){if(s)for(;a>o&&(i=t.apply(e[o],r),i!==!1);o++);else for(o in e)if(i=t.apply(e[o],r),i===!1)break}else if(s)for(;a>o&&(i=t.call(e[o],o,e[o]),i!==!1);o++);else for(o in e)if(i=t.call(e[o],o,e[o]),i===!1)break;return e},trim:function(e){return null==e?"":(e+"").replace(ot,"")},makeArray:function(e,t){var r=t||[];return null!=e&&(n(Object(e))?it.merge(r,"string"==typeof e?[e]:e):K.call(r,e)),r},inArray:function(e,t,n){var r;if(t){if(Q)return Q.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;n>r;)e[i++]=t[r++];if(n!==n)for(;void 0!==t[r];)e[i++]=t[r++];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;a>o;o++)r=!t(e[o],o),r!==s&&i.push(e[o]);return i},map:function(e,t,r){var i,o=0,a=e.length,s=n(e),l=[];if(s)for(;a>o;o++)i=t(e[o],o,r),null!=i&&l.push(i);else for(o in e)i=t(e[o],o,r),null!=i&&l.push(i);return J.apply([],l)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(i=e[t],t=e,e=i),it.isFunction(e)?(n=Y.call(arguments,2),r=function(){return e.apply(t||this,n.concat(Y.call(arguments)))},r.guid=e.guid=e.guid||it.guid++,r):void 0},now:function(){return+new Date},support:nt}),it.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){Z["[object "+t+"]"]=t.toLowerCase()});var ut=function(e){function t(e,t,n,r){var i,o,a,s,l,u,d,p,h,m;if((t?t.ownerDocument||t:P)!==H&&L(t),t=t||H,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(q&&!r){if(i=yt.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&R(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return Z.apply(n,t.getElementsByTagName(e)),n;if((a=i[3])&&w.getElementsByClassName&&t.getElementsByClassName)return Z.apply(n,t.getElementsByClassName(a)),n}if(w.qsa&&(!F||!F.test(e))){if(p=d=B,h=t,m=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){for(u=C(e),(d=t.getAttribute("id"))?p=d.replace(xt,"\\$&"):t.setAttribute("id",p),p="[id='"+p+"'] ",l=u.length;l--;)u[l]=p+f(u[l]);h=bt.test(e)&&c(t.parentNode)||t,m=u.join(",")}if(m)try{return Z.apply(n,h.querySelectorAll(m)),n}catch(g){}finally{d||t.removeAttribute("id")}}}return S(e.replace(lt,"$1"),t,n,r)}function n(){function e(n,r){return t.push(n+" ")>T.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[B]=!0,e}function i(e){var t=H.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=e.length;r--;)T.attrHandle[n[r]]=t}function a(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||G)-(~e.sourceIndex||G);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function s(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function l(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function u(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function c(e){return e&&typeof e.getElementsByTagName!==V&&e}function d(){}function f(e){for(var t=0,n=e.length,r="";n>t;t++)r+=e[t].value;return r}function p(e,t,n){var r=t.dir,i=n&&"parentNode"===r,o=W++;return t.first?function(t,n,o){for(;t=t[r];)if(1===t.nodeType||i)return e(t,n,o)}:function(t,n,a){var s,l,u=[I,o];if(a){for(;t=t[r];)if((1===t.nodeType||i)&&e(t,n,a))return!0}else for(;t=t[r];)if(1===t.nodeType||i){if(l=t[B]||(t[B]={}),(s=l[r])&&s[0]===I&&s[1]===o)return u[2]=s[2];if(l[r]=u,u[2]=e(t,n,a))return!0}}}function h(e){return e.length>1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function m(e,n,r){for(var i=0,o=n.length;o>i;i++)t(e,n[i],r);return r}function g(e,t,n,r,i){for(var o,a=[],s=0,l=e.length,u=null!=t;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function v(e,t,n,i,o,a){return i&&!i[B]&&(i=v(i)),o&&!o[B]&&(o=v(o,a)),r(function(r,a,s,l){var u,c,d,f=[],p=[],h=a.length,v=r||m(t||"*",s.nodeType?[s]:s,[]),y=!e||!r&&t?v:g(v,f,e,s,l),b=n?o||(r?e:h||i)?[]:a:y;if(n&&n(y,b,s,l),i)for(u=g(b,p),i(u,[],s,l),c=u.length;c--;)(d=u[c])&&(b[p[c]]=!(y[p[c]]=d));if(r){if(o||e){if(o){for(u=[],c=b.length;c--;)(d=b[c])&&u.push(y[c]=d);o(null,b=[],u,l)}for(c=b.length;c--;)(d=b[c])&&(u=o?tt.call(r,d):f[c])>-1&&(r[u]=!(a[u]=d))}}else b=g(b===a?b.splice(h,b.length):b),o?o(null,a,b,l):Z.apply(a,b)})}function y(e){for(var t,n,r,i=e.length,o=T.relative[e[0].type],a=o||T.relative[" "],s=o?1:0,l=p(function(e){return e===t},a,!0),u=p(function(e){return tt.call(t,e)>-1},a,!0),c=[function(e,n,r){return!o&&(r||n!==j)||((t=n).nodeType?l(e,n,r):u(e,n,r))}];i>s;s++)if(n=T.relative[e[s].type])c=[p(h(c),n)];else{if(n=T.filter[e[s].type].apply(null,e[s].matches),n[B]){for(r=++s;i>r&&!T.relative[e[r].type];r++);return v(s>1&&h(c),s>1&&f(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(lt,"$1"),n,r>s&&y(e.slice(s,r)),i>r&&y(e=e.slice(r)),i>r&&f(e))}c.push(n)}return h(c)}function b(e,n){var i=n.length>0,o=e.length>0,a=function(r,a,s,l,u){var c,d,f,p=0,h="0",m=r&&[],v=[],y=j,b=r||o&&T.find.TAG("*",u),x=I+=null==y?1:Math.random()||.1,w=b.length;for(u&&(j=a!==H&&a);h!==w&&null!=(c=b[h]);h++){if(o&&c){for(d=0;f=e[d++];)if(f(c,a,s)){l.push(c);break}u&&(I=x)}i&&((c=!f&&c)&&p--,r&&m.push(c))}if(p+=h,i&&h!==p){for(d=0;f=n[d++];)f(m,v,a,s);if(r){if(p>0)for(;h--;)m[h]||v[h]||(v[h]=K.call(l));v=g(v)}Z.apply(l,v),u&&!r&&v.length>0&&p+n.length>1&&t.uniqueSort(l)}return u&&(I=x,j=y),m};return i?r(a):a}var x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B="sizzle"+-new Date,P=e.document,I=0,W=0,$=n(),z=n(),X=n(),U=function(e,t){return e===t&&(D=!0),0},V="undefined",G=1<<31,Y={}.hasOwnProperty,J=[],K=J.pop,Q=J.push,Z=J.push,et=J.slice,tt=J.indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(this[t]===e)return t;return-1},nt="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",rt="[\\x20\\t\\r\\n\\f]",it="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",ot=it.replace("w","w#"),at="\\["+rt+"*("+it+")(?:"+rt+"*([*^$|!~]?=)"+rt+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+ot+"))|)"+rt+"*\\]",st=":("+it+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+at+")*)|.*)\\)|)",lt=new RegExp("^"+rt+"+|((?:^|[^\\\\])(?:\\\\.)*)"+rt+"+$","g"),ut=new RegExp("^"+rt+"*,"+rt+"*"),ct=new RegExp("^"+rt+"*([>+~]|"+rt+")"+rt+"*"),dt=new RegExp("="+rt+"*([^\\]'\"]*?)"+rt+"*\\]","g"),ft=new RegExp(st),pt=new RegExp("^"+ot+"$"),ht={ID:new RegExp("^#("+it+")"),CLASS:new RegExp("^\\.("+it+")"),TAG:new RegExp("^("+it.replace("w","w*")+")"),ATTR:new RegExp("^"+at),PSEUDO:new RegExp("^"+st),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+rt+"*(even|odd|(([+-]|)(\\d*)n|)"+rt+"*(?:([+-]|)"+rt+"*(\\d+)|))"+rt+"*\\)|)","i"),bool:new RegExp("^(?:"+nt+")$","i"),needsContext:new RegExp("^"+rt+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+rt+"*((?:-\\d)?\\d*)"+rt+"*\\)|)(?=[^-]|$)","i")},mt=/^(?:input|select|textarea|button)$/i,gt=/^h\d$/i,vt=/^[^{]+\{\s*\[native \w/,yt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,bt=/[+~]/,xt=/'|\\/g,wt=new RegExp("\\\\([\\da-f]{1,6}"+rt+"?|("+rt+")|.)","ig"),Tt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{Z.apply(J=et.call(P.childNodes),P.childNodes),J[P.childNodes.length].nodeType}catch(Et){Z={apply:J.length?function(e,t){Q.apply(e,et.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}w=t.support={},k=t.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},L=t.setDocument=function(e){var t,n=e?e.ownerDocument||e:P,r=n.defaultView;return n!==H&&9===n.nodeType&&n.documentElement?(H=n,_=n.documentElement,q=!k(n),r&&r!==r.top&&(r.addEventListener?r.addEventListener("unload",function(){L()},!1):r.attachEvent&&r.attachEvent("onunload",function(){L()})),w.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),w.getElementsByTagName=i(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),w.getElementsByClassName=vt.test(n.getElementsByClassName)&&i(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),w.getById=i(function(e){return _.appendChild(e).id=B,!n.getElementsByName||!n.getElementsByName(B).length}),w.getById?(T.find.ID=function(e,t){if(typeof t.getElementById!==V&&q){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){return e.getAttribute("id")===t}}):(delete T.find.ID,T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){var n=typeof e.getAttributeNode!==V&&e.getAttributeNode("id");return n&&n.value===t}}),T.find.TAG=w.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==V?t.getElementsByTagName(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},T.find.CLASS=w.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==V&&q?t.getElementsByClassName(e):void 0},M=[],F=[],(w.qsa=vt.test(n.querySelectorAll))&&(i(function(e){e.innerHTML="<select msallowclip=''><option selected=''></option></select>",e.querySelectorAll("[msallowclip^='']").length&&F.push("[*^$]="+rt+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||F.push("\\["+rt+"*(?:value|"+nt+")"),e.querySelectorAll(":checked").length||F.push(":checked")}),i(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&F.push("name"+rt+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||F.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),F.push(",.*:")})),(w.matchesSelector=vt.test(O=_.matches||_.webkitMatchesSelector||_.mozMatchesSelector||_.oMatchesSelector||_.msMatchesSelector))&&i(function(e){w.disconnectedMatch=O.call(e,"div"),O.call(e,"[s!='']:x"),M.push("!=",st)}),F=F.length&&new RegExp(F.join("|")),M=M.length&&new RegExp(M.join("|")),t=vt.test(_.compareDocumentPosition),R=t||vt.test(_.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},U=t?function(e,t){if(e===t)return D=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r?r:(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&r||!w.sortDetached&&t.compareDocumentPosition(e)===r?e===n||e.ownerDocument===P&&R(P,e)?-1:t===n||t.ownerDocument===P&&R(P,t)?1:A?tt.call(A,e)-tt.call(A,t):0:4&r?-1:1)}:function(e,t){if(e===t)return D=!0,0;var r,i=0,o=e.parentNode,s=t.parentNode,l=[e],u=[t];if(!o||!s)return e===n?-1:t===n?1:o?-1:s?1:A?tt.call(A,e)-tt.call(A,t):0;if(o===s)return a(e,t);for(r=e;r=r.parentNode;)l.unshift(r);for(r=t;r=r.parentNode;)u.unshift(r);for(;l[i]===u[i];)i++;return i?a(l[i],u[i]):l[i]===P?-1:u[i]===P?1:0},n):H},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==H&&L(e),n=n.replace(dt,"='$1']"),!(!w.matchesSelector||!q||M&&M.test(n)||F&&F.test(n)))try{var r=O.call(e,n);if(r||w.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return t(n,H,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==H&&L(e),R(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==H&&L(e);var n=T.attrHandle[t.toLowerCase()],r=n&&Y.call(T.attrHandle,t.toLowerCase())?n(e,t,!q):void 0;return void 0!==r?r:w.attributes||!q?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(D=!w.detectDuplicates,A=!w.sortStable&&e.slice(0),e.sort(U),D){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return A=null,e},E=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=E(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=E(t);return n},T=t.selectors={cacheLength:50,createPseudo:r,match:ht,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(wt,Tt),e[3]=(e[3]||e[4]||e[5]||"").replace(wt,Tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return ht.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&ft.test(n)&&(t=C(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(wt,Tt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=$[e+" "];return t||(t=new RegExp("(^|"+rt+")"+e+"("+rt+"|$)"))&&$(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==V&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:n?(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o+" ").indexOf(r)>-1:"|="===n?o===r||o.slice(0,r.length+1)===r+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,d,f,p,h,m=o!==a?"nextSibling":"previousSibling",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!l&&!s;if(g){if(o){for(;m;){for(d=t;d=d[m];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=m="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?g.firstChild:g.lastChild],a&&y){for(c=g[B]||(g[B]={}),u=c[e]||[],p=u[0]===I&&u[1],f=u[0]===I&&u[2],d=p&&g.childNodes[p];d=++p&&d&&d[m]||(f=p=0)||h.pop();)if(1===d.nodeType&&++f&&d===t){c[e]=[I,p,f];break}}else if(y&&(u=(t[B]||(t[B]={}))[e])&&u[0]===I)f=u[1];else for(;(d=++p&&d&&d[m]||(f=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++f||(y&&((d[B]||(d[B]={}))[e]=[I,f]),d!==t)););return f-=i,f===r||f%r===0&&f/r>=0}}},PSEUDO:function(e,n){var i,o=T.pseudos[e]||T.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[B]?o(n):o.length>1?(i=[e,e,"",n],T.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),a=i.length;a--;)r=tt.call(e,i[a]),e[r]=!(t[r]=i[a])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=N(e.replace(lt,"$1"));return i[B]?r(function(e,t,n,r){for(var o,a=i(e,null,r,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return function(t){return(t.textContent||t.innerText||E(t)).indexOf(e)>-1}}),lang:r(function(e){return pt.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(wt,Tt).toLowerCase(),function(t){var n;do if(n=q?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===_},focus:function(e){return e===H.activeElement&&(!H.hasFocus||H.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.pseudos.empty(e)},header:function(e){return gt.test(e.nodeName)},input:function(e){return mt.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:u(function(){return[0]}),last:u(function(e,t){return[t-1]}),eq:u(function(e,t,n){return[0>n?n+t:n]}),even:u(function(e,t){for(var n=0;t>n;n+=2)e.push(n);return e}),odd:u(function(e,t){for(var n=1;t>n;n+=2)e.push(n);return e}),lt:u(function(e,t,n){for(var r=0>n?n+t:n;--r>=0;)e.push(r);return e}),gt:u(function(e,t,n){for(var r=0>n?n+t:n;++r<t;)e.push(r);return e})}},T.pseudos.nth=T.pseudos.eq;for(x in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})T.pseudos[x]=s(x);for(x in{submit:!0,reset:!0})T.pseudos[x]=l(x);return d.prototype=T.filters=T.pseudos,T.setFilters=new d,C=t.tokenize=function(e,n){var r,i,o,a,s,l,u,c=z[e+" "];if(c)return n?0:c.slice(0);for(s=e,l=[],u=T.preFilter;s;){(!r||(i=ut.exec(s)))&&(i&&(s=s.slice(i[0].length)||s),l.push(o=[])),r=!1,(i=ct.exec(s))&&(r=i.shift(),o.push({value:r,type:i[0].replace(lt," ")}),s=s.slice(r.length));for(a in T.filter)!(i=ht[a].exec(s))||u[a]&&!(i=u[a](i))||(r=i.shift(),o.push({value:r,type:a,matches:i}),s=s.slice(r.length));if(!r)break}return n?s.length:s?t.error(e):z(e,l).slice(0)},N=t.compile=function(e,t){var n,r=[],i=[],o=X[e+" "];if(!o){for(t||(t=C(e)),n=t.length;n--;)o=y(t[n]),o[B]?r.push(o):i.push(o);o=X(e,b(i,r)),o.selector=e}return o},S=t.select=function(e,t,n,r){var i,o,a,s,l,u="function"==typeof e&&e,d=!r&&C(e=u.selector||e);if(n=n||[],1===d.length){if(o=d[0]=d[0].slice(0),o.length>2&&"ID"===(a=o[0]).type&&w.getById&&9===t.nodeType&&q&&T.relative[o[1].type]){if(t=(T.find.ID(a.matches[0].replace(wt,Tt),t)||[])[0],!t)return n;u&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=ht.needsContext.test(e)?0:o.length;i--&&(a=o[i],!T.relative[s=a.type]);)if((l=T.find[s])&&(r=l(a.matches[0].replace(wt,Tt),bt.test(o[0].type)&&c(t.parentNode)||t))){if(o.splice(i,1),e=r.length&&f(o),!e)return Z.apply(n,r),n; +break}}return(u||N(e,d))(r,t,!q,n,bt.test(e)&&c(t.parentNode)||t),n},w.sortStable=B.split("").sort(U).join("")===B,w.detectDuplicates=!!D,L(),w.sortDetached=i(function(e){return 1&e.compareDocumentPosition(H.createElement("div"))}),i(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){return n?void 0:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),w.attributes&&i(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?void 0:e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(nt,function(e,t,n){var r;return n?void 0:e[t]===!0?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(e);it.find=ut,it.expr=ut.selectors,it.expr[":"]=it.expr.pseudos,it.unique=ut.uniqueSort,it.text=ut.getText,it.isXMLDoc=ut.isXML,it.contains=ut.contains;var ct=it.expr.match.needsContext,dt=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,ft=/^.[^:#\[\.,]*$/;it.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?it.find.matchesSelector(r,e)?[r]:[]:it.find.matches(e,it.grep(t,function(e){return 1===e.nodeType}))},it.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(it(e).filter(function(){for(t=0;i>t;t++)if(it.contains(r[t],this))return!0}));for(t=0;i>t;t++)it.find(e,r[t],n);return n=this.pushStack(i>1?it.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(r(this,e||[],!1))},not:function(e){return this.pushStack(r(this,e||[],!0))},is:function(e){return!!r(this,"string"==typeof e&&ct.test(e)?it(e):e||[],!1).length}});var pt,ht=e.document,mt=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,gt=it.fn.init=function(e,t){var n,r;if(!e)return this;if("string"==typeof e){if(n="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:mt.exec(e),!n||!n[1]&&t)return!t||t.jquery?(t||pt).find(e):this.constructor(t).find(e);if(n[1]){if(t=t instanceof it?t[0]:t,it.merge(this,it.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:ht,!0)),dt.test(n[1])&&it.isPlainObject(t))for(n in t)it.isFunction(this[n])?this[n](t[n]):this.attr(n,t[n]);return this}if(r=ht.getElementById(n[2]),r&&r.parentNode){if(r.id!==n[2])return pt.find(e);this.length=1,this[0]=r}return this.context=ht,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):it.isFunction(e)?"undefined"!=typeof pt.ready?pt.ready(e):e(it):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),it.makeArray(e,this))};gt.prototype=it.fn,pt=it(ht);var vt=/^(?:parents|prev(?:Until|All))/,yt={children:!0,contents:!0,next:!0,prev:!0};it.extend({dir:function(e,t,n){for(var r=[],i=e[t];i&&9!==i.nodeType&&(void 0===n||1!==i.nodeType||!it(i).is(n));)1===i.nodeType&&r.push(i),i=i[t];return r},sibling:function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}}),it.fn.extend({has:function(e){var t,n=it(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(it.contains(this,n[t]))return!0})},closest:function(e,t){for(var n,r=0,i=this.length,o=[],a=ct.test(e)||"string"!=typeof e?it(e,t||this.context):0;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&it.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?it.unique(o):o)},index:function(e){return e?"string"==typeof e?it.inArray(this[0],it(e)):it.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(it.unique(it.merge(this.get(),it(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),it.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return it.dir(e,"parentNode")},parentsUntil:function(e,t,n){return it.dir(e,"parentNode",n)},next:function(e){return i(e,"nextSibling")},prev:function(e){return i(e,"previousSibling")},nextAll:function(e){return it.dir(e,"nextSibling")},prevAll:function(e){return it.dir(e,"previousSibling")},nextUntil:function(e,t,n){return it.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return it.dir(e,"previousSibling",n)},siblings:function(e){return it.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return it.sibling(e.firstChild)},contents:function(e){return it.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:it.merge([],e.childNodes)}},function(e,t){it.fn[e]=function(n,r){var i=it.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=it.filter(r,i)),this.length>1&&(yt[e]||(i=it.unique(i)),vt.test(e)&&(i=i.reverse())),this.pushStack(i)}});var bt=/\S+/g,xt={};it.Callbacks=function(e){e="string"==typeof e?xt[e]||o(e):it.extend({},e);var t,n,r,i,a,s,l=[],u=!e.once&&[],c=function(o){for(n=e.memory&&o,r=!0,a=s||0,s=0,i=l.length,t=!0;l&&i>a;a++)if(l[a].apply(o[0],o[1])===!1&&e.stopOnFalse){n=!1;break}t=!1,l&&(u?u.length&&c(u.shift()):n?l=[]:d.disable())},d={add:function(){if(l){var r=l.length;!function o(t){it.each(t,function(t,n){var r=it.type(n);"function"===r?e.unique&&d.has(n)||l.push(n):n&&n.length&&"string"!==r&&o(n)})}(arguments),t?i=l.length:n&&(s=r,c(n))}return this},remove:function(){return l&&it.each(arguments,function(e,n){for(var r;(r=it.inArray(n,l,r))>-1;)l.splice(r,1),t&&(i>=r&&i--,a>=r&&a--)}),this},has:function(e){return e?it.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],i=0,this},disable:function(){return l=u=n=void 0,this},disabled:function(){return!l},lock:function(){return u=void 0,n||d.disable(),this},locked:function(){return!u},fireWith:function(e,n){return!l||r&&!u||(n=n||[],n=[e,n.slice?n.slice():n],t?u.push(n):c(n)),this},fire:function(){return d.fireWith(this,arguments),this},fired:function(){return!!r}};return d},it.extend({Deferred:function(e){var t=[["resolve","done",it.Callbacks("once memory"),"resolved"],["reject","fail",it.Callbacks("once memory"),"rejected"],["notify","progress",it.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return it.Deferred(function(n){it.each(t,function(t,o){var a=it.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&it.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?it.extend(e,r):r}},i={};return r.pipe=r.then,it.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,i=0,o=Y.call(arguments),a=o.length,s=1!==a||e&&it.isFunction(e.promise)?a:0,l=1===s?e:it.Deferred(),u=function(e,n,r){return function(i){n[e]=this,r[e]=arguments.length>1?Y.call(arguments):i,r===t?l.notifyWith(n,r):--s||l.resolveWith(n,r)}};if(a>1)for(t=new Array(a),n=new Array(a),r=new Array(a);a>i;i++)o[i]&&it.isFunction(o[i].promise)?o[i].promise().done(u(i,r,o)).fail(l.reject).progress(u(i,n,t)):--s;return s||l.resolveWith(r,o),l.promise()}});var wt;it.fn.ready=function(e){return it.ready.promise().done(e),this},it.extend({isReady:!1,readyWait:1,holdReady:function(e){e?it.readyWait++:it.ready(!0)},ready:function(e){if(e===!0?!--it.readyWait:!it.isReady){if(!ht.body)return setTimeout(it.ready);it.isReady=!0,e!==!0&&--it.readyWait>0||(wt.resolveWith(ht,[it]),it.fn.triggerHandler&&(it(ht).triggerHandler("ready"),it(ht).off("ready")))}}}),it.ready.promise=function(t){if(!wt)if(wt=it.Deferred(),"complete"===ht.readyState)setTimeout(it.ready);else if(ht.addEventListener)ht.addEventListener("DOMContentLoaded",s,!1),e.addEventListener("load",s,!1);else{ht.attachEvent("onreadystatechange",s),e.attachEvent("onload",s);var n=!1;try{n=null==e.frameElement&&ht.documentElement}catch(r){}n&&n.doScroll&&!function i(){if(!it.isReady){try{n.doScroll("left")}catch(e){return setTimeout(i,50)}a(),it.ready()}}()}return wt.promise(t)};var Tt,Et="undefined";for(Tt in it(nt))break;nt.ownLast="0"!==Tt,nt.inlineBlockNeedsLayout=!1,it(function(){var e,t,n,r;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",nt.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(r))}),function(){var e=ht.createElement("div");if(null==nt.deleteExpando){nt.deleteExpando=!0;try{delete e.test}catch(t){nt.deleteExpando=!1}}e=null}(),it.acceptData=function(e){var t=it.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return 1!==n&&9!==n?!1:!t||t!==!0&&e.getAttribute("classid")===t};var kt=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Ct=/([A-Z])/g;it.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?it.cache[e[it.expando]]:e[it.expando],!!e&&!u(e)},data:function(e,t,n){return c(e,t,n)},removeData:function(e,t){return d(e,t)},_data:function(e,t,n){return c(e,t,n,!0)},_removeData:function(e,t){return d(e,t,!0)}}),it.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=it.data(o),1===o.nodeType&&!it._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&(r=a[n].name,0===r.indexOf("data-")&&(r=it.camelCase(r.slice(5)),l(o,r,i[r])));it._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){it.data(this,e)}):arguments.length>1?this.each(function(){it.data(this,e,t)}):o?l(o,e,it.data(o,e)):void 0},removeData:function(e){return this.each(function(){it.removeData(this,e)})}}),it.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=it._data(e,t),n&&(!r||it.isArray(n)?r=it._data(e,t,it.makeArray(n)):r.push(n)),r||[]):void 0},dequeue:function(e,t){t=t||"fx";var n=it.queue(e,t),r=n.length,i=n.shift(),o=it._queueHooks(e,t),a=function(){it.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return it._data(e,n)||it._data(e,n,{empty:it.Callbacks("once memory").add(function(){it._removeData(e,t+"queue"),it._removeData(e,n)})})}}),it.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?it.queue(this[0],e):void 0===t?this:this.each(function(){var n=it.queue(this,e,t);it._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&it.dequeue(this,e)})},dequeue:function(e){return this.each(function(){it.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=it.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};for("string"!=typeof e&&(t=e,e=void 0),e=e||"fx";a--;)n=it._data(o[a],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var Nt=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,St=["Top","Right","Bottom","Left"],jt=function(e,t){return e=t||e,"none"===it.css(e,"display")||!it.contains(e.ownerDocument,e)},At=it.access=function(e,t,n,r,i,o,a){var s=0,l=e.length,u=null==n;if("object"===it.type(n)){i=!0;for(s in n)it.access(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,it.isFunction(r)||(a=!0),u&&(a?(t.call(e,r),t=null):(u=t,t=function(e,t,n){return u.call(it(e),n)})),t))for(;l>s;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:u?t.call(e):l?t(e[0],n):o},Dt=/^(?:checkbox|radio)$/i;!function(){var e=ht.createElement("input"),t=ht.createElement("div"),n=ht.createDocumentFragment();if(t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",nt.leadingWhitespace=3===t.firstChild.nodeType,nt.tbody=!t.getElementsByTagName("tbody").length,nt.htmlSerialize=!!t.getElementsByTagName("link").length,nt.html5Clone="<:nav></:nav>"!==ht.createElement("nav").cloneNode(!0).outerHTML,e.type="checkbox",e.checked=!0,n.appendChild(e),nt.appendChecked=e.checked,t.innerHTML="<textarea>x</textarea>",nt.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,n.appendChild(t),t.innerHTML="<input type='radio' checked='checked' name='t'/>",nt.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,nt.noCloneEvent=!0,t.attachEvent&&(t.attachEvent("onclick",function(){nt.noCloneEvent=!1}),t.cloneNode(!0).click()),null==nt.deleteExpando){nt.deleteExpando=!0;try{delete t.test}catch(r){nt.deleteExpando=!1}}}(),function(){var t,n,r=ht.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(nt[t+"Bubbles"]=n in e)||(r.setAttribute(n,"t"),nt[t+"Bubbles"]=r.attributes[n].expando===!1);r=null}();var Lt=/^(?:input|select|textarea)$/i,Ht=/^key/,_t=/^(?:mouse|pointer|contextmenu)|click/,qt=/^(?:focusinfocus|focusoutblur)$/,Ft=/^([^.]*)(?:\.(.+)|)$/;it.event={global:{},add:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it._data(e);if(g){for(n.handler&&(l=n,n=l.handler,i=l.selector),n.guid||(n.guid=it.guid++),(a=g.events)||(a=g.events={}),(c=g.handle)||(c=g.handle=function(e){return typeof it===Et||e&&it.event.triggered===e.type?void 0:it.event.dispatch.apply(c.elem,arguments)},c.elem=e),t=(t||"").match(bt)||[""],s=t.length;s--;)o=Ft.exec(t[s])||[],p=m=o[1],h=(o[2]||"").split(".").sort(),p&&(u=it.event.special[p]||{},p=(i?u.delegateType:u.bindType)||p,u=it.event.special[p]||{},d=it.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&it.expr.match.needsContext.test(i),namespace:h.join(".")},l),(f=a[p])||(f=a[p]=[],f.delegateCount=0,u.setup&&u.setup.call(e,r,h,c)!==!1||(e.addEventListener?e.addEventListener(p,c,!1):e.attachEvent&&e.attachEvent("on"+p,c))),u.add&&(u.add.call(e,d),d.handler.guid||(d.handler.guid=n.guid)),i?f.splice(f.delegateCount++,0,d):f.push(d),it.event.global[p]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it.hasData(e)&&it._data(e);if(g&&(c=g.events)){for(t=(t||"").match(bt)||[""],u=t.length;u--;)if(s=Ft.exec(t[u])||[],p=m=s[1],h=(s[2]||"").split(".").sort(),p){for(d=it.event.special[p]||{},p=(r?d.delegateType:d.bindType)||p,f=c[p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;o--;)a=f[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,d.remove&&d.remove.call(e,a));l&&!f.length&&(d.teardown&&d.teardown.call(e,h,g.handle)!==!1||it.removeEvent(e,p,g.handle),delete c[p])}else for(p in c)it.event.remove(e,p+t[u],n,r,!0);it.isEmptyObject(c)&&(delete g.handle,it._removeData(e,"events"))}},trigger:function(t,n,r,i){var o,a,s,l,u,c,d,f=[r||ht],p=tt.call(t,"type")?t.type:t,h=tt.call(t,"namespace")?t.namespace.split("."):[];if(s=c=r=r||ht,3!==r.nodeType&&8!==r.nodeType&&!qt.test(p+it.event.triggered)&&(p.indexOf(".")>=0&&(h=p.split("."),p=h.shift(),h.sort()),a=p.indexOf(":")<0&&"on"+p,t=t[it.expando]?t:new it.Event(p,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=h.join("."),t.namespace_re=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:it.makeArray(n,[t]),u=it.event.special[p]||{},i||!u.trigger||u.trigger.apply(r,n)!==!1)){if(!i&&!u.noBubble&&!it.isWindow(r)){for(l=u.delegateType||p,qt.test(l+p)||(s=s.parentNode);s;s=s.parentNode)f.push(s),c=s;c===(r.ownerDocument||ht)&&f.push(c.defaultView||c.parentWindow||e)}for(d=0;(s=f[d++])&&!t.isPropagationStopped();)t.type=d>1?l:u.bindType||p,o=(it._data(s,"events")||{})[t.type]&&it._data(s,"handle"),o&&o.apply(s,n),o=a&&s[a],o&&o.apply&&it.acceptData(s)&&(t.result=o.apply(s,n),t.result===!1&&t.preventDefault());if(t.type=p,!i&&!t.isDefaultPrevented()&&(!u._default||u._default.apply(f.pop(),n)===!1)&&it.acceptData(r)&&a&&r[p]&&!it.isWindow(r)){c=r[a],c&&(r[a]=null),it.event.triggered=p;try{r[p]()}catch(m){}it.event.triggered=void 0,c&&(r[a]=c)}return t.result}},dispatch:function(e){e=it.event.fix(e);var t,n,r,i,o,a=[],s=Y.call(arguments),l=(it._data(this,"events")||{})[e.type]||[],u=it.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!u.preDispatch||u.preDispatch.call(this,e)!==!1){for(a=it.event.handlers.call(this,e,l),t=0;(i=a[t++])&&!e.isPropagationStopped();)for(e.currentTarget=i.elem,o=0;(r=i.handlers[o++])&&!e.isImmediatePropagationStopped();)(!e.namespace_re||e.namespace_re.test(r.namespace))&&(e.handleObj=r,e.data=r.data,n=((it.event.special[r.origType]||{}).handle||r.handler).apply(i.elem,s),void 0!==n&&(e.result=n)===!1&&(e.preventDefault(),e.stopPropagation()));return u.postDispatch&&u.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,l=e.target;if(s&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(i=[],o=0;s>o;o++)r=t[o],n=r.selector+" ",void 0===i[n]&&(i[n]=r.needsContext?it(n,this).index(l)>=0:it.find(n,this,null,[l]).length),i[n]&&i.push(r);i.length&&a.push({elem:l,handlers:i})}return s<t.length&&a.push({elem:this,handlers:t.slice(s)}),a},fix:function(e){if(e[it.expando])return e;var t,n,r,i=e.type,o=e,a=this.fixHooks[i];for(a||(this.fixHooks[i]=a=_t.test(i)?this.mouseHooks:Ht.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new it.Event(o),t=r.length;t--;)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||ht),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,a.filter?a.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,o=t.button,a=t.fromElement;return null==e.pageX&&null!=t.clientX&&(r=e.target.ownerDocument||ht,i=r.documentElement,n=r.body,e.pageX=t.clientX+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0),e.pageY=t.clientY+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?t.toElement:a),e.which||void 0===o||(e.which=1&o?1:2&o?3:4&o?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==h()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===h()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return it.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(e){return it.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=it.extend(new it.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?it.event.trigger(i,null,t):it.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},it.removeEvent=ht.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===Et&&(e[r]=null),e.detachEvent(r,n))},it.Event=function(e,t){return this instanceof it.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&e.returnValue===!1?f:p):this.type=e,t&&it.extend(this,t),this.timeStamp=e&&e.timeStamp||it.now(),void(this[it.expando]=!0)):new it.Event(e,t)},it.Event.prototype={isDefaultPrevented:p,isPropagationStopped:p,isImmediatePropagationStopped:p,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=f,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=f,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=f,e&&e.stopImmediatePropagation&&e.stopImmediatePropagation(),this.stopPropagation()}},it.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){it.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!it.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),nt.submitBubbles||(it.event.special.submit={setup:function(){return it.nodeName(this,"form")?!1:void it.event.add(this,"click._submit keypress._submit",function(e){var t=e.target,n=it.nodeName(t,"input")||it.nodeName(t,"button")?t.form:void 0;n&&!it._data(n,"submitBubbles")&&(it.event.add(n,"submit._submit",function(e){e._submit_bubble=!0}),it._data(n,"submitBubbles",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&it.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return it.nodeName(this,"form")?!1:void it.event.remove(this,"._submit")}}),nt.changeBubbles||(it.event.special.change={setup:function(){return Lt.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(it.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),it.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),it.event.simulate("change",this,e,!0)})),!1):void it.event.add(this,"beforeactivate._change",function(e){var t=e.target;Lt.test(t.nodeName)&&!it._data(t,"changeBubbles")&&(it.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||it.event.simulate("change",this.parentNode,e,!0)}),it._data(t,"changeBubbles",!0))})},handle:function(e){var t=e.target;return this!==t||e.isSimulated||e.isTrigger||"radio"!==t.type&&"checkbox"!==t.type?e.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return it.event.remove(this,"._change"),!Lt.test(this.nodeName)}}),nt.focusinBubbles||it.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){it.event.simulate(t,e.target,it.event.fix(e),!0)};it.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=it._data(r,t);i||r.addEventListener(e,n,!0),it._data(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=it._data(r,t)-1;i?it._data(r,t,i):(r.removeEventListener(e,n,!0),it._removeData(r,t))}}}),it.fn.extend({on:function(e,t,n,r,i){var o,a;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=void 0);for(o in e)this.on(o,t,n,e[o],i);return this}if(null==n&&null==r?(r=t,n=t=void 0):null==r&&("string"==typeof t?(r=n,n=void 0):(r=n,n=t,t=void 0)),r===!1)r=p;else if(!r)return this;return 1===i&&(a=r,r=function(e){return it().off(e),a.apply(this,arguments)},r.guid=a.guid||(a.guid=it.guid++)),this.each(function(){it.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,it(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=void 0),n===!1&&(n=p),this.each(function(){it.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){it.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?it.event.trigger(e,t,n,!0):void 0}});var Mt="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",Ot=/ jQuery\d+="(?:null|\d+)"/g,Rt=new RegExp("<(?:"+Mt+")[\\s/>]","i"),Bt=/^\s+/,Pt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,It=/<([\w:]+)/,Wt=/<tbody/i,$t=/<|&#?\w+;/,zt=/<(?:script|style|link)/i,Xt=/checked\s*(?:[^=]|=\s*.checked.)/i,Ut=/^$|\/(?:java|ecma)script/i,Vt=/^true\/(.*)/,Gt=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Yt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:nt.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},Jt=m(ht),Kt=Jt.appendChild(ht.createElement("div"));Yt.optgroup=Yt.option,Yt.tbody=Yt.tfoot=Yt.colgroup=Yt.caption=Yt.thead,Yt.th=Yt.td,it.extend({clone:function(e,t,n){var r,i,o,a,s,l=it.contains(e.ownerDocument,e);if(nt.html5Clone||it.isXMLDoc(e)||!Rt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Kt.innerHTML=e.outerHTML,Kt.removeChild(o=Kt.firstChild)),!(nt.noCloneEvent&&nt.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||it.isXMLDoc(e)))for(r=g(o),s=g(e),a=0;null!=(i=s[a]);++a)r[a]&&E(i,r[a]);if(t)if(n)for(s=s||g(e),r=r||g(o),a=0;null!=(i=s[a]);a++)T(i,r[a]);else T(e,o);return r=g(o,"script"),r.length>0&&w(r,!l&&g(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){for(var i,o,a,s,l,u,c,d=e.length,f=m(t),p=[],h=0;d>h;h++)if(o=e[h],o||0===o)if("object"===it.type(o))it.merge(p,o.nodeType?[o]:o);else if($t.test(o)){for(s=s||f.appendChild(t.createElement("div")),l=(It.exec(o)||["",""])[1].toLowerCase(),c=Yt[l]||Yt._default,s.innerHTML=c[1]+o.replace(Pt,"<$1></$2>")+c[2],i=c[0];i--;)s=s.lastChild;if(!nt.leadingWhitespace&&Bt.test(o)&&p.push(t.createTextNode(Bt.exec(o)[0])),!nt.tbody)for(o="table"!==l||Wt.test(o)?"<table>"!==c[1]||Wt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;i--;)it.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u);for(it.merge(p,s.childNodes),s.textContent="";s.firstChild;)s.removeChild(s.firstChild);s=f.lastChild}else p.push(t.createTextNode(o));for(s&&f.removeChild(s),nt.appendChecked||it.grep(g(p,"input"),v),h=0;o=p[h++];)if((!r||-1===it.inArray(o,r))&&(a=it.contains(o.ownerDocument,o),s=g(f.appendChild(o),"script"),a&&w(s),n))for(i=0;o=s[i++];)Ut.test(o.type||"")&&n.push(o);return s=null,f},cleanData:function(e,t){for(var n,r,i,o,a=0,s=it.expando,l=it.cache,u=nt.deleteExpando,c=it.event.special;null!=(n=e[a]);a++)if((t||it.acceptData(n))&&(i=n[s],o=i&&l[i])){if(o.events)for(r in o.events)c[r]?it.event.remove(n,r):it.removeEvent(n,r,o.handle);l[i]&&(delete l[i],u?delete n[s]:typeof n.removeAttribute!==Et?n.removeAttribute(s):n[s]=null,G.push(i))}}}),it.fn.extend({text:function(e){return At(this,function(e){return void 0===e?it.text(this):this.empty().append((this[0]&&this[0].ownerDocument||ht).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){for(var n,r=e?it.filter(e,this):this,i=0;null!=(n=r[i]);i++)t||1!==n.nodeType||it.cleanData(g(n)),n.parentNode&&(t&&it.contains(n.ownerDocument,n)&&w(g(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&it.cleanData(g(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&it.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return it.clone(this,e,t)})},html:function(e){return At(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(Ot,""):void 0;if(!("string"!=typeof e||zt.test(e)||!nt.htmlSerialize&&Rt.test(e)||!nt.leadingWhitespace&&Bt.test(e)||Yt[(It.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(Pt,"<$1></$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(it.cleanData(g(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=arguments[0];return this.domManip(arguments,function(t){e=this.parentNode,it.cleanData(g(this)),e&&e.replaceChild(t,this)}),e&&(e.length||e.nodeType)?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t){e=J.apply([],e);var n,r,i,o,a,s,l=0,u=this.length,c=this,d=u-1,f=e[0],p=it.isFunction(f);if(p||u>1&&"string"==typeof f&&!nt.checkClone&&Xt.test(f))return this.each(function(n){var r=c.eq(n);p&&(e[0]=f.call(this,n,r.html())),r.domManip(e,t)});if(u&&(s=it.buildFragment(e,this[0].ownerDocument,!1,this),n=s.firstChild,1===s.childNodes.length&&(s=n),n)){for(o=it.map(g(s,"script"),b),i=o.length;u>l;l++)r=s,l!==d&&(r=it.clone(r,!0,!0),i&&it.merge(o,g(r,"script"))),t.call(this[l],r,l);if(i)for(a=o[o.length-1].ownerDocument,it.map(o,x),l=0;i>l;l++)r=o[l],Ut.test(r.type||"")&&!it._data(r,"globalEval")&&it.contains(a,r)&&(r.src?it._evalUrl&&it._evalUrl(r.src):it.globalEval((r.text||r.textContent||r.innerHTML||"").replace(Gt,"")));s=n=null}return this}}),it.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){it.fn[e]=function(e){for(var n,r=0,i=[],o=it(e),a=o.length-1;a>=r;r++)n=r===a?this:this.clone(!0),it(o[r])[t](n),K.apply(i,n.get());return this.pushStack(i)}});var Qt,Zt={};!function(){var e;nt.shrinkWrapBlocks=function(){if(null!=e)return e;e=!1;var t,n,r;return n=ht.getElementsByTagName("body")[0],n&&n.style?(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",t.appendChild(ht.createElement("div")).style.width="5px",e=3!==t.offsetWidth),n.removeChild(r),e):void 0}}();var en,tn,nn=/^margin/,rn=new RegExp("^("+Nt+")(?!px)[a-z%]+$","i"),on=/^(top|right|bottom|left)$/;e.getComputedStyle?(en=function(e){return e.ownerDocument.defaultView.getComputedStyle(e,null)},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n.getPropertyValue(t)||n[t]:void 0,n&&(""!==a||it.contains(e.ownerDocument,e)||(a=it.style(e,t)),rn.test(a)&&nn.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0===a?a:a+""}):ht.documentElement.currentStyle&&(en=function(e){return e.currentStyle},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n[t]:void 0,null==a&&s&&s[t]&&(a=s[t]),rn.test(a)&&!on.test(t)&&(r=s.left,i=e.runtimeStyle,o=i&&i.left,o&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"}),function(){function t(){var t,n,r,i;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),t.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",o=a=!1,l=!0,e.getComputedStyle&&(o="1%"!==(e.getComputedStyle(t,null)||{}).top,a="4px"===(e.getComputedStyle(t,null)||{width:"4px"}).width,i=t.appendChild(ht.createElement("div")),i.style.cssText=t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",t.style.width="1px",l=!parseFloat((e.getComputedStyle(i,null)||{}).marginRight)),t.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=t.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",s=0===i[0].offsetHeight,s&&(i[0].style.display="",i[1].style.display="none",s=0===i[0].offsetHeight),n.removeChild(r))}var n,r,i,o,a,s,l;n=ht.createElement("div"),n.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",i=n.getElementsByTagName("a")[0],r=i&&i.style,r&&(r.cssText="float:left;opacity:.5",nt.opacity="0.5"===r.opacity,nt.cssFloat=!!r.cssFloat,n.style.backgroundClip="content-box",n.cloneNode(!0).style.backgroundClip="",nt.clearCloneStyle="content-box"===n.style.backgroundClip,nt.boxSizing=""===r.boxSizing||""===r.MozBoxSizing||""===r.WebkitBoxSizing,it.extend(nt,{reliableHiddenOffsets:function(){return null==s&&t(),s +},boxSizingReliable:function(){return null==a&&t(),a},pixelPosition:function(){return null==o&&t(),o},reliableMarginRight:function(){return null==l&&t(),l}}))}(),it.swap=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};var an=/alpha\([^)]*\)/i,sn=/opacity\s*=\s*([^)]*)/,ln=/^(none|table(?!-c[ea]).+)/,un=new RegExp("^("+Nt+")(.*)$","i"),cn=new RegExp("^([+-])=("+Nt+")","i"),dn={position:"absolute",visibility:"hidden",display:"block"},fn={letterSpacing:"0",fontWeight:"400"},pn=["Webkit","O","Moz","ms"];it.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=tn(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":nt.cssFloat?"cssFloat":"styleFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=it.camelCase(t),l=e.style;if(t=it.cssProps[s]||(it.cssProps[s]=S(l,s)),a=it.cssHooks[t]||it.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];if(o=typeof n,"string"===o&&(i=cn.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(it.css(e,t)),o="number"),null!=n&&n===n&&("number"!==o||it.cssNumber[s]||(n+="px"),nt.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),!(a&&"set"in a&&void 0===(n=a.set(e,n,r)))))try{l[t]=n}catch(u){}}},css:function(e,t,n,r){var i,o,a,s=it.camelCase(t);return t=it.cssProps[s]||(it.cssProps[s]=S(e.style,s)),a=it.cssHooks[t]||it.cssHooks[s],a&&"get"in a&&(o=a.get(e,!0,n)),void 0===o&&(o=tn(e,t,r)),"normal"===o&&t in fn&&(o=fn[t]),""===n||n?(i=parseFloat(o),n===!0||it.isNumeric(i)?i||0:o):o}}),it.each(["height","width"],function(e,t){it.cssHooks[t]={get:function(e,n,r){return n?ln.test(it.css(e,"display"))&&0===e.offsetWidth?it.swap(e,dn,function(){return L(e,t,r)}):L(e,t,r):void 0},set:function(e,n,r){var i=r&&en(e);return A(e,n,r?D(e,t,r,nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,i),i):0)}}}),nt.opacity||(it.cssHooks.opacity={get:function(e,t){return sn.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=it.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===it.trim(o.replace(an,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=an.test(o)?o.replace(an,i):o+" "+i)}}),it.cssHooks.marginRight=N(nt.reliableMarginRight,function(e,t){return t?it.swap(e,{display:"inline-block"},tn,[e,"marginRight"]):void 0}),it.each({margin:"",padding:"",border:"Width"},function(e,t){it.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];4>r;r++)i[e+St[r]+t]=o[r]||o[r-2]||o[0];return i}},nn.test(e)||(it.cssHooks[e+t].set=A)}),it.fn.extend({css:function(e,t){return At(this,function(e,t,n){var r,i,o={},a=0;if(it.isArray(t)){for(r=en(e),i=t.length;i>a;a++)o[t[a]]=it.css(e,t[a],!1,r);return o}return void 0!==n?it.style(e,t,n):it.css(e,t)},e,t,arguments.length>1)},show:function(){return j(this,!0)},hide:function(){return j(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){jt(this)?it(this).show():it(this).hide()})}}),it.Tween=H,H.prototype={constructor:H,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(it.cssNumber[n]?"":"px")},cur:function(){var e=H.propHooks[this.prop];return e&&e.get?e.get(this):H.propHooks._default.get(this)},run:function(e){var t,n=H.propHooks[this.prop];return this.pos=t=this.options.duration?it.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):H.propHooks._default.set(this),this}},H.prototype.init.prototype=H.prototype,H.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=it.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){it.fx.step[e.prop]?it.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[it.cssProps[e.prop]]||it.cssHooks[e.prop])?it.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},H.propHooks.scrollTop=H.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},it.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},it.fx=H.prototype.init,it.fx.step={};var hn,mn,gn=/^(?:toggle|show|hide)$/,vn=new RegExp("^(?:([+-])=|)("+Nt+")([a-z%]*)$","i"),yn=/queueHooks$/,bn=[M],xn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=vn.exec(t),o=i&&i[3]||(it.cssNumber[e]?"":"px"),a=(it.cssNumber[e]||"px"!==o&&+r)&&vn.exec(it.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,it.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};it.Animation=it.extend(R,{tweener:function(e,t){it.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");for(var n,r=0,i=e.length;i>r;r++)n=e[r],xn[n]=xn[n]||[],xn[n].unshift(t)},prefilter:function(e,t){t?bn.unshift(e):bn.push(e)}}),it.speed=function(e,t,n){var r=e&&"object"==typeof e?it.extend({},e):{complete:n||!n&&t||it.isFunction(e)&&e,duration:e,easing:n&&t||t&&!it.isFunction(t)&&t};return r.duration=it.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in it.fx.speeds?it.fx.speeds[r.duration]:it.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){it.isFunction(r.old)&&r.old.call(this),r.queue&&it.dequeue(this,r.queue)},r},it.fn.extend({fadeTo:function(e,t,n,r){return this.filter(jt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=it.isEmptyObject(e),o=it.speed(t,n,r),a=function(){var t=R(this,it.extend({},e),o);(i||it._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=it.timers,a=it._data(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&yn.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));(t||!n)&&it.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=it._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=it.timers,a=r?r.length:0;for(n.finish=!0,it.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),it.each(["toggle","show","hide"],function(e,t){var n=it.fn[t];it.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(q(t,!0),e,r,i)}}),it.each({slideDown:q("show"),slideUp:q("hide"),slideToggle:q("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){it.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),it.timers=[],it.fx.tick=function(){var e,t=it.timers,n=0;for(hn=it.now();n<t.length;n++)e=t[n],e()||t[n]!==e||t.splice(n--,1);t.length||it.fx.stop(),hn=void 0},it.fx.timer=function(e){it.timers.push(e),e()?it.fx.start():it.timers.pop()},it.fx.interval=13,it.fx.start=function(){mn||(mn=setInterval(it.fx.tick,it.fx.interval))},it.fx.stop=function(){clearInterval(mn),mn=null},it.fx.speeds={slow:600,fast:200,_default:400},it.fn.delay=function(e,t){return e=it.fx?it.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},function(){var e,t,n,r,i;t=ht.createElement("div"),t.setAttribute("className","t"),t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",r=t.getElementsByTagName("a")[0],n=ht.createElement("select"),i=n.appendChild(ht.createElement("option")),e=t.getElementsByTagName("input")[0],r.style.cssText="top:1px",nt.getSetAttribute="t"!==t.className,nt.style=/top/.test(r.getAttribute("style")),nt.hrefNormalized="/a"===r.getAttribute("href"),nt.checkOn=!!e.value,nt.optSelected=i.selected,nt.enctype=!!ht.createElement("form").enctype,n.disabled=!0,nt.optDisabled=!i.disabled,e=ht.createElement("input"),e.setAttribute("value",""),nt.input=""===e.getAttribute("value"),e.value="t",e.setAttribute("type","radio"),nt.radioValue="t"===e.value}();var wn=/\r/g;it.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=it.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,it(this).val()):e,null==i?i="":"number"==typeof i?i+="":it.isArray(i)&&(i=it.map(i,function(e){return null==e?"":e+""})),t=it.valHooks[this.type]||it.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return t=it.valHooks[i.type]||it.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(wn,""):null==n?"":n)}}}),it.extend({valHooks:{option:{get:function(e){var t=it.find.attr(e,"value");return null!=t?t:it.trim(it.text(e))}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(nt.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&it.nodeName(n.parentNode,"optgroup"))){if(t=it(n).val(),o)return t;a.push(t)}return a},set:function(e,t){for(var n,r,i=e.options,o=it.makeArray(t),a=i.length;a--;)if(r=i[a],it.inArray(it.valHooks.option.get(r),o)>=0)try{r.selected=n=!0}catch(s){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),it.each(["radio","checkbox"],function(){it.valHooks[this]={set:function(e,t){return it.isArray(t)?e.checked=it.inArray(it(e).val(),t)>=0:void 0}},nt.checkOn||(it.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Tn,En,kn=it.expr.attrHandle,Cn=/^(?:checked|selected)$/i,Nn=nt.getSetAttribute,Sn=nt.input;it.fn.extend({attr:function(e,t){return At(this,it.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){it.removeAttr(this,e)})}}),it.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(e&&3!==o&&8!==o&&2!==o)return typeof e.getAttribute===Et?it.prop(e,t,n):(1===o&&it.isXMLDoc(e)||(t=t.toLowerCase(),r=it.attrHooks[t]||(it.expr.match.bool.test(t)?En:Tn)),void 0===n?r&&"get"in r&&null!==(i=r.get(e,t))?i:(i=it.find.attr(e,t),null==i?void 0:i):null!==n?r&&"set"in r&&void 0!==(i=r.set(e,n,t))?i:(e.setAttribute(t,n+""),n):void it.removeAttr(e,t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(bt);if(o&&1===e.nodeType)for(;n=o[i++];)r=it.propFix[n]||n,it.expr.match.bool.test(n)?Sn&&Nn||!Cn.test(n)?e[r]=!1:e[it.camelCase("default-"+n)]=e[r]=!1:it.attr(e,n,""),e.removeAttribute(Nn?n:r)},attrHooks:{type:{set:function(e,t){if(!nt.radioValue&&"radio"===t&&it.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}}}),En={set:function(e,t,n){return t===!1?it.removeAttr(e,n):Sn&&Nn||!Cn.test(n)?e.setAttribute(!Nn&&it.propFix[n]||n,n):e[it.camelCase("default-"+n)]=e[n]=!0,n}},it.each(it.expr.match.bool.source.match(/\w+/g),function(e,t){var n=kn[t]||it.find.attr;kn[t]=Sn&&Nn||!Cn.test(t)?function(e,t,r){var i,o;return r||(o=kn[t],kn[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,kn[t]=o),i}:function(e,t,n){return n?void 0:e[it.camelCase("default-"+t)]?t.toLowerCase():null}}),Sn&&Nn||(it.attrHooks.value={set:function(e,t,n){return it.nodeName(e,"input")?void(e.defaultValue=t):Tn&&Tn.set(e,t,n)}}),Nn||(Tn={set:function(e,t,n){var r=e.getAttributeNode(n);return r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n)?t:void 0}},kn.id=kn.name=kn.coords=function(e,t,n){var r;return n?void 0:(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},it.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:void 0},set:Tn.set},it.attrHooks.contenteditable={set:function(e,t,n){Tn.set(e,""===t?!1:t,n)}},it.each(["width","height"],function(e,t){it.attrHooks[t]={set:function(e,n){return""===n?(e.setAttribute(t,"auto"),n):void 0}}})),nt.style||(it.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var jn=/^(?:input|select|textarea|button|object)$/i,An=/^(?:a|area)$/i;it.fn.extend({prop:function(e,t){return At(this,it.prop,e,t,arguments.length>1)},removeProp:function(e){return e=it.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(t){}})}}),it.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,a=e.nodeType;if(e&&3!==a&&8!==a&&2!==a)return o=1!==a||!it.isXMLDoc(e),o&&(t=it.propFix[t]||t,i=it.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=it.find.attr(e,"tabindex");return t?parseInt(t,10):jn.test(e.nodeName)||An.test(e.nodeName)&&e.href?0:-1}}}}),nt.hrefNormalized||it.each(["href","src"],function(e,t){it.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),nt.optSelected||(it.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),it.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){it.propFix[this.toLowerCase()]=this}),nt.enctype||(it.propFix.enctype="encoding");var Dn=/[\t\r\n\f]/g;it.fn.extend({addClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u="string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):" ")){for(o=0;i=t[o++];)r.indexOf(" "+i+" ")<0&&(r+=i+" ");a=it.trim(r),n.className!==a&&(n.className=a)}return this},removeClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u=0===arguments.length||"string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):"")){for(o=0;i=t[o++];)for(;r.indexOf(" "+i+" ")>=0;)r=r.replace(" "+i+" "," ");a=e?it.trim(r):"",n.className!==a&&(n.className=a)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):this.each(it.isFunction(e)?function(n){it(this).toggleClass(e.call(this,n,this.className,t),t)}:function(){if("string"===n)for(var t,r=0,i=it(this),o=e.match(bt)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else(n===Et||"boolean"===n)&&(this.className&&it._data(this,"__className__",this.className),this.className=this.className||e===!1?"":it._data(this,"__className__")||"")})},hasClass:function(e){for(var t=" "+e+" ",n=0,r=this.length;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(Dn," ").indexOf(t)>=0)return!0;return!1}}),it.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){it.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),it.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var Ln=it.now(),Hn=/\?/,_n=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;it.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=it.trim(t+"");return i&&!it.trim(i.replace(_n,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():it.error("Invalid JSON: "+t)},it.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{e.DOMParser?(r=new DOMParser,n=r.parseFromString(t,"text/xml")):(n=new ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(t))}catch(i){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||it.error("Invalid XML: "+t),n};var qn,Fn,Mn=/#.*$/,On=/([?&])_=[^&]*/,Rn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Bn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Pn=/^(?:GET|HEAD)$/,In=/^\/\//,Wn=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,$n={},zn={},Xn="*/".concat("*");try{Fn=location.href}catch(Un){Fn=ht.createElement("a"),Fn.href="",Fn=Fn.href}qn=Wn.exec(Fn.toLowerCase())||[],it.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Fn,type:"GET",isLocal:Bn.test(qn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Xn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":it.parseJSON,"text xml":it.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?I(I(e,it.ajaxSettings),t):I(it.ajaxSettings,e)},ajaxPrefilter:B($n),ajaxTransport:B(zn),ajax:function(e,t){function n(e,t,n,r){var i,c,v,y,x,T=t;2!==b&&(b=2,s&&clearTimeout(s),u=void 0,a=r||"",w.readyState=e>0?4:0,i=e>=200&&300>e||304===e,n&&(y=W(d,w,n)),y=$(d,y,w,i),i?(d.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(it.lastModified[o]=x),x=w.getResponseHeader("etag"),x&&(it.etag[o]=x)),204===e||"HEAD"===d.type?T="nocontent":304===e?T="notmodified":(T=y.state,c=y.data,v=y.error,i=!v)):(v=T,(e||!T)&&(T="error",0>e&&(e=0))),w.status=e,w.statusText=(t||T)+"",i?h.resolveWith(f,[c,T,w]):h.rejectWith(f,[w,T,v]),w.statusCode(g),g=void 0,l&&p.trigger(i?"ajaxSuccess":"ajaxError",[w,d,i?c:v]),m.fireWith(f,[w,T]),l&&(p.trigger("ajaxComplete",[w,d]),--it.active||it.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,i,o,a,s,l,u,c,d=it.ajaxSetup({},t),f=d.context||d,p=d.context&&(f.nodeType||f.jquery)?it(f):it.event,h=it.Deferred(),m=it.Callbacks("once memory"),g=d.statusCode||{},v={},y={},b=0,x="canceled",w={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=Rn.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=y[n]=y[n]||e,v[e]=t),this},overrideMimeType:function(e){return b||(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)g[t]=[g[t],e[t]];else w.always(e[w.status]);return this},abort:function(e){var t=e||x;return u&&u.abort(t),n(0,t),this}};if(h.promise(w).complete=m.add,w.success=w.done,w.error=w.fail,d.url=((e||d.url||Fn)+"").replace(Mn,"").replace(In,qn[1]+"//"),d.type=t.method||t.type||d.method||d.type,d.dataTypes=it.trim(d.dataType||"*").toLowerCase().match(bt)||[""],null==d.crossDomain&&(r=Wn.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]===qn[1]&&r[2]===qn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(qn[3]||("http:"===qn[1]?"80":"443")))),d.data&&d.processData&&"string"!=typeof d.data&&(d.data=it.param(d.data,d.traditional)),P($n,d,t,w),2===b)return w;l=d.global,l&&0===it.active++&&it.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!Pn.test(d.type),o=d.url,d.hasContent||(d.data&&(o=d.url+=(Hn.test(o)?"&":"?")+d.data,delete d.data),d.cache===!1&&(d.url=On.test(o)?o.replace(On,"$1_="+Ln++):o+(Hn.test(o)?"&":"?")+"_="+Ln++)),d.ifModified&&(it.lastModified[o]&&w.setRequestHeader("If-Modified-Since",it.lastModified[o]),it.etag[o]&&w.setRequestHeader("If-None-Match",it.etag[o])),(d.data&&d.hasContent&&d.contentType!==!1||t.contentType)&&w.setRequestHeader("Content-Type",d.contentType),w.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+Xn+"; q=0.01":""):d.accepts["*"]);for(i in d.headers)w.setRequestHeader(i,d.headers[i]);if(d.beforeSend&&(d.beforeSend.call(f,w,d)===!1||2===b))return w.abort();x="abort";for(i in{success:1,error:1,complete:1})w[i](d[i]);if(u=P(zn,d,t,w)){w.readyState=1,l&&p.trigger("ajaxSend",[w,d]),d.async&&d.timeout>0&&(s=setTimeout(function(){w.abort("timeout")},d.timeout));try{b=1,u.send(v,n)}catch(T){if(!(2>b))throw T;n(-1,T)}}else n(-1,"No Transport");return w},getJSON:function(e,t,n){return it.get(e,t,n,"json")},getScript:function(e,t){return it.get(e,void 0,t,"script")}}),it.each(["get","post"],function(e,t){it[t]=function(e,n,r,i){return it.isFunction(n)&&(i=i||r,r=n,n=void 0),it.ajax({url:e,type:t,dataType:i,data:n,success:r})}}),it.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){it.fn[t]=function(e){return this.on(t,e)}}),it._evalUrl=function(e){return it.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},it.fn.extend({wrapAll:function(e){if(it.isFunction(e))return this.each(function(t){it(this).wrapAll(e.call(this,t))});if(this[0]){var t=it(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return this.each(it.isFunction(e)?function(t){it(this).wrapInner(e.call(this,t))}:function(){var t=it(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=it.isFunction(e);return this.each(function(n){it(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){it.nodeName(this,"body")||it(this).replaceWith(this.childNodes)}).end()}}),it.expr.filters.hidden=function(e){return e.offsetWidth<=0&&e.offsetHeight<=0||!nt.reliableHiddenOffsets()&&"none"===(e.style&&e.style.display||it.css(e,"display"))},it.expr.filters.visible=function(e){return!it.expr.filters.hidden(e)};var Vn=/%20/g,Gn=/\[\]$/,Yn=/\r?\n/g,Jn=/^(?:submit|button|image|reset|file)$/i,Kn=/^(?:input|select|textarea|keygen)/i;it.param=function(e,t){var n,r=[],i=function(e,t){t=it.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=it.ajaxSettings&&it.ajaxSettings.traditional),it.isArray(e)||e.jquery&&!it.isPlainObject(e))it.each(e,function(){i(this.name,this.value)});else for(n in e)z(n,e[n],t,i);return r.join("&").replace(Vn,"+")},it.fn.extend({serialize:function(){return it.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=it.prop(this,"elements");return e?it.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!it(this).is(":disabled")&&Kn.test(this.nodeName)&&!Jn.test(e)&&(this.checked||!Dt.test(e))}).map(function(e,t){var n=it(this).val();return null==n?null:it.isArray(n)?it.map(n,function(e){return{name:t.name,value:e.replace(Yn,"\r\n")}}):{name:t.name,value:n.replace(Yn,"\r\n")}}).get()}}),it.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&X()||U()}:X;var Qn=0,Zn={},er=it.ajaxSettings.xhr();e.ActiveXObject&&it(e).on("unload",function(){for(var e in Zn)Zn[e](void 0,!0)}),nt.cors=!!er&&"withCredentials"in er,er=nt.ajax=!!er,er&&it.ajaxTransport(function(e){if(!e.crossDomain||nt.cors){var t;return{send:function(n,r){var i,o=e.xhr(),a=++Qn;if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)o[i]=e.xhrFields[i];e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest");for(i in n)void 0!==n[i]&&o.setRequestHeader(i,n[i]+"");o.send(e.hasContent&&e.data||null),t=function(n,i){var s,l,u;if(t&&(i||4===o.readyState))if(delete Zn[a],t=void 0,o.onreadystatechange=it.noop,i)4!==o.readyState&&o.abort();else{u={},s=o.status,"string"==typeof o.responseText&&(u.text=o.responseText);try{l=o.statusText}catch(c){l=""}s||!e.isLocal||e.crossDomain?1223===s&&(s=204):s=u.text?200:404}u&&r(s,l,u,o.getAllResponseHeaders())},e.async?4===o.readyState?setTimeout(t):o.onreadystatechange=Zn[a]=t:t()},abort:function(){t&&t(void 0,!0)}}}}),it.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return it.globalEval(e),e}}}),it.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),it.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=ht.head||it("head")[0]||ht.documentElement;return{send:function(r,i){t=ht.createElement("script"),t.async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||i(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var tr=[],nr=/(=)\?(?=&|$)|\?\?/;it.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=tr.pop()||it.expando+"_"+Ln++;return this[e]=!0,e}}),it.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=t.jsonp!==!1&&(nr.test(t.url)?"url":"string"==typeof t.data&&!(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&nr.test(t.data)&&"data");return s||"jsonp"===t.dataTypes[0]?(i=t.jsonpCallback=it.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(nr,"$1"+i):t.jsonp!==!1&&(t.url+=(Hn.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||it.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,tr.push(i)),a&&it.isFunction(o)&&o(a[0]),a=o=void 0}),"script"):void 0}),it.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||ht;var r=dt.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=it.buildFragment([e],t,i),i&&i.length&&it(i).remove(),it.merge([],r.childNodes))};var rr=it.fn.load;it.fn.load=function(e,t,n){if("string"!=typeof e&&rr)return rr.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>=0&&(r=it.trim(e.slice(s,e.length)),e=e.slice(0,s)),it.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(o="POST"),a.length>0&&it.ajax({url:e,type:o,dataType:"html",data:t}).done(function(e){i=arguments,a.html(r?it("<div>").append(it.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){a.each(n,i||[e.responseText,t,e])}),this},it.expr.filters.animated=function(e){return it.grep(it.timers,function(t){return e===t.elem}).length};var ir=e.document.documentElement;it.offset={setOffset:function(e,t,n){var r,i,o,a,s,l,u,c=it.css(e,"position"),d=it(e),f={};"static"===c&&(e.style.position="relative"),s=d.offset(),o=it.css(e,"top"),l=it.css(e,"left"),u=("absolute"===c||"fixed"===c)&&it.inArray("auto",[o,l])>-1,u?(r=d.position(),a=r.top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(l)||0),it.isFunction(t)&&(t=t.call(e,n,s)),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):d.css(f)}},it.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){it.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;if(o)return t=o.documentElement,it.contains(t,i)?(typeof i.getBoundingClientRect!==Et&&(r=i.getBoundingClientRect()),n=V(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===it.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),it.nodeName(e[0],"html")||(n=e.offset()),n.top+=it.css(e[0],"borderTopWidth",!0),n.left+=it.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-it.css(r,"marginTop",!0),left:t.left-n.left-it.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent||ir;e&&!it.nodeName(e,"html")&&"static"===it.css(e,"position");)e=e.offsetParent;return e||ir})}}),it.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);it.fn[e]=function(r){return At(this,function(e,r,i){var o=V(e);return void 0===i?o?t in o?o[t]:o.document.documentElement[r]:e[r]:void(o?o.scrollTo(n?it(o).scrollLeft():i,n?i:it(o).scrollTop()):e[r]=i)},e,r,arguments.length,null)}}),it.each(["top","left"],function(e,t){it.cssHooks[t]=N(nt.pixelPosition,function(e,n){return n?(n=tn(e,t),rn.test(n)?it(e).position()[t]+"px":n):void 0})}),it.each({Height:"height",Width:"width"},function(e,t){it.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){it.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(r===!0||i===!0?"margin":"border");return At(this,function(t,n,r){var i;return it.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?it.css(t,n,a):it.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),it.fn.size=function(){return this.length},it.fn.andSelf=it.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return it});var or=e.jQuery,ar=e.$;return it.noConflict=function(t){return e.$===it&&(e.$=ar),t&&e.jQuery===it&&(e.jQuery=or),it},typeof t===Et&&(e.jQuery=e.$=it),it}),function(e,t){e.rails!==t&&e.error("jquery-ujs has already been loaded!");var n,r=e(document);e.rails=n={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]",buttonClickSelector:"button[data-remote], button[data-confirm]",inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form",formInputClickSelector:"form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])",disableSelector:"input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled",enableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled",requiredInputSelector:"input[name][required]:not([disabled]),textarea[name][required]:not([disabled])",fileInputSelector:"input[type=file]",linkDisableSelector:"a[data-disable-with], a[data-disable]",buttonDisableSelector:"button[data-remote][data-disable-with], button[data-remote][data-disable]",CSRFProtection:function(t){var n=e('meta[name="csrf-token"]').attr("content");n&&t.setRequestHeader("X-CSRF-Token",n)},refreshCSRFTokens:function(){var t=e("meta[name=csrf-token]").attr("content"),n=e("meta[name=csrf-param]").attr("content");e('form input[name="'+n+'"]').val(t)},fire:function(t,n,r){var i=e.Event(n);return t.trigger(i,r),i.result!==!1},confirm:function(e){return confirm(e)},ajax:function(t){return e.ajax(t)},href:function(e){return e.attr("href")},handleRemote:function(r){var i,o,a,s,l,u,c,d;if(n.fire(r,"ajax:before")){if(s=r.data("cross-domain"),l=s===t?null:s,u=r.data("with-credentials")||null,c=r.data("type")||e.ajaxSettings&&e.ajaxSettings.dataType,r.is("form")){i=r.attr("method"),o=r.attr("action"),a=r.serializeArray();var f=r.data("ujs:submit-button");f&&(a.push(f),r.data("ujs:submit-button",null))}else r.is(n.inputChangeSelector)?(i=r.data("method"),o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):r.is(n.buttonClickSelector)?(i=r.data("method")||"get",o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):(i=r.data("method"),o=n.href(r),a=r.data("params")||null);return d={type:i||"GET",data:a,dataType:c,beforeSend:function(e,i){return i.dataType===t&&e.setRequestHeader("accept","*/*;q=0.5, "+i.accepts.script),n.fire(r,"ajax:beforeSend",[e,i])?void r.trigger("ajax:send",e):!1 +},success:function(e,t,n){r.trigger("ajax:success",[e,t,n])},complete:function(e,t){r.trigger("ajax:complete",[e,t])},error:function(e,t,n){r.trigger("ajax:error",[e,t,n])},crossDomain:l},u&&(d.xhrFields={withCredentials:u}),o&&(d.url=o),n.ajax(d)}return!1},handleMethod:function(r){var i=n.href(r),o=r.data("method"),a=r.attr("target"),s=e("meta[name=csrf-token]").attr("content"),l=e("meta[name=csrf-param]").attr("content"),u=e('<form method="post" action="'+i+'"></form>'),c='<input name="_method" value="'+o+'" type="hidden" />';l!==t&&s!==t&&(c+='<input name="'+l+'" value="'+s+'" type="hidden" />'),a&&u.attr("target",a),u.hide().append(c).appendTo("body"),u.submit()},formElements:function(t,n){return t.is("form")?e(t[0].elements).filter(n):t.find(n)},disableFormElements:function(t){n.formElements(t,n.disableSelector).each(function(){n.disableFormElement(e(this))})},disableFormElement:function(e){var n,r;n=e.is("button")?"html":"val",r=e.data("disable-with"),e.data("ujs:enable-with",e[n]()),r!==t&&e[n](r),e.prop("disabled",!0)},enableFormElements:function(t){n.formElements(t,n.enableSelector).each(function(){n.enableFormElement(e(this))})},enableFormElement:function(e){var t=e.is("button")?"html":"val";e.data("ujs:enable-with")&&e[t](e.data("ujs:enable-with")),e.prop("disabled",!1)},allowAction:function(e){var t,r=e.data("confirm"),i=!1;return r?(n.fire(e,"confirm")&&(i=n.confirm(r),t=n.fire(e,"confirm:complete",[i])),i&&t):!0},blankInputs:function(t,n,r){var i,o,a=e(),s=n||"input,textarea",l=t.find(s);return l.each(function(){if(i=e(this),o=i.is("input[type=checkbox],input[type=radio]")?i.is(":checked"):i.val(),!o==!r){if(i.is("input[type=radio]")&&l.filter('input[type=radio]:checked[name="'+i.attr("name")+'"]').length)return!0;a=a.add(i)}}),a.length?a:!1},nonBlankInputs:function(e,t){return n.blankInputs(e,t,!0)},stopEverything:function(t){return e(t.target).trigger("ujs:everythingStopped"),t.stopImmediatePropagation(),!1},disableElement:function(e){var r=e.data("disable-with");e.data("ujs:enable-with",e.html()),r!==t&&e.html(r),e.bind("click.railsDisable",function(e){return n.stopEverything(e)})},enableElement:function(e){e.data("ujs:enable-with")!==t&&(e.html(e.data("ujs:enable-with")),e.removeData("ujs:enable-with")),e.unbind("click.railsDisable")}},n.fire(r,"rails:attachBindings")&&(e.ajaxPrefilter(function(e,t,r){e.crossDomain||n.CSRFProtection(r)}),r.delegate(n.linkDisableSelector,"ajax:complete",function(){n.enableElement(e(this))}),r.delegate(n.buttonDisableSelector,"ajax:complete",function(){n.enableFormElement(e(this))}),r.delegate(n.linkClickSelector,"click.rails",function(r){var i=e(this),o=i.data("method"),a=i.data("params"),s=r.metaKey||r.ctrlKey;if(!n.allowAction(i))return n.stopEverything(r);if(!s&&i.is(n.linkDisableSelector)&&n.disableElement(i),i.data("remote")!==t){if(s&&(!o||"GET"===o)&&!a)return!0;var l=n.handleRemote(i);return l===!1?n.enableElement(i):l.error(function(){n.enableElement(i)}),!1}return i.data("method")?(n.handleMethod(i),!1):void 0}),r.delegate(n.buttonClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);r.is(n.buttonDisableSelector)&&n.disableFormElement(r);var i=n.handleRemote(r);return i===!1?n.enableFormElement(r):i.error(function(){n.enableFormElement(r)}),!1}),r.delegate(n.inputChangeSelector,"change.rails",function(t){var r=e(this);return n.allowAction(r)?(n.handleRemote(r),!1):n.stopEverything(t)}),r.delegate(n.formSubmitSelector,"submit.rails",function(r){var i,o,a=e(this),s=a.data("remote")!==t;if(!n.allowAction(a))return n.stopEverything(r);if(a.attr("novalidate")==t&&(i=n.blankInputs(a,n.requiredInputSelector),i&&n.fire(a,"ajax:aborted:required",[i])))return n.stopEverything(r);if(s){if(o=n.nonBlankInputs(a,n.fileInputSelector)){setTimeout(function(){n.disableFormElements(a)},13);var l=n.fire(a,"ajax:aborted:file",[o]);return l||setTimeout(function(){n.enableFormElements(a)},13),l}return n.handleRemote(a),!1}setTimeout(function(){n.disableFormElements(a)},13)}),r.delegate(n.formInputClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);var i=r.attr("name"),o=i?{name:i,value:r.val()}:null;r.closest("form").data("ujs:submit-button",o)}),r.delegate(n.formSubmitSelector,"ajax:send.rails",function(t){this==t.target&&n.disableFormElements(e(this))}),r.delegate(n.formSubmitSelector,"ajax:complete.rails",function(t){this==t.target&&n.enableFormElements(e(this))}),e(function(){n.refreshCSRFTokens()}))}(jQuery),function(){var e,t,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v,y,b,x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B,P,I,W,$,z,X,U,V,G,Y=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1},J={}.hasOwnProperty,K=function(e,t){function n(){this.constructor=e}for(var r in t)J.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},Q=[].slice;j={},d=10,$=!1,m=null,S=null,q=null,h=null,V=null,b=function(e){var t;return e=new n(e),B(),c(),F(e),$&&(t=z(e.absolute))?(x(t),w(e)):w(e,W)},z=function(e){var t;return t=j[e],t&&!t.transitionCacheDisabled?t:void 0},g=function(e){return null==e&&(e=!0),$=e},w=function(e,t){return null==t&&(t=function(){return function(){}}(this)),X("page:fetch",{url:e.absolute}),null!=V&&V.abort(),V=new XMLHttpRequest,V.open("GET",e.withoutHashForIE10compatibility(),!0),V.setRequestHeader("Accept","text/html, application/xhtml+xml, application/xml"),V.setRequestHeader("X-XHR-Referer",q),V.onload=function(){var n;return X("page:receive"),(n=H())?(f.apply(null,y(n)),M(),t(),X("page:load")):document.location.href=e.absolute},V.onloadend=function(){return V=null},V.onerror=function(){return document.location.href=e.absolute},V.send()},x=function(e){return null!=V&&V.abort(),f(e.title,e.body),_(e),X("page:restore")},c=function(){var e;return e=new n(m.url),j[e.absolute]={url:e.relative,body:document.body,title:document.title,positionY:window.pageYOffset,positionX:window.pageXOffset,cachedAt:(new Date).getTime(),transitionCacheDisabled:null!=document.querySelector("[data-no-transition-cache]")},p(d)},D=function(e){return null==e&&(e=d),/^[\d]+$/.test(e)?d=parseInt(e):void 0},p=function(e){var t,n,r,i,o,a;for(r=Object.keys(j),t=r.map(function(e){return j[e].cachedAt}).sort(function(e,t){return t-e}),a=[],i=0,o=r.length;o>i;i++)n=r[i],j[n].cachedAt<=t[e]&&(X("page:expire",j[n]),a.push(delete j[n]));return a},f=function(t,n,r,i){return document.title=t,document.documentElement.replaceChild(n,document.body),null!=r&&e.update(r),i&&v(),m=window.history.state,X("page:change"),X("page:update")},v=function(){var e,t,n,r,i,o,a,s,l,u,c,d;for(o=Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])')),a=0,l=o.length;l>a;a++)if(i=o[a],""===(c=i.type)||"text/javascript"===c){for(t=document.createElement("script"),d=i.attributes,s=0,u=d.length;u>s;s++)e=d[s],t.setAttribute(e.name,e.value);t.appendChild(document.createTextNode(i.innerHTML)),r=i.parentNode,n=i.nextSibling,r.removeChild(i),r.insertBefore(t,n)}},P=function(e){return e.innerHTML=e.innerHTML.replace(/<noscript[\S\s]*?<\/noscript>/gi,""),e},F=function(e){return(e=new n(e)).absolute!==q?window.history.pushState({turbolinks:!0,url:e.absolute},"",e.absolute):void 0},M=function(){var e,t;return(e=V.getResponseHeader("X-XHR-Redirected-To"))?(e=new n(e),t=e.hasNoHash()?document.location.hash:"",window.history.replaceState(m,"",e.href+t)):void 0},B=function(){return q=document.location.href},R=function(){return window.history.replaceState({turbolinks:!0,url:document.location.href},"",document.location.href)},O=function(){return m=window.history.state},_=function(e){return window.scrollTo(e.positionX,e.positionY)},W=function(){return document.location.hash?document.location.href=document.location.href:window.scrollTo(0,0)},L=function(e){var t,n;return t=(null!=(n=document.cookie.match(new RegExp(e+"=(\\w+)")))?n[1].toUpperCase():void 0)||"",document.cookie=e+"=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/",t},X=function(e,t){var n;return n=document.createEvent("Events"),t&&(n.data=t),n.initEvent(e,!0,!0),document.dispatchEvent(n)},A=function(){return!X("page:before-change")},H=function(){var e,t,n,r,i,o;return t=function(){var e;return 400<=(e=V.status)&&600>e},o=function(){return V.getResponseHeader("Content-Type").match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/)},r=function(e){var t,n,r,i,o;for(i=e.head.childNodes,o=[],n=0,r=i.length;r>n;n++)t=i[n],null!=("function"==typeof t.getAttribute?t.getAttribute("data-turbolinks-track"):void 0)&&o.push(t.getAttribute("src")||t.getAttribute("href"));return o},e=function(e){var t;return S||(S=r(document)),t=r(e),t.length!==S.length||i(t,S).length!==S.length},i=function(e,t){var n,r,i,o,a;for(e.length>t.length&&(o=[t,e],e=o[0],t=o[1]),a=[],r=0,i=e.length;i>r;r++)n=e[r],Y.call(t,n)>=0&&a.push(n);return a},!t()&&o()&&(n=h(V.responseText),n&&!e(n))?n:void 0},y=function(t){var n;return n=t.querySelector("title"),[null!=n?n.textContent:void 0,P(t.body),e.get(t).token,"runScripts"]},e={get:function(e){var t;return null==e&&(e=document),{node:t=e.querySelector('meta[name="csrf-token"]'),token:null!=t&&"function"==typeof t.getAttribute?t.getAttribute("content"):void 0}},update:function(e){var t;return t=this.get(),null!=t.token&&null!=e&&t.token!==e?t.node.setAttribute("content",e):void 0}},i=function(){var e,t,n,r,i,o;t=function(e){return(new DOMParser).parseFromString(e,"text/html")},e=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.documentElement.innerHTML=e,t},n=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.open("replace"),t.write(e),t.close(),t};try{if(window.DOMParser)return i=t("<html><body><p>test"),t}catch(a){return r=a,i=e("<html><body><p>test"),e}finally{if(1!==(null!=i&&null!=(o=i.body)?o.childNodes.length:void 0))return n}},n=function(){function e(t){return this.original=null!=t?t:document.location.href,this.original.constructor===e?this.original:void this._parse()}return e.prototype.withoutHash=function(){return this.href.replace(this.hash,"")},e.prototype.withoutHashForIE10compatibility=function(){return this.withoutHash()},e.prototype.hasNoHash=function(){return 0===this.hash.length},e.prototype._parse=function(){var e;return(null!=this.link?this.link:this.link=document.createElement("a")).href=this.original,e=this.link,this.href=e.href,this.protocol=e.protocol,this.host=e.host,this.hostname=e.hostname,this.port=e.port,this.pathname=e.pathname,this.search=e.search,this.hash=e.hash,this.origin=[this.protocol,"//",this.hostname].join(""),0!==this.port.length&&(this.origin+=":"+this.port),this.relative=[this.pathname,this.search,this.hash].join(""),this.absolute=this.href},e}(),r=function(e){function t(e){return this.link=e,this.link.constructor===t?this.link:(this.original=this.link.href,void t.__super__.constructor.apply(this,arguments))}return K(t,e),t.HTML_EXTENSIONS=["html"],t.allowExtensions=function(){var e,n,r,i;for(n=1<=arguments.length?Q.call(arguments,0):[],r=0,i=n.length;i>r;r++)e=n[r],t.HTML_EXTENSIONS.push(e);return t.HTML_EXTENSIONS},t.prototype.shouldIgnore=function(){return this._crossOrigin()||this._anchored()||this._nonHtml()||this._optOut()||this._target()},t.prototype._crossOrigin=function(){return this.origin!==(new n).origin},t.prototype._anchored=function(){var e;return(this.hash&&this.withoutHash())===(e=new n).withoutHash()||this.href===e.href+"#"},t.prototype._nonHtml=function(){return this.pathname.match(/\.[a-z]+$/g)&&!this.pathname.match(new RegExp("\\.(?:"+t.HTML_EXTENSIONS.join("|")+")?$","g"))},t.prototype._optOut=function(){var e,t;for(t=this.link;!e&&t!==document;)e=null!=t.getAttribute("data-no-turbolink"),t=t.parentNode;return e},t.prototype._target=function(){return 0!==this.link.target.length},t}(n),t=function(){function e(e){this.event=e,this.event.defaultPrevented||(this._extractLink(),this._validForTurbolinks()&&(A()||U(this.link.href),this.event.preventDefault()))}return e.installHandlerLast=function(t){return t.defaultPrevented?void 0:(document.removeEventListener("click",e.handle,!1),document.addEventListener("click",e.handle,!1))},e.handle=function(t){return new e(t)},e.prototype._extractLink=function(){var e;for(e=this.event.target;e.parentNode&&"A"!==e.nodeName;)e=e.parentNode;return"A"===e.nodeName&&0!==e.href.length?this.link=new r(e):void 0},e.prototype._validForTurbolinks=function(){return null!=this.link&&!(this.link.shouldIgnore()||this._nonStandardClick())},e.prototype._nonStandardClick=function(){return this.event.which>1||this.event.metaKey||this.event.ctrlKey||this.event.shiftKey||this.event.altKey},e}(),u=function(e){return setTimeout(e,500)},k=function(){return document.addEventListener("DOMContentLoaded",function(){return X("page:change"),X("page:update")},!0)},N=function(){return"undefined"!=typeof jQuery?jQuery(document).on("ajaxSuccess",function(e,t){return jQuery.trim(t.responseText)?X("page:update"):void 0}):void 0},C=function(e){var t,r;return(null!=(r=e.state)?r.turbolinks:void 0)?(t=j[new n(e.state.url).absolute])?(c(),x(t)):U(e.target.location.href):void 0},E=function(){return R(),O(),h=i(),document.addEventListener("click",t.installHandlerLast,!0),u(function(){return window.addEventListener("popstate",C,!1)})},T=void 0!==window.history.state||navigator.userAgent.match(/Firefox\/2[6|7]/),s=window.history&&window.history.pushState&&window.history.replaceState&&T,o=!navigator.userAgent.match(/CriOS\//),I="GET"===(G=L("request_method"))||""===G,l=s&&o&&I,a=document.addEventListener&&document.createEvent,a&&(k(),N()),l?(U=b,E()):U=function(e){return document.location.href=e},this.Turbolinks={visit:U,pagesCached:D,enableTransitionCache:g,allowLinkExtensions:r.allowExtensions,supported:l}}.call(this),function(){}.call(this);
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz b/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz Binary files differnew file mode 100644 index 0000000000..f62c656dc8 --- /dev/null +++ b/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz diff --git a/actionpack/test/fixtures/public/gzip/foo.zoo b/actionpack/test/fixtures/public/gzip/foo.zoo new file mode 100644 index 0000000000..1826a7660e --- /dev/null +++ b/actionpack/test/fixtures/public/gzip/foo.zoo @@ -0,0 +1,4 @@ +!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){function n(e){var t=e.length,n=it.type(e);return"function"===n||it.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e}function r(e,t,n){if(it.isFunction(t))return it.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return it.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ft.test(t))return it.filter(t,e,n);t=it.filter(t,e)}return it.grep(e,function(e){return it.inArray(e,t)>=0!==n})}function i(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function o(e){var t=xt[e]={};return it.each(e.match(bt)||[],function(e,n){t[n]=!0}),t}function a(){ht.addEventListener?(ht.removeEventListener("DOMContentLoaded",s,!1),e.removeEventListener("load",s,!1)):(ht.detachEvent("onreadystatechange",s),e.detachEvent("onload",s))}function s(){(ht.addEventListener||"load"===event.type||"complete"===ht.readyState)&&(a(),it.ready())}function l(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(Ct,"-$1").toLowerCase();if(n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:kt.test(n)?it.parseJSON(n):n}catch(i){}it.data(e,t,n)}else n=void 0}return n}function u(e){var t;for(t in e)if(("data"!==t||!it.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function c(e,t,n,r){if(it.acceptData(e)){var i,o,a=it.expando,s=e.nodeType,l=s?it.cache:e,u=s?e[a]:e[a]&&a;if(u&&l[u]&&(r||l[u].data)||void 0!==n||"string"!=typeof t)return u||(u=s?e[a]=G.pop()||it.guid++:a),l[u]||(l[u]=s?{}:{toJSON:it.noop}),("object"==typeof t||"function"==typeof t)&&(r?l[u]=it.extend(l[u],t):l[u].data=it.extend(l[u].data,t)),o=l[u],r||(o.data||(o.data={}),o=o.data),void 0!==n&&(o[it.camelCase(t)]=n),"string"==typeof t?(i=o[t],null==i&&(i=o[it.camelCase(t)])):i=o,i}}function d(e,t,n){if(it.acceptData(e)){var r,i,o=e.nodeType,a=o?it.cache:e,s=o?e[it.expando]:it.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){it.isArray(t)?t=t.concat(it.map(t,it.camelCase)):t in r?t=[t]:(t=it.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;for(;i--;)delete r[t[i]];if(n?!u(r):!it.isEmptyObject(r))return}(n||(delete a[s].data,u(a[s])))&&(o?it.cleanData([e],!0):nt.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}function f(){return!0}function p(){return!1}function h(){try{return ht.activeElement}catch(e){}}function m(e){var t=Mt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function g(e,t){var n,r,i=0,o=typeof e.getElementsByTagName!==Et?e.getElementsByTagName(t||"*"):typeof e.querySelectorAll!==Et?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||it.nodeName(r,t)?o.push(r):it.merge(o,g(r,t));return void 0===t||t&&it.nodeName(e,t)?it.merge([e],o):o}function v(e){Dt.test(e.type)&&(e.defaultChecked=e.checked)}function y(e,t){return it.nodeName(e,"table")&&it.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function b(e){return e.type=(null!==it.find.attr(e,"type"))+"/"+e.type,e}function x(e){var t=Vt.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function w(e,t){for(var n,r=0;null!=(n=e[r]);r++)it._data(n,"globalEval",!t||it._data(t[r],"globalEval"))}function T(e,t){if(1===t.nodeType&&it.hasData(e)){var n,r,i,o=it._data(e),a=it._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)it.event.add(t,n,s[n][r])}a.data&&(a.data=it.extend({},a.data))}}function E(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!nt.noCloneEvent&&t[it.expando]){i=it._data(t);for(r in i.events)it.removeEvent(t,r,i.handle);t.removeAttribute(it.expando)}"script"===n&&t.text!==e.text?(b(t).text=e.text,x(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),nt.html5Clone&&e.innerHTML&&!it.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Dt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function k(t,n){var r,i=it(n.createElement(t)).appendTo(n.body),o=e.getDefaultComputedStyle&&(r=e.getDefaultComputedStyle(i[0]))?r.display:it.css(i[0],"display");return i.detach(),o}function C(e){var t=ht,n=Zt[e];return n||(n=k(e,t),"none"!==n&&n||(Qt=(Qt||it("<iframe frameborder='0' width='0' height='0'/>")).appendTo(t.documentElement),t=(Qt[0].contentWindow||Qt[0].contentDocument).document,t.write(),t.close(),n=k(e,t),Qt.detach()),Zt[e]=n),n}function N(e,t){return{get:function(){var n=e();if(null!=n)return n?void delete this.get:(this.get=t).apply(this,arguments)}}}function S(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=pn.length;i--;)if(t=pn[i]+n,t in e)return t;return r}function j(e,t){for(var n,r,i,o=[],a=0,s=e.length;s>a;a++)r=e[a],r.style&&(o[a]=it._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&jt(r)&&(o[a]=it._data(r,"olddisplay",C(r.nodeName)))):(i=jt(r),(n&&"none"!==n||!i)&&it._data(r,"olddisplay",i?n:it.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}function A(e,t,n){var r=un.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function D(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=it.css(e,n+St[o],!0,i)),r?("content"===n&&(a-=it.css(e,"padding"+St[o],!0,i)),"margin"!==n&&(a-=it.css(e,"border"+St[o]+"Width",!0,i))):(a+=it.css(e,"padding"+St[o],!0,i),"padding"!==n&&(a+=it.css(e,"border"+St[o]+"Width",!0,i)));return a}function L(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=en(e),a=nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=tn(e,t,o),(0>i||null==i)&&(i=e.style[t]),rn.test(i))return i;r=a&&(nt.boxSizingReliable()||i===e.style[t]),i=parseFloat(i)||0}return i+D(e,t,n||(a?"border":"content"),r,o)+"px"}function H(e,t,n,r,i){return new H.prototype.init(e,t,n,r,i)}function _(){return setTimeout(function(){hn=void 0}),hn=it.now()}function q(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=St[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function F(e,t,n){for(var r,i=(xn[t]||[]).concat(xn["*"]),o=0,a=i.length;a>o;o++)if(r=i[o].call(n,t,e))return r}function M(e,t,n){var r,i,o,a,s,l,u,c,d=this,f={},p=e.style,h=e.nodeType&&jt(e),m=it._data(e,"fxshow");n.queue||(s=it._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,d.always(function(){d.always(function(){s.unqueued--,it.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],u=it.css(e,"display"),c="none"===u?it._data(e,"olddisplay")||C(e.nodeName):u,"inline"===c&&"none"===it.css(e,"float")&&(nt.inlineBlockNeedsLayout&&"inline"!==C(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",nt.shrinkWrapBlocks()||d.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],gn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(h?"hide":"show")){if("show"!==i||!m||void 0===m[r])continue;h=!0}f[r]=m&&m[r]||it.style(e,r)}else u=void 0;if(it.isEmptyObject(f))"inline"===("none"===u?C(e.nodeName):u)&&(p.display=u);else{m?"hidden"in m&&(h=m.hidden):m=it._data(e,"fxshow",{}),o&&(m.hidden=!h),h?it(e).show():d.done(function(){it(e).hide()}),d.done(function(){var t;it._removeData(e,"fxshow");for(t in f)it.style(e,t,f[t])});for(r in f)a=F(h?m[r]:0,r,d),r in m||(m[r]=a.start,h&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function O(e,t){var n,r,i,o,a;for(n in e)if(r=it.camelCase(n),i=t[r],o=e[n],it.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=it.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function R(e,t,n){var r,i,o=0,a=bn.length,s=it.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;for(var t=hn||_(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:it.extend({},t),opts:it.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:hn||_(),duration:n.duration,tweens:[],createTween:function(t,n){var r=it.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(O(c,u.opts.specialEasing);a>o;o++)if(r=bn[o].call(u,e,c,u.opts))return r;return it.map(c,F,u),it.isFunction(u.opts.start)&&u.opts.start.call(e,u),it.fx.timer(it.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function B(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(bt)||[];if(it.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function P(e,t,n,r){function i(s){var l;return o[s]=!0,it.each(e[s]||[],function(e,s){var u=s(t,n,r);return"string"!=typeof u||a||o[u]?a?!(l=u):void 0:(t.dataTypes.unshift(u),i(u),!1)}),l}var o={},a=e===zn;return i(t.dataTypes[0])||!o["*"]&&i("*")}function I(e,t){var n,r,i=it.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&it.extend(!0,e,n),e}function W(e,t,n){for(var r,i,o,a,s=e.contents,l=e.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){l.unshift(a);break}if(l[0]in n)o=l[0];else{for(a in n){if(!l[0]||e.converters[a+" "+l[0]]){o=a;break}r||(r=a)}o=o||r}return o?(o!==l[0]&&l.unshift(o),n[o]):void 0}function $(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(d){return{state:"parsererror",error:a?d:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}function z(e,t,n,r){var i;if(it.isArray(t))it.each(t,function(t,i){n||Gn.test(e)?r(e,i):z(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==it.type(t))r(e,t);else for(i in t)z(e+"["+i+"]",t[i],n,r)}function X(){try{return new e.XMLHttpRequest}catch(t){}}function U(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function V(e){return it.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}var G=[],Y=G.slice,J=G.concat,K=G.push,Q=G.indexOf,Z={},et=Z.toString,tt=Z.hasOwnProperty,nt={},rt="1.11.1",it=function(e,t){return new it.fn.init(e,t)},ot=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,at=/^-ms-/,st=/-([\da-z])/gi,lt=function(e,t){return t.toUpperCase()};it.fn=it.prototype={jquery:rt,constructor:it,selector:"",length:0,toArray:function(){return Y.call(this)},get:function(e){return null!=e?0>e?this[e+this.length]:this[e]:Y.call(this)},pushStack:function(e){var t=it.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return it.each(this,e,t)},map:function(e){return this.pushStack(it.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(Y.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:K,sort:G.sort,splice:G.splice},it.extend=it.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,l=arguments.length,u=!1;for("boolean"==typeof a&&(u=a,a=arguments[s]||{},s++),"object"==typeof a||it.isFunction(a)||(a={}),s===l&&(a=this,s--);l>s;s++)if(null!=(i=arguments[s]))for(r in i)e=a[r],n=i[r],a!==n&&(u&&n&&(it.isPlainObject(n)||(t=it.isArray(n)))?(t?(t=!1,o=e&&it.isArray(e)?e:[]):o=e&&it.isPlainObject(e)?e:{},a[r]=it.extend(u,o,n)):void 0!==n&&(a[r]=n));return a},it.extend({expando:"jQuery"+(rt+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isFunction:function(e){return"function"===it.type(e)},isArray:Array.isArray||function(e){return"array"===it.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!it.isArray(e)&&e-parseFloat(e)>=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==it.type(e)||e.nodeType||it.isWindow(e))return!1;try{if(e.constructor&&!tt.call(e,"constructor")&&!tt.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}if(nt.ownLast)for(t in e)return tt.call(e,t);for(t in e);return void 0===t||tt.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?Z[et.call(e)]||"object":typeof e},globalEval:function(t){t&&it.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(at,"ms-").replace(st,lt)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,r){var i,o=0,a=e.length,s=n(e);if(r){if(s)for(;a>o&&(i=t.apply(e[o],r),i!==!1);o++);else for(o in e)if(i=t.apply(e[o],r),i===!1)break}else if(s)for(;a>o&&(i=t.call(e[o],o,e[o]),i!==!1);o++);else for(o in e)if(i=t.call(e[o],o,e[o]),i===!1)break;return e},trim:function(e){return null==e?"":(e+"").replace(ot,"")},makeArray:function(e,t){var r=t||[];return null!=e&&(n(Object(e))?it.merge(r,"string"==typeof e?[e]:e):K.call(r,e)),r},inArray:function(e,t,n){var r;if(t){if(Q)return Q.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;n>r;)e[i++]=t[r++];if(n!==n)for(;void 0!==t[r];)e[i++]=t[r++];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;a>o;o++)r=!t(e[o],o),r!==s&&i.push(e[o]);return i},map:function(e,t,r){var i,o=0,a=e.length,s=n(e),l=[];if(s)for(;a>o;o++)i=t(e[o],o,r),null!=i&&l.push(i);else for(o in e)i=t(e[o],o,r),null!=i&&l.push(i);return J.apply([],l)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(i=e[t],t=e,e=i),it.isFunction(e)?(n=Y.call(arguments,2),r=function(){return e.apply(t||this,n.concat(Y.call(arguments)))},r.guid=e.guid=e.guid||it.guid++,r):void 0},now:function(){return+new Date},support:nt}),it.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){Z["[object "+t+"]"]=t.toLowerCase()});var ut=function(e){function t(e,t,n,r){var i,o,a,s,l,u,d,p,h,m;if((t?t.ownerDocument||t:P)!==H&&L(t),t=t||H,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(q&&!r){if(i=yt.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&R(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return Z.apply(n,t.getElementsByTagName(e)),n;if((a=i[3])&&w.getElementsByClassName&&t.getElementsByClassName)return Z.apply(n,t.getElementsByClassName(a)),n}if(w.qsa&&(!F||!F.test(e))){if(p=d=B,h=t,m=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){for(u=C(e),(d=t.getAttribute("id"))?p=d.replace(xt,"\\$&"):t.setAttribute("id",p),p="[id='"+p+"'] ",l=u.length;l--;)u[l]=p+f(u[l]);h=bt.test(e)&&c(t.parentNode)||t,m=u.join(",")}if(m)try{return Z.apply(n,h.querySelectorAll(m)),n}catch(g){}finally{d||t.removeAttribute("id")}}}return S(e.replace(lt,"$1"),t,n,r)}function n(){function e(n,r){return t.push(n+" ")>T.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[B]=!0,e}function i(e){var t=H.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=e.length;r--;)T.attrHandle[n[r]]=t}function a(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||G)-(~e.sourceIndex||G);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function s(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function l(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function u(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function c(e){return e&&typeof e.getElementsByTagName!==V&&e}function d(){}function f(e){for(var t=0,n=e.length,r="";n>t;t++)r+=e[t].value;return r}function p(e,t,n){var r=t.dir,i=n&&"parentNode"===r,o=W++;return t.first?function(t,n,o){for(;t=t[r];)if(1===t.nodeType||i)return e(t,n,o)}:function(t,n,a){var s,l,u=[I,o];if(a){for(;t=t[r];)if((1===t.nodeType||i)&&e(t,n,a))return!0}else for(;t=t[r];)if(1===t.nodeType||i){if(l=t[B]||(t[B]={}),(s=l[r])&&s[0]===I&&s[1]===o)return u[2]=s[2];if(l[r]=u,u[2]=e(t,n,a))return!0}}}function h(e){return e.length>1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function m(e,n,r){for(var i=0,o=n.length;o>i;i++)t(e,n[i],r);return r}function g(e,t,n,r,i){for(var o,a=[],s=0,l=e.length,u=null!=t;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function v(e,t,n,i,o,a){return i&&!i[B]&&(i=v(i)),o&&!o[B]&&(o=v(o,a)),r(function(r,a,s,l){var u,c,d,f=[],p=[],h=a.length,v=r||m(t||"*",s.nodeType?[s]:s,[]),y=!e||!r&&t?v:g(v,f,e,s,l),b=n?o||(r?e:h||i)?[]:a:y;if(n&&n(y,b,s,l),i)for(u=g(b,p),i(u,[],s,l),c=u.length;c--;)(d=u[c])&&(b[p[c]]=!(y[p[c]]=d));if(r){if(o||e){if(o){for(u=[],c=b.length;c--;)(d=b[c])&&u.push(y[c]=d);o(null,b=[],u,l)}for(c=b.length;c--;)(d=b[c])&&(u=o?tt.call(r,d):f[c])>-1&&(r[u]=!(a[u]=d))}}else b=g(b===a?b.splice(h,b.length):b),o?o(null,a,b,l):Z.apply(a,b)})}function y(e){for(var t,n,r,i=e.length,o=T.relative[e[0].type],a=o||T.relative[" "],s=o?1:0,l=p(function(e){return e===t},a,!0),u=p(function(e){return tt.call(t,e)>-1},a,!0),c=[function(e,n,r){return!o&&(r||n!==j)||((t=n).nodeType?l(e,n,r):u(e,n,r))}];i>s;s++)if(n=T.relative[e[s].type])c=[p(h(c),n)];else{if(n=T.filter[e[s].type].apply(null,e[s].matches),n[B]){for(r=++s;i>r&&!T.relative[e[r].type];r++);return v(s>1&&h(c),s>1&&f(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(lt,"$1"),n,r>s&&y(e.slice(s,r)),i>r&&y(e=e.slice(r)),i>r&&f(e))}c.push(n)}return h(c)}function b(e,n){var i=n.length>0,o=e.length>0,a=function(r,a,s,l,u){var c,d,f,p=0,h="0",m=r&&[],v=[],y=j,b=r||o&&T.find.TAG("*",u),x=I+=null==y?1:Math.random()||.1,w=b.length;for(u&&(j=a!==H&&a);h!==w&&null!=(c=b[h]);h++){if(o&&c){for(d=0;f=e[d++];)if(f(c,a,s)){l.push(c);break}u&&(I=x)}i&&((c=!f&&c)&&p--,r&&m.push(c))}if(p+=h,i&&h!==p){for(d=0;f=n[d++];)f(m,v,a,s);if(r){if(p>0)for(;h--;)m[h]||v[h]||(v[h]=K.call(l));v=g(v)}Z.apply(l,v),u&&!r&&v.length>0&&p+n.length>1&&t.uniqueSort(l)}return u&&(I=x,j=y),m};return i?r(a):a}var x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B="sizzle"+-new Date,P=e.document,I=0,W=0,$=n(),z=n(),X=n(),U=function(e,t){return e===t&&(D=!0),0},V="undefined",G=1<<31,Y={}.hasOwnProperty,J=[],K=J.pop,Q=J.push,Z=J.push,et=J.slice,tt=J.indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(this[t]===e)return t;return-1},nt="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",rt="[\\x20\\t\\r\\n\\f]",it="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",ot=it.replace("w","w#"),at="\\["+rt+"*("+it+")(?:"+rt+"*([*^$|!~]?=)"+rt+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+ot+"))|)"+rt+"*\\]",st=":("+it+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+at+")*)|.*)\\)|)",lt=new RegExp("^"+rt+"+|((?:^|[^\\\\])(?:\\\\.)*)"+rt+"+$","g"),ut=new RegExp("^"+rt+"*,"+rt+"*"),ct=new RegExp("^"+rt+"*([>+~]|"+rt+")"+rt+"*"),dt=new RegExp("="+rt+"*([^\\]'\"]*?)"+rt+"*\\]","g"),ft=new RegExp(st),pt=new RegExp("^"+ot+"$"),ht={ID:new RegExp("^#("+it+")"),CLASS:new RegExp("^\\.("+it+")"),TAG:new RegExp("^("+it.replace("w","w*")+")"),ATTR:new RegExp("^"+at),PSEUDO:new RegExp("^"+st),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+rt+"*(even|odd|(([+-]|)(\\d*)n|)"+rt+"*(?:([+-]|)"+rt+"*(\\d+)|))"+rt+"*\\)|)","i"),bool:new RegExp("^(?:"+nt+")$","i"),needsContext:new RegExp("^"+rt+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+rt+"*((?:-\\d)?\\d*)"+rt+"*\\)|)(?=[^-]|$)","i")},mt=/^(?:input|select|textarea|button)$/i,gt=/^h\d$/i,vt=/^[^{]+\{\s*\[native \w/,yt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,bt=/[+~]/,xt=/'|\\/g,wt=new RegExp("\\\\([\\da-f]{1,6}"+rt+"?|("+rt+")|.)","ig"),Tt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{Z.apply(J=et.call(P.childNodes),P.childNodes),J[P.childNodes.length].nodeType}catch(Et){Z={apply:J.length?function(e,t){Q.apply(e,et.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}w=t.support={},k=t.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},L=t.setDocument=function(e){var t,n=e?e.ownerDocument||e:P,r=n.defaultView;return n!==H&&9===n.nodeType&&n.documentElement?(H=n,_=n.documentElement,q=!k(n),r&&r!==r.top&&(r.addEventListener?r.addEventListener("unload",function(){L()},!1):r.attachEvent&&r.attachEvent("onunload",function(){L()})),w.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),w.getElementsByTagName=i(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),w.getElementsByClassName=vt.test(n.getElementsByClassName)&&i(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),w.getById=i(function(e){return _.appendChild(e).id=B,!n.getElementsByName||!n.getElementsByName(B).length}),w.getById?(T.find.ID=function(e,t){if(typeof t.getElementById!==V&&q){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){return e.getAttribute("id")===t}}):(delete T.find.ID,T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){var n=typeof e.getAttributeNode!==V&&e.getAttributeNode("id");return n&&n.value===t}}),T.find.TAG=w.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==V?t.getElementsByTagName(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},T.find.CLASS=w.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==V&&q?t.getElementsByClassName(e):void 0},M=[],F=[],(w.qsa=vt.test(n.querySelectorAll))&&(i(function(e){e.innerHTML="<select msallowclip=''><option selected=''></option></select>",e.querySelectorAll("[msallowclip^='']").length&&F.push("[*^$]="+rt+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||F.push("\\["+rt+"*(?:value|"+nt+")"),e.querySelectorAll(":checked").length||F.push(":checked")}),i(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&F.push("name"+rt+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||F.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),F.push(",.*:")})),(w.matchesSelector=vt.test(O=_.matches||_.webkitMatchesSelector||_.mozMatchesSelector||_.oMatchesSelector||_.msMatchesSelector))&&i(function(e){w.disconnectedMatch=O.call(e,"div"),O.call(e,"[s!='']:x"),M.push("!=",st)}),F=F.length&&new RegExp(F.join("|")),M=M.length&&new RegExp(M.join("|")),t=vt.test(_.compareDocumentPosition),R=t||vt.test(_.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},U=t?function(e,t){if(e===t)return D=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r?r:(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&r||!w.sortDetached&&t.compareDocumentPosition(e)===r?e===n||e.ownerDocument===P&&R(P,e)?-1:t===n||t.ownerDocument===P&&R(P,t)?1:A?tt.call(A,e)-tt.call(A,t):0:4&r?-1:1)}:function(e,t){if(e===t)return D=!0,0;var r,i=0,o=e.parentNode,s=t.parentNode,l=[e],u=[t];if(!o||!s)return e===n?-1:t===n?1:o?-1:s?1:A?tt.call(A,e)-tt.call(A,t):0;if(o===s)return a(e,t);for(r=e;r=r.parentNode;)l.unshift(r);for(r=t;r=r.parentNode;)u.unshift(r);for(;l[i]===u[i];)i++;return i?a(l[i],u[i]):l[i]===P?-1:u[i]===P?1:0},n):H},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==H&&L(e),n=n.replace(dt,"='$1']"),!(!w.matchesSelector||!q||M&&M.test(n)||F&&F.test(n)))try{var r=O.call(e,n);if(r||w.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return t(n,H,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==H&&L(e),R(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==H&&L(e);var n=T.attrHandle[t.toLowerCase()],r=n&&Y.call(T.attrHandle,t.toLowerCase())?n(e,t,!q):void 0;return void 0!==r?r:w.attributes||!q?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(D=!w.detectDuplicates,A=!w.sortStable&&e.slice(0),e.sort(U),D){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return A=null,e},E=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=E(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=E(t);return n},T=t.selectors={cacheLength:50,createPseudo:r,match:ht,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(wt,Tt),e[3]=(e[3]||e[4]||e[5]||"").replace(wt,Tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return ht.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&ft.test(n)&&(t=C(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(wt,Tt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=$[e+" "];return t||(t=new RegExp("(^|"+rt+")"+e+"("+rt+"|$)"))&&$(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==V&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:n?(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o+" ").indexOf(r)>-1:"|="===n?o===r||o.slice(0,r.length+1)===r+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,d,f,p,h,m=o!==a?"nextSibling":"previousSibling",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!l&&!s;if(g){if(o){for(;m;){for(d=t;d=d[m];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=m="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?g.firstChild:g.lastChild],a&&y){for(c=g[B]||(g[B]={}),u=c[e]||[],p=u[0]===I&&u[1],f=u[0]===I&&u[2],d=p&&g.childNodes[p];d=++p&&d&&d[m]||(f=p=0)||h.pop();)if(1===d.nodeType&&++f&&d===t){c[e]=[I,p,f];break}}else if(y&&(u=(t[B]||(t[B]={}))[e])&&u[0]===I)f=u[1];else for(;(d=++p&&d&&d[m]||(f=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++f||(y&&((d[B]||(d[B]={}))[e]=[I,f]),d!==t)););return f-=i,f===r||f%r===0&&f/r>=0}}},PSEUDO:function(e,n){var i,o=T.pseudos[e]||T.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[B]?o(n):o.length>1?(i=[e,e,"",n],T.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),a=i.length;a--;)r=tt.call(e,i[a]),e[r]=!(t[r]=i[a])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=N(e.replace(lt,"$1"));return i[B]?r(function(e,t,n,r){for(var o,a=i(e,null,r,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return function(t){return(t.textContent||t.innerText||E(t)).indexOf(e)>-1}}),lang:r(function(e){return pt.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(wt,Tt).toLowerCase(),function(t){var n;do if(n=q?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===_},focus:function(e){return e===H.activeElement&&(!H.hasFocus||H.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.pseudos.empty(e)},header:function(e){return gt.test(e.nodeName)},input:function(e){return mt.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:u(function(){return[0]}),last:u(function(e,t){return[t-1]}),eq:u(function(e,t,n){return[0>n?n+t:n]}),even:u(function(e,t){for(var n=0;t>n;n+=2)e.push(n);return e}),odd:u(function(e,t){for(var n=1;t>n;n+=2)e.push(n);return e}),lt:u(function(e,t,n){for(var r=0>n?n+t:n;--r>=0;)e.push(r);return e}),gt:u(function(e,t,n){for(var r=0>n?n+t:n;++r<t;)e.push(r);return e})}},T.pseudos.nth=T.pseudos.eq;for(x in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})T.pseudos[x]=s(x);for(x in{submit:!0,reset:!0})T.pseudos[x]=l(x);return d.prototype=T.filters=T.pseudos,T.setFilters=new d,C=t.tokenize=function(e,n){var r,i,o,a,s,l,u,c=z[e+" "];if(c)return n?0:c.slice(0);for(s=e,l=[],u=T.preFilter;s;){(!r||(i=ut.exec(s)))&&(i&&(s=s.slice(i[0].length)||s),l.push(o=[])),r=!1,(i=ct.exec(s))&&(r=i.shift(),o.push({value:r,type:i[0].replace(lt," ")}),s=s.slice(r.length));for(a in T.filter)!(i=ht[a].exec(s))||u[a]&&!(i=u[a](i))||(r=i.shift(),o.push({value:r,type:a,matches:i}),s=s.slice(r.length));if(!r)break}return n?s.length:s?t.error(e):z(e,l).slice(0)},N=t.compile=function(e,t){var n,r=[],i=[],o=X[e+" "];if(!o){for(t||(t=C(e)),n=t.length;n--;)o=y(t[n]),o[B]?r.push(o):i.push(o);o=X(e,b(i,r)),o.selector=e}return o},S=t.select=function(e,t,n,r){var i,o,a,s,l,u="function"==typeof e&&e,d=!r&&C(e=u.selector||e);if(n=n||[],1===d.length){if(o=d[0]=d[0].slice(0),o.length>2&&"ID"===(a=o[0]).type&&w.getById&&9===t.nodeType&&q&&T.relative[o[1].type]){if(t=(T.find.ID(a.matches[0].replace(wt,Tt),t)||[])[0],!t)return n;u&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=ht.needsContext.test(e)?0:o.length;i--&&(a=o[i],!T.relative[s=a.type]);)if((l=T.find[s])&&(r=l(a.matches[0].replace(wt,Tt),bt.test(o[0].type)&&c(t.parentNode)||t))){if(o.splice(i,1),e=r.length&&f(o),!e)return Z.apply(n,r),n; +break}}return(u||N(e,d))(r,t,!q,n,bt.test(e)&&c(t.parentNode)||t),n},w.sortStable=B.split("").sort(U).join("")===B,w.detectDuplicates=!!D,L(),w.sortDetached=i(function(e){return 1&e.compareDocumentPosition(H.createElement("div"))}),i(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){return n?void 0:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),w.attributes&&i(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?void 0:e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(nt,function(e,t,n){var r;return n?void 0:e[t]===!0?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(e);it.find=ut,it.expr=ut.selectors,it.expr[":"]=it.expr.pseudos,it.unique=ut.uniqueSort,it.text=ut.getText,it.isXMLDoc=ut.isXML,it.contains=ut.contains;var ct=it.expr.match.needsContext,dt=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,ft=/^.[^:#\[\.,]*$/;it.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?it.find.matchesSelector(r,e)?[r]:[]:it.find.matches(e,it.grep(t,function(e){return 1===e.nodeType}))},it.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(it(e).filter(function(){for(t=0;i>t;t++)if(it.contains(r[t],this))return!0}));for(t=0;i>t;t++)it.find(e,r[t],n);return n=this.pushStack(i>1?it.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(r(this,e||[],!1))},not:function(e){return this.pushStack(r(this,e||[],!0))},is:function(e){return!!r(this,"string"==typeof e&&ct.test(e)?it(e):e||[],!1).length}});var pt,ht=e.document,mt=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,gt=it.fn.init=function(e,t){var n,r;if(!e)return this;if("string"==typeof e){if(n="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:mt.exec(e),!n||!n[1]&&t)return!t||t.jquery?(t||pt).find(e):this.constructor(t).find(e);if(n[1]){if(t=t instanceof it?t[0]:t,it.merge(this,it.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:ht,!0)),dt.test(n[1])&&it.isPlainObject(t))for(n in t)it.isFunction(this[n])?this[n](t[n]):this.attr(n,t[n]);return this}if(r=ht.getElementById(n[2]),r&&r.parentNode){if(r.id!==n[2])return pt.find(e);this.length=1,this[0]=r}return this.context=ht,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):it.isFunction(e)?"undefined"!=typeof pt.ready?pt.ready(e):e(it):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),it.makeArray(e,this))};gt.prototype=it.fn,pt=it(ht);var vt=/^(?:parents|prev(?:Until|All))/,yt={children:!0,contents:!0,next:!0,prev:!0};it.extend({dir:function(e,t,n){for(var r=[],i=e[t];i&&9!==i.nodeType&&(void 0===n||1!==i.nodeType||!it(i).is(n));)1===i.nodeType&&r.push(i),i=i[t];return r},sibling:function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}}),it.fn.extend({has:function(e){var t,n=it(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(it.contains(this,n[t]))return!0})},closest:function(e,t){for(var n,r=0,i=this.length,o=[],a=ct.test(e)||"string"!=typeof e?it(e,t||this.context):0;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&it.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?it.unique(o):o)},index:function(e){return e?"string"==typeof e?it.inArray(this[0],it(e)):it.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(it.unique(it.merge(this.get(),it(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),it.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return it.dir(e,"parentNode")},parentsUntil:function(e,t,n){return it.dir(e,"parentNode",n)},next:function(e){return i(e,"nextSibling")},prev:function(e){return i(e,"previousSibling")},nextAll:function(e){return it.dir(e,"nextSibling")},prevAll:function(e){return it.dir(e,"previousSibling")},nextUntil:function(e,t,n){return it.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return it.dir(e,"previousSibling",n)},siblings:function(e){return it.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return it.sibling(e.firstChild)},contents:function(e){return it.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:it.merge([],e.childNodes)}},function(e,t){it.fn[e]=function(n,r){var i=it.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=it.filter(r,i)),this.length>1&&(yt[e]||(i=it.unique(i)),vt.test(e)&&(i=i.reverse())),this.pushStack(i)}});var bt=/\S+/g,xt={};it.Callbacks=function(e){e="string"==typeof e?xt[e]||o(e):it.extend({},e);var t,n,r,i,a,s,l=[],u=!e.once&&[],c=function(o){for(n=e.memory&&o,r=!0,a=s||0,s=0,i=l.length,t=!0;l&&i>a;a++)if(l[a].apply(o[0],o[1])===!1&&e.stopOnFalse){n=!1;break}t=!1,l&&(u?u.length&&c(u.shift()):n?l=[]:d.disable())},d={add:function(){if(l){var r=l.length;!function o(t){it.each(t,function(t,n){var r=it.type(n);"function"===r?e.unique&&d.has(n)||l.push(n):n&&n.length&&"string"!==r&&o(n)})}(arguments),t?i=l.length:n&&(s=r,c(n))}return this},remove:function(){return l&&it.each(arguments,function(e,n){for(var r;(r=it.inArray(n,l,r))>-1;)l.splice(r,1),t&&(i>=r&&i--,a>=r&&a--)}),this},has:function(e){return e?it.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],i=0,this},disable:function(){return l=u=n=void 0,this},disabled:function(){return!l},lock:function(){return u=void 0,n||d.disable(),this},locked:function(){return!u},fireWith:function(e,n){return!l||r&&!u||(n=n||[],n=[e,n.slice?n.slice():n],t?u.push(n):c(n)),this},fire:function(){return d.fireWith(this,arguments),this},fired:function(){return!!r}};return d},it.extend({Deferred:function(e){var t=[["resolve","done",it.Callbacks("once memory"),"resolved"],["reject","fail",it.Callbacks("once memory"),"rejected"],["notify","progress",it.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return it.Deferred(function(n){it.each(t,function(t,o){var a=it.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&it.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?it.extend(e,r):r}},i={};return r.pipe=r.then,it.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,i=0,o=Y.call(arguments),a=o.length,s=1!==a||e&&it.isFunction(e.promise)?a:0,l=1===s?e:it.Deferred(),u=function(e,n,r){return function(i){n[e]=this,r[e]=arguments.length>1?Y.call(arguments):i,r===t?l.notifyWith(n,r):--s||l.resolveWith(n,r)}};if(a>1)for(t=new Array(a),n=new Array(a),r=new Array(a);a>i;i++)o[i]&&it.isFunction(o[i].promise)?o[i].promise().done(u(i,r,o)).fail(l.reject).progress(u(i,n,t)):--s;return s||l.resolveWith(r,o),l.promise()}});var wt;it.fn.ready=function(e){return it.ready.promise().done(e),this},it.extend({isReady:!1,readyWait:1,holdReady:function(e){e?it.readyWait++:it.ready(!0)},ready:function(e){if(e===!0?!--it.readyWait:!it.isReady){if(!ht.body)return setTimeout(it.ready);it.isReady=!0,e!==!0&&--it.readyWait>0||(wt.resolveWith(ht,[it]),it.fn.triggerHandler&&(it(ht).triggerHandler("ready"),it(ht).off("ready")))}}}),it.ready.promise=function(t){if(!wt)if(wt=it.Deferred(),"complete"===ht.readyState)setTimeout(it.ready);else if(ht.addEventListener)ht.addEventListener("DOMContentLoaded",s,!1),e.addEventListener("load",s,!1);else{ht.attachEvent("onreadystatechange",s),e.attachEvent("onload",s);var n=!1;try{n=null==e.frameElement&&ht.documentElement}catch(r){}n&&n.doScroll&&!function i(){if(!it.isReady){try{n.doScroll("left")}catch(e){return setTimeout(i,50)}a(),it.ready()}}()}return wt.promise(t)};var Tt,Et="undefined";for(Tt in it(nt))break;nt.ownLast="0"!==Tt,nt.inlineBlockNeedsLayout=!1,it(function(){var e,t,n,r;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",nt.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(r))}),function(){var e=ht.createElement("div");if(null==nt.deleteExpando){nt.deleteExpando=!0;try{delete e.test}catch(t){nt.deleteExpando=!1}}e=null}(),it.acceptData=function(e){var t=it.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return 1!==n&&9!==n?!1:!t||t!==!0&&e.getAttribute("classid")===t};var kt=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Ct=/([A-Z])/g;it.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?it.cache[e[it.expando]]:e[it.expando],!!e&&!u(e)},data:function(e,t,n){return c(e,t,n)},removeData:function(e,t){return d(e,t)},_data:function(e,t,n){return c(e,t,n,!0)},_removeData:function(e,t){return d(e,t,!0)}}),it.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=it.data(o),1===o.nodeType&&!it._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&(r=a[n].name,0===r.indexOf("data-")&&(r=it.camelCase(r.slice(5)),l(o,r,i[r])));it._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){it.data(this,e)}):arguments.length>1?this.each(function(){it.data(this,e,t)}):o?l(o,e,it.data(o,e)):void 0},removeData:function(e){return this.each(function(){it.removeData(this,e)})}}),it.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=it._data(e,t),n&&(!r||it.isArray(n)?r=it._data(e,t,it.makeArray(n)):r.push(n)),r||[]):void 0},dequeue:function(e,t){t=t||"fx";var n=it.queue(e,t),r=n.length,i=n.shift(),o=it._queueHooks(e,t),a=function(){it.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return it._data(e,n)||it._data(e,n,{empty:it.Callbacks("once memory").add(function(){it._removeData(e,t+"queue"),it._removeData(e,n)})})}}),it.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?it.queue(this[0],e):void 0===t?this:this.each(function(){var n=it.queue(this,e,t);it._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&it.dequeue(this,e)})},dequeue:function(e){return this.each(function(){it.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=it.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};for("string"!=typeof e&&(t=e,e=void 0),e=e||"fx";a--;)n=it._data(o[a],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var Nt=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,St=["Top","Right","Bottom","Left"],jt=function(e,t){return e=t||e,"none"===it.css(e,"display")||!it.contains(e.ownerDocument,e)},At=it.access=function(e,t,n,r,i,o,a){var s=0,l=e.length,u=null==n;if("object"===it.type(n)){i=!0;for(s in n)it.access(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,it.isFunction(r)||(a=!0),u&&(a?(t.call(e,r),t=null):(u=t,t=function(e,t,n){return u.call(it(e),n)})),t))for(;l>s;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:u?t.call(e):l?t(e[0],n):o},Dt=/^(?:checkbox|radio)$/i;!function(){var e=ht.createElement("input"),t=ht.createElement("div"),n=ht.createDocumentFragment();if(t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",nt.leadingWhitespace=3===t.firstChild.nodeType,nt.tbody=!t.getElementsByTagName("tbody").length,nt.htmlSerialize=!!t.getElementsByTagName("link").length,nt.html5Clone="<:nav></:nav>"!==ht.createElement("nav").cloneNode(!0).outerHTML,e.type="checkbox",e.checked=!0,n.appendChild(e),nt.appendChecked=e.checked,t.innerHTML="<textarea>x</textarea>",nt.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,n.appendChild(t),t.innerHTML="<input type='radio' checked='checked' name='t'/>",nt.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,nt.noCloneEvent=!0,t.attachEvent&&(t.attachEvent("onclick",function(){nt.noCloneEvent=!1}),t.cloneNode(!0).click()),null==nt.deleteExpando){nt.deleteExpando=!0;try{delete t.test}catch(r){nt.deleteExpando=!1}}}(),function(){var t,n,r=ht.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(nt[t+"Bubbles"]=n in e)||(r.setAttribute(n,"t"),nt[t+"Bubbles"]=r.attributes[n].expando===!1);r=null}();var Lt=/^(?:input|select|textarea)$/i,Ht=/^key/,_t=/^(?:mouse|pointer|contextmenu)|click/,qt=/^(?:focusinfocus|focusoutblur)$/,Ft=/^([^.]*)(?:\.(.+)|)$/;it.event={global:{},add:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it._data(e);if(g){for(n.handler&&(l=n,n=l.handler,i=l.selector),n.guid||(n.guid=it.guid++),(a=g.events)||(a=g.events={}),(c=g.handle)||(c=g.handle=function(e){return typeof it===Et||e&&it.event.triggered===e.type?void 0:it.event.dispatch.apply(c.elem,arguments)},c.elem=e),t=(t||"").match(bt)||[""],s=t.length;s--;)o=Ft.exec(t[s])||[],p=m=o[1],h=(o[2]||"").split(".").sort(),p&&(u=it.event.special[p]||{},p=(i?u.delegateType:u.bindType)||p,u=it.event.special[p]||{},d=it.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&it.expr.match.needsContext.test(i),namespace:h.join(".")},l),(f=a[p])||(f=a[p]=[],f.delegateCount=0,u.setup&&u.setup.call(e,r,h,c)!==!1||(e.addEventListener?e.addEventListener(p,c,!1):e.attachEvent&&e.attachEvent("on"+p,c))),u.add&&(u.add.call(e,d),d.handler.guid||(d.handler.guid=n.guid)),i?f.splice(f.delegateCount++,0,d):f.push(d),it.event.global[p]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it.hasData(e)&&it._data(e);if(g&&(c=g.events)){for(t=(t||"").match(bt)||[""],u=t.length;u--;)if(s=Ft.exec(t[u])||[],p=m=s[1],h=(s[2]||"").split(".").sort(),p){for(d=it.event.special[p]||{},p=(r?d.delegateType:d.bindType)||p,f=c[p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;o--;)a=f[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,d.remove&&d.remove.call(e,a));l&&!f.length&&(d.teardown&&d.teardown.call(e,h,g.handle)!==!1||it.removeEvent(e,p,g.handle),delete c[p])}else for(p in c)it.event.remove(e,p+t[u],n,r,!0);it.isEmptyObject(c)&&(delete g.handle,it._removeData(e,"events"))}},trigger:function(t,n,r,i){var o,a,s,l,u,c,d,f=[r||ht],p=tt.call(t,"type")?t.type:t,h=tt.call(t,"namespace")?t.namespace.split("."):[];if(s=c=r=r||ht,3!==r.nodeType&&8!==r.nodeType&&!qt.test(p+it.event.triggered)&&(p.indexOf(".")>=0&&(h=p.split("."),p=h.shift(),h.sort()),a=p.indexOf(":")<0&&"on"+p,t=t[it.expando]?t:new it.Event(p,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=h.join("."),t.namespace_re=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:it.makeArray(n,[t]),u=it.event.special[p]||{},i||!u.trigger||u.trigger.apply(r,n)!==!1)){if(!i&&!u.noBubble&&!it.isWindow(r)){for(l=u.delegateType||p,qt.test(l+p)||(s=s.parentNode);s;s=s.parentNode)f.push(s),c=s;c===(r.ownerDocument||ht)&&f.push(c.defaultView||c.parentWindow||e)}for(d=0;(s=f[d++])&&!t.isPropagationStopped();)t.type=d>1?l:u.bindType||p,o=(it._data(s,"events")||{})[t.type]&&it._data(s,"handle"),o&&o.apply(s,n),o=a&&s[a],o&&o.apply&&it.acceptData(s)&&(t.result=o.apply(s,n),t.result===!1&&t.preventDefault());if(t.type=p,!i&&!t.isDefaultPrevented()&&(!u._default||u._default.apply(f.pop(),n)===!1)&&it.acceptData(r)&&a&&r[p]&&!it.isWindow(r)){c=r[a],c&&(r[a]=null),it.event.triggered=p;try{r[p]()}catch(m){}it.event.triggered=void 0,c&&(r[a]=c)}return t.result}},dispatch:function(e){e=it.event.fix(e);var t,n,r,i,o,a=[],s=Y.call(arguments),l=(it._data(this,"events")||{})[e.type]||[],u=it.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!u.preDispatch||u.preDispatch.call(this,e)!==!1){for(a=it.event.handlers.call(this,e,l),t=0;(i=a[t++])&&!e.isPropagationStopped();)for(e.currentTarget=i.elem,o=0;(r=i.handlers[o++])&&!e.isImmediatePropagationStopped();)(!e.namespace_re||e.namespace_re.test(r.namespace))&&(e.handleObj=r,e.data=r.data,n=((it.event.special[r.origType]||{}).handle||r.handler).apply(i.elem,s),void 0!==n&&(e.result=n)===!1&&(e.preventDefault(),e.stopPropagation()));return u.postDispatch&&u.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,l=e.target;if(s&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(i=[],o=0;s>o;o++)r=t[o],n=r.selector+" ",void 0===i[n]&&(i[n]=r.needsContext?it(n,this).index(l)>=0:it.find(n,this,null,[l]).length),i[n]&&i.push(r);i.length&&a.push({elem:l,handlers:i})}return s<t.length&&a.push({elem:this,handlers:t.slice(s)}),a},fix:function(e){if(e[it.expando])return e;var t,n,r,i=e.type,o=e,a=this.fixHooks[i];for(a||(this.fixHooks[i]=a=_t.test(i)?this.mouseHooks:Ht.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new it.Event(o),t=r.length;t--;)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||ht),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,a.filter?a.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,o=t.button,a=t.fromElement;return null==e.pageX&&null!=t.clientX&&(r=e.target.ownerDocument||ht,i=r.documentElement,n=r.body,e.pageX=t.clientX+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0),e.pageY=t.clientY+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?t.toElement:a),e.which||void 0===o||(e.which=1&o?1:2&o?3:4&o?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==h()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===h()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return it.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(e){return it.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=it.extend(new it.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?it.event.trigger(i,null,t):it.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},it.removeEvent=ht.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===Et&&(e[r]=null),e.detachEvent(r,n))},it.Event=function(e,t){return this instanceof it.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&e.returnValue===!1?f:p):this.type=e,t&&it.extend(this,t),this.timeStamp=e&&e.timeStamp||it.now(),void(this[it.expando]=!0)):new it.Event(e,t)},it.Event.prototype={isDefaultPrevented:p,isPropagationStopped:p,isImmediatePropagationStopped:p,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=f,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=f,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=f,e&&e.stopImmediatePropagation&&e.stopImmediatePropagation(),this.stopPropagation()}},it.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){it.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!it.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),nt.submitBubbles||(it.event.special.submit={setup:function(){return it.nodeName(this,"form")?!1:void it.event.add(this,"click._submit keypress._submit",function(e){var t=e.target,n=it.nodeName(t,"input")||it.nodeName(t,"button")?t.form:void 0;n&&!it._data(n,"submitBubbles")&&(it.event.add(n,"submit._submit",function(e){e._submit_bubble=!0}),it._data(n,"submitBubbles",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&it.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return it.nodeName(this,"form")?!1:void it.event.remove(this,"._submit")}}),nt.changeBubbles||(it.event.special.change={setup:function(){return Lt.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(it.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),it.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),it.event.simulate("change",this,e,!0)})),!1):void it.event.add(this,"beforeactivate._change",function(e){var t=e.target;Lt.test(t.nodeName)&&!it._data(t,"changeBubbles")&&(it.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||it.event.simulate("change",this.parentNode,e,!0)}),it._data(t,"changeBubbles",!0))})},handle:function(e){var t=e.target;return this!==t||e.isSimulated||e.isTrigger||"radio"!==t.type&&"checkbox"!==t.type?e.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return it.event.remove(this,"._change"),!Lt.test(this.nodeName)}}),nt.focusinBubbles||it.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){it.event.simulate(t,e.target,it.event.fix(e),!0)};it.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=it._data(r,t);i||r.addEventListener(e,n,!0),it._data(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=it._data(r,t)-1;i?it._data(r,t,i):(r.removeEventListener(e,n,!0),it._removeData(r,t))}}}),it.fn.extend({on:function(e,t,n,r,i){var o,a;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=void 0);for(o in e)this.on(o,t,n,e[o],i);return this}if(null==n&&null==r?(r=t,n=t=void 0):null==r&&("string"==typeof t?(r=n,n=void 0):(r=n,n=t,t=void 0)),r===!1)r=p;else if(!r)return this;return 1===i&&(a=r,r=function(e){return it().off(e),a.apply(this,arguments)},r.guid=a.guid||(a.guid=it.guid++)),this.each(function(){it.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,it(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=void 0),n===!1&&(n=p),this.each(function(){it.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){it.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?it.event.trigger(e,t,n,!0):void 0}});var Mt="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",Ot=/ jQuery\d+="(?:null|\d+)"/g,Rt=new RegExp("<(?:"+Mt+")[\\s/>]","i"),Bt=/^\s+/,Pt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,It=/<([\w:]+)/,Wt=/<tbody/i,$t=/<|&#?\w+;/,zt=/<(?:script|style|link)/i,Xt=/checked\s*(?:[^=]|=\s*.checked.)/i,Ut=/^$|\/(?:java|ecma)script/i,Vt=/^true\/(.*)/,Gt=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Yt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:nt.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},Jt=m(ht),Kt=Jt.appendChild(ht.createElement("div"));Yt.optgroup=Yt.option,Yt.tbody=Yt.tfoot=Yt.colgroup=Yt.caption=Yt.thead,Yt.th=Yt.td,it.extend({clone:function(e,t,n){var r,i,o,a,s,l=it.contains(e.ownerDocument,e);if(nt.html5Clone||it.isXMLDoc(e)||!Rt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Kt.innerHTML=e.outerHTML,Kt.removeChild(o=Kt.firstChild)),!(nt.noCloneEvent&&nt.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||it.isXMLDoc(e)))for(r=g(o),s=g(e),a=0;null!=(i=s[a]);++a)r[a]&&E(i,r[a]);if(t)if(n)for(s=s||g(e),r=r||g(o),a=0;null!=(i=s[a]);a++)T(i,r[a]);else T(e,o);return r=g(o,"script"),r.length>0&&w(r,!l&&g(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){for(var i,o,a,s,l,u,c,d=e.length,f=m(t),p=[],h=0;d>h;h++)if(o=e[h],o||0===o)if("object"===it.type(o))it.merge(p,o.nodeType?[o]:o);else if($t.test(o)){for(s=s||f.appendChild(t.createElement("div")),l=(It.exec(o)||["",""])[1].toLowerCase(),c=Yt[l]||Yt._default,s.innerHTML=c[1]+o.replace(Pt,"<$1></$2>")+c[2],i=c[0];i--;)s=s.lastChild;if(!nt.leadingWhitespace&&Bt.test(o)&&p.push(t.createTextNode(Bt.exec(o)[0])),!nt.tbody)for(o="table"!==l||Wt.test(o)?"<table>"!==c[1]||Wt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;i--;)it.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u);for(it.merge(p,s.childNodes),s.textContent="";s.firstChild;)s.removeChild(s.firstChild);s=f.lastChild}else p.push(t.createTextNode(o));for(s&&f.removeChild(s),nt.appendChecked||it.grep(g(p,"input"),v),h=0;o=p[h++];)if((!r||-1===it.inArray(o,r))&&(a=it.contains(o.ownerDocument,o),s=g(f.appendChild(o),"script"),a&&w(s),n))for(i=0;o=s[i++];)Ut.test(o.type||"")&&n.push(o);return s=null,f},cleanData:function(e,t){for(var n,r,i,o,a=0,s=it.expando,l=it.cache,u=nt.deleteExpando,c=it.event.special;null!=(n=e[a]);a++)if((t||it.acceptData(n))&&(i=n[s],o=i&&l[i])){if(o.events)for(r in o.events)c[r]?it.event.remove(n,r):it.removeEvent(n,r,o.handle);l[i]&&(delete l[i],u?delete n[s]:typeof n.removeAttribute!==Et?n.removeAttribute(s):n[s]=null,G.push(i))}}}),it.fn.extend({text:function(e){return At(this,function(e){return void 0===e?it.text(this):this.empty().append((this[0]&&this[0].ownerDocument||ht).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){for(var n,r=e?it.filter(e,this):this,i=0;null!=(n=r[i]);i++)t||1!==n.nodeType||it.cleanData(g(n)),n.parentNode&&(t&&it.contains(n.ownerDocument,n)&&w(g(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&it.cleanData(g(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&it.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return it.clone(this,e,t)})},html:function(e){return At(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(Ot,""):void 0;if(!("string"!=typeof e||zt.test(e)||!nt.htmlSerialize&&Rt.test(e)||!nt.leadingWhitespace&&Bt.test(e)||Yt[(It.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(Pt,"<$1></$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(it.cleanData(g(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=arguments[0];return this.domManip(arguments,function(t){e=this.parentNode,it.cleanData(g(this)),e&&e.replaceChild(t,this)}),e&&(e.length||e.nodeType)?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t){e=J.apply([],e);var n,r,i,o,a,s,l=0,u=this.length,c=this,d=u-1,f=e[0],p=it.isFunction(f);if(p||u>1&&"string"==typeof f&&!nt.checkClone&&Xt.test(f))return this.each(function(n){var r=c.eq(n);p&&(e[0]=f.call(this,n,r.html())),r.domManip(e,t)});if(u&&(s=it.buildFragment(e,this[0].ownerDocument,!1,this),n=s.firstChild,1===s.childNodes.length&&(s=n),n)){for(o=it.map(g(s,"script"),b),i=o.length;u>l;l++)r=s,l!==d&&(r=it.clone(r,!0,!0),i&&it.merge(o,g(r,"script"))),t.call(this[l],r,l);if(i)for(a=o[o.length-1].ownerDocument,it.map(o,x),l=0;i>l;l++)r=o[l],Ut.test(r.type||"")&&!it._data(r,"globalEval")&&it.contains(a,r)&&(r.src?it._evalUrl&&it._evalUrl(r.src):it.globalEval((r.text||r.textContent||r.innerHTML||"").replace(Gt,"")));s=n=null}return this}}),it.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){it.fn[e]=function(e){for(var n,r=0,i=[],o=it(e),a=o.length-1;a>=r;r++)n=r===a?this:this.clone(!0),it(o[r])[t](n),K.apply(i,n.get());return this.pushStack(i)}});var Qt,Zt={};!function(){var e;nt.shrinkWrapBlocks=function(){if(null!=e)return e;e=!1;var t,n,r;return n=ht.getElementsByTagName("body")[0],n&&n.style?(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",t.appendChild(ht.createElement("div")).style.width="5px",e=3!==t.offsetWidth),n.removeChild(r),e):void 0}}();var en,tn,nn=/^margin/,rn=new RegExp("^("+Nt+")(?!px)[a-z%]+$","i"),on=/^(top|right|bottom|left)$/;e.getComputedStyle?(en=function(e){return e.ownerDocument.defaultView.getComputedStyle(e,null)},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n.getPropertyValue(t)||n[t]:void 0,n&&(""!==a||it.contains(e.ownerDocument,e)||(a=it.style(e,t)),rn.test(a)&&nn.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0===a?a:a+""}):ht.documentElement.currentStyle&&(en=function(e){return e.currentStyle},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n[t]:void 0,null==a&&s&&s[t]&&(a=s[t]),rn.test(a)&&!on.test(t)&&(r=s.left,i=e.runtimeStyle,o=i&&i.left,o&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"}),function(){function t(){var t,n,r,i;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),t.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",o=a=!1,l=!0,e.getComputedStyle&&(o="1%"!==(e.getComputedStyle(t,null)||{}).top,a="4px"===(e.getComputedStyle(t,null)||{width:"4px"}).width,i=t.appendChild(ht.createElement("div")),i.style.cssText=t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",t.style.width="1px",l=!parseFloat((e.getComputedStyle(i,null)||{}).marginRight)),t.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=t.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",s=0===i[0].offsetHeight,s&&(i[0].style.display="",i[1].style.display="none",s=0===i[0].offsetHeight),n.removeChild(r))}var n,r,i,o,a,s,l;n=ht.createElement("div"),n.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",i=n.getElementsByTagName("a")[0],r=i&&i.style,r&&(r.cssText="float:left;opacity:.5",nt.opacity="0.5"===r.opacity,nt.cssFloat=!!r.cssFloat,n.style.backgroundClip="content-box",n.cloneNode(!0).style.backgroundClip="",nt.clearCloneStyle="content-box"===n.style.backgroundClip,nt.boxSizing=""===r.boxSizing||""===r.MozBoxSizing||""===r.WebkitBoxSizing,it.extend(nt,{reliableHiddenOffsets:function(){return null==s&&t(),s +},boxSizingReliable:function(){return null==a&&t(),a},pixelPosition:function(){return null==o&&t(),o},reliableMarginRight:function(){return null==l&&t(),l}}))}(),it.swap=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};var an=/alpha\([^)]*\)/i,sn=/opacity\s*=\s*([^)]*)/,ln=/^(none|table(?!-c[ea]).+)/,un=new RegExp("^("+Nt+")(.*)$","i"),cn=new RegExp("^([+-])=("+Nt+")","i"),dn={position:"absolute",visibility:"hidden",display:"block"},fn={letterSpacing:"0",fontWeight:"400"},pn=["Webkit","O","Moz","ms"];it.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=tn(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":nt.cssFloat?"cssFloat":"styleFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=it.camelCase(t),l=e.style;if(t=it.cssProps[s]||(it.cssProps[s]=S(l,s)),a=it.cssHooks[t]||it.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];if(o=typeof n,"string"===o&&(i=cn.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(it.css(e,t)),o="number"),null!=n&&n===n&&("number"!==o||it.cssNumber[s]||(n+="px"),nt.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),!(a&&"set"in a&&void 0===(n=a.set(e,n,r)))))try{l[t]=n}catch(u){}}},css:function(e,t,n,r){var i,o,a,s=it.camelCase(t);return t=it.cssProps[s]||(it.cssProps[s]=S(e.style,s)),a=it.cssHooks[t]||it.cssHooks[s],a&&"get"in a&&(o=a.get(e,!0,n)),void 0===o&&(o=tn(e,t,r)),"normal"===o&&t in fn&&(o=fn[t]),""===n||n?(i=parseFloat(o),n===!0||it.isNumeric(i)?i||0:o):o}}),it.each(["height","width"],function(e,t){it.cssHooks[t]={get:function(e,n,r){return n?ln.test(it.css(e,"display"))&&0===e.offsetWidth?it.swap(e,dn,function(){return L(e,t,r)}):L(e,t,r):void 0},set:function(e,n,r){var i=r&&en(e);return A(e,n,r?D(e,t,r,nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,i),i):0)}}}),nt.opacity||(it.cssHooks.opacity={get:function(e,t){return sn.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=it.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===it.trim(o.replace(an,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=an.test(o)?o.replace(an,i):o+" "+i)}}),it.cssHooks.marginRight=N(nt.reliableMarginRight,function(e,t){return t?it.swap(e,{display:"inline-block"},tn,[e,"marginRight"]):void 0}),it.each({margin:"",padding:"",border:"Width"},function(e,t){it.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];4>r;r++)i[e+St[r]+t]=o[r]||o[r-2]||o[0];return i}},nn.test(e)||(it.cssHooks[e+t].set=A)}),it.fn.extend({css:function(e,t){return At(this,function(e,t,n){var r,i,o={},a=0;if(it.isArray(t)){for(r=en(e),i=t.length;i>a;a++)o[t[a]]=it.css(e,t[a],!1,r);return o}return void 0!==n?it.style(e,t,n):it.css(e,t)},e,t,arguments.length>1)},show:function(){return j(this,!0)},hide:function(){return j(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){jt(this)?it(this).show():it(this).hide()})}}),it.Tween=H,H.prototype={constructor:H,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(it.cssNumber[n]?"":"px")},cur:function(){var e=H.propHooks[this.prop];return e&&e.get?e.get(this):H.propHooks._default.get(this)},run:function(e){var t,n=H.propHooks[this.prop];return this.pos=t=this.options.duration?it.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):H.propHooks._default.set(this),this}},H.prototype.init.prototype=H.prototype,H.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=it.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){it.fx.step[e.prop]?it.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[it.cssProps[e.prop]]||it.cssHooks[e.prop])?it.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},H.propHooks.scrollTop=H.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},it.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},it.fx=H.prototype.init,it.fx.step={};var hn,mn,gn=/^(?:toggle|show|hide)$/,vn=new RegExp("^(?:([+-])=|)("+Nt+")([a-z%]*)$","i"),yn=/queueHooks$/,bn=[M],xn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=vn.exec(t),o=i&&i[3]||(it.cssNumber[e]?"":"px"),a=(it.cssNumber[e]||"px"!==o&&+r)&&vn.exec(it.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,it.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};it.Animation=it.extend(R,{tweener:function(e,t){it.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");for(var n,r=0,i=e.length;i>r;r++)n=e[r],xn[n]=xn[n]||[],xn[n].unshift(t)},prefilter:function(e,t){t?bn.unshift(e):bn.push(e)}}),it.speed=function(e,t,n){var r=e&&"object"==typeof e?it.extend({},e):{complete:n||!n&&t||it.isFunction(e)&&e,duration:e,easing:n&&t||t&&!it.isFunction(t)&&t};return r.duration=it.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in it.fx.speeds?it.fx.speeds[r.duration]:it.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){it.isFunction(r.old)&&r.old.call(this),r.queue&&it.dequeue(this,r.queue)},r},it.fn.extend({fadeTo:function(e,t,n,r){return this.filter(jt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=it.isEmptyObject(e),o=it.speed(t,n,r),a=function(){var t=R(this,it.extend({},e),o);(i||it._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=it.timers,a=it._data(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&yn.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));(t||!n)&&it.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=it._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=it.timers,a=r?r.length:0;for(n.finish=!0,it.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),it.each(["toggle","show","hide"],function(e,t){var n=it.fn[t];it.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(q(t,!0),e,r,i)}}),it.each({slideDown:q("show"),slideUp:q("hide"),slideToggle:q("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){it.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),it.timers=[],it.fx.tick=function(){var e,t=it.timers,n=0;for(hn=it.now();n<t.length;n++)e=t[n],e()||t[n]!==e||t.splice(n--,1);t.length||it.fx.stop(),hn=void 0},it.fx.timer=function(e){it.timers.push(e),e()?it.fx.start():it.timers.pop()},it.fx.interval=13,it.fx.start=function(){mn||(mn=setInterval(it.fx.tick,it.fx.interval))},it.fx.stop=function(){clearInterval(mn),mn=null},it.fx.speeds={slow:600,fast:200,_default:400},it.fn.delay=function(e,t){return e=it.fx?it.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},function(){var e,t,n,r,i;t=ht.createElement("div"),t.setAttribute("className","t"),t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",r=t.getElementsByTagName("a")[0],n=ht.createElement("select"),i=n.appendChild(ht.createElement("option")),e=t.getElementsByTagName("input")[0],r.style.cssText="top:1px",nt.getSetAttribute="t"!==t.className,nt.style=/top/.test(r.getAttribute("style")),nt.hrefNormalized="/a"===r.getAttribute("href"),nt.checkOn=!!e.value,nt.optSelected=i.selected,nt.enctype=!!ht.createElement("form").enctype,n.disabled=!0,nt.optDisabled=!i.disabled,e=ht.createElement("input"),e.setAttribute("value",""),nt.input=""===e.getAttribute("value"),e.value="t",e.setAttribute("type","radio"),nt.radioValue="t"===e.value}();var wn=/\r/g;it.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=it.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,it(this).val()):e,null==i?i="":"number"==typeof i?i+="":it.isArray(i)&&(i=it.map(i,function(e){return null==e?"":e+""})),t=it.valHooks[this.type]||it.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return t=it.valHooks[i.type]||it.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(wn,""):null==n?"":n)}}}),it.extend({valHooks:{option:{get:function(e){var t=it.find.attr(e,"value");return null!=t?t:it.trim(it.text(e))}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(nt.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&it.nodeName(n.parentNode,"optgroup"))){if(t=it(n).val(),o)return t;a.push(t)}return a},set:function(e,t){for(var n,r,i=e.options,o=it.makeArray(t),a=i.length;a--;)if(r=i[a],it.inArray(it.valHooks.option.get(r),o)>=0)try{r.selected=n=!0}catch(s){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),it.each(["radio","checkbox"],function(){it.valHooks[this]={set:function(e,t){return it.isArray(t)?e.checked=it.inArray(it(e).val(),t)>=0:void 0}},nt.checkOn||(it.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Tn,En,kn=it.expr.attrHandle,Cn=/^(?:checked|selected)$/i,Nn=nt.getSetAttribute,Sn=nt.input;it.fn.extend({attr:function(e,t){return At(this,it.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){it.removeAttr(this,e)})}}),it.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(e&&3!==o&&8!==o&&2!==o)return typeof e.getAttribute===Et?it.prop(e,t,n):(1===o&&it.isXMLDoc(e)||(t=t.toLowerCase(),r=it.attrHooks[t]||(it.expr.match.bool.test(t)?En:Tn)),void 0===n?r&&"get"in r&&null!==(i=r.get(e,t))?i:(i=it.find.attr(e,t),null==i?void 0:i):null!==n?r&&"set"in r&&void 0!==(i=r.set(e,n,t))?i:(e.setAttribute(t,n+""),n):void it.removeAttr(e,t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(bt);if(o&&1===e.nodeType)for(;n=o[i++];)r=it.propFix[n]||n,it.expr.match.bool.test(n)?Sn&&Nn||!Cn.test(n)?e[r]=!1:e[it.camelCase("default-"+n)]=e[r]=!1:it.attr(e,n,""),e.removeAttribute(Nn?n:r)},attrHooks:{type:{set:function(e,t){if(!nt.radioValue&&"radio"===t&&it.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}}}),En={set:function(e,t,n){return t===!1?it.removeAttr(e,n):Sn&&Nn||!Cn.test(n)?e.setAttribute(!Nn&&it.propFix[n]||n,n):e[it.camelCase("default-"+n)]=e[n]=!0,n}},it.each(it.expr.match.bool.source.match(/\w+/g),function(e,t){var n=kn[t]||it.find.attr;kn[t]=Sn&&Nn||!Cn.test(t)?function(e,t,r){var i,o;return r||(o=kn[t],kn[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,kn[t]=o),i}:function(e,t,n){return n?void 0:e[it.camelCase("default-"+t)]?t.toLowerCase():null}}),Sn&&Nn||(it.attrHooks.value={set:function(e,t,n){return it.nodeName(e,"input")?void(e.defaultValue=t):Tn&&Tn.set(e,t,n)}}),Nn||(Tn={set:function(e,t,n){var r=e.getAttributeNode(n);return r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n)?t:void 0}},kn.id=kn.name=kn.coords=function(e,t,n){var r;return n?void 0:(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},it.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:void 0},set:Tn.set},it.attrHooks.contenteditable={set:function(e,t,n){Tn.set(e,""===t?!1:t,n)}},it.each(["width","height"],function(e,t){it.attrHooks[t]={set:function(e,n){return""===n?(e.setAttribute(t,"auto"),n):void 0}}})),nt.style||(it.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var jn=/^(?:input|select|textarea|button|object)$/i,An=/^(?:a|area)$/i;it.fn.extend({prop:function(e,t){return At(this,it.prop,e,t,arguments.length>1)},removeProp:function(e){return e=it.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(t){}})}}),it.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,a=e.nodeType;if(e&&3!==a&&8!==a&&2!==a)return o=1!==a||!it.isXMLDoc(e),o&&(t=it.propFix[t]||t,i=it.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=it.find.attr(e,"tabindex");return t?parseInt(t,10):jn.test(e.nodeName)||An.test(e.nodeName)&&e.href?0:-1}}}}),nt.hrefNormalized||it.each(["href","src"],function(e,t){it.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),nt.optSelected||(it.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),it.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){it.propFix[this.toLowerCase()]=this}),nt.enctype||(it.propFix.enctype="encoding");var Dn=/[\t\r\n\f]/g;it.fn.extend({addClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u="string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):" ")){for(o=0;i=t[o++];)r.indexOf(" "+i+" ")<0&&(r+=i+" ");a=it.trim(r),n.className!==a&&(n.className=a)}return this},removeClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u=0===arguments.length||"string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):"")){for(o=0;i=t[o++];)for(;r.indexOf(" "+i+" ")>=0;)r=r.replace(" "+i+" "," ");a=e?it.trim(r):"",n.className!==a&&(n.className=a)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):this.each(it.isFunction(e)?function(n){it(this).toggleClass(e.call(this,n,this.className,t),t)}:function(){if("string"===n)for(var t,r=0,i=it(this),o=e.match(bt)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else(n===Et||"boolean"===n)&&(this.className&&it._data(this,"__className__",this.className),this.className=this.className||e===!1?"":it._data(this,"__className__")||"")})},hasClass:function(e){for(var t=" "+e+" ",n=0,r=this.length;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(Dn," ").indexOf(t)>=0)return!0;return!1}}),it.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){it.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),it.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var Ln=it.now(),Hn=/\?/,_n=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;it.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=it.trim(t+"");return i&&!it.trim(i.replace(_n,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():it.error("Invalid JSON: "+t)},it.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{e.DOMParser?(r=new DOMParser,n=r.parseFromString(t,"text/xml")):(n=new ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(t))}catch(i){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||it.error("Invalid XML: "+t),n};var qn,Fn,Mn=/#.*$/,On=/([?&])_=[^&]*/,Rn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Bn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Pn=/^(?:GET|HEAD)$/,In=/^\/\//,Wn=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,$n={},zn={},Xn="*/".concat("*");try{Fn=location.href}catch(Un){Fn=ht.createElement("a"),Fn.href="",Fn=Fn.href}qn=Wn.exec(Fn.toLowerCase())||[],it.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Fn,type:"GET",isLocal:Bn.test(qn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Xn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":it.parseJSON,"text xml":it.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?I(I(e,it.ajaxSettings),t):I(it.ajaxSettings,e)},ajaxPrefilter:B($n),ajaxTransport:B(zn),ajax:function(e,t){function n(e,t,n,r){var i,c,v,y,x,T=t;2!==b&&(b=2,s&&clearTimeout(s),u=void 0,a=r||"",w.readyState=e>0?4:0,i=e>=200&&300>e||304===e,n&&(y=W(d,w,n)),y=$(d,y,w,i),i?(d.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(it.lastModified[o]=x),x=w.getResponseHeader("etag"),x&&(it.etag[o]=x)),204===e||"HEAD"===d.type?T="nocontent":304===e?T="notmodified":(T=y.state,c=y.data,v=y.error,i=!v)):(v=T,(e||!T)&&(T="error",0>e&&(e=0))),w.status=e,w.statusText=(t||T)+"",i?h.resolveWith(f,[c,T,w]):h.rejectWith(f,[w,T,v]),w.statusCode(g),g=void 0,l&&p.trigger(i?"ajaxSuccess":"ajaxError",[w,d,i?c:v]),m.fireWith(f,[w,T]),l&&(p.trigger("ajaxComplete",[w,d]),--it.active||it.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,i,o,a,s,l,u,c,d=it.ajaxSetup({},t),f=d.context||d,p=d.context&&(f.nodeType||f.jquery)?it(f):it.event,h=it.Deferred(),m=it.Callbacks("once memory"),g=d.statusCode||{},v={},y={},b=0,x="canceled",w={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=Rn.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=y[n]=y[n]||e,v[e]=t),this},overrideMimeType:function(e){return b||(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)g[t]=[g[t],e[t]];else w.always(e[w.status]);return this},abort:function(e){var t=e||x;return u&&u.abort(t),n(0,t),this}};if(h.promise(w).complete=m.add,w.success=w.done,w.error=w.fail,d.url=((e||d.url||Fn)+"").replace(Mn,"").replace(In,qn[1]+"//"),d.type=t.method||t.type||d.method||d.type,d.dataTypes=it.trim(d.dataType||"*").toLowerCase().match(bt)||[""],null==d.crossDomain&&(r=Wn.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]===qn[1]&&r[2]===qn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(qn[3]||("http:"===qn[1]?"80":"443")))),d.data&&d.processData&&"string"!=typeof d.data&&(d.data=it.param(d.data,d.traditional)),P($n,d,t,w),2===b)return w;l=d.global,l&&0===it.active++&&it.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!Pn.test(d.type),o=d.url,d.hasContent||(d.data&&(o=d.url+=(Hn.test(o)?"&":"?")+d.data,delete d.data),d.cache===!1&&(d.url=On.test(o)?o.replace(On,"$1_="+Ln++):o+(Hn.test(o)?"&":"?")+"_="+Ln++)),d.ifModified&&(it.lastModified[o]&&w.setRequestHeader("If-Modified-Since",it.lastModified[o]),it.etag[o]&&w.setRequestHeader("If-None-Match",it.etag[o])),(d.data&&d.hasContent&&d.contentType!==!1||t.contentType)&&w.setRequestHeader("Content-Type",d.contentType),w.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+Xn+"; q=0.01":""):d.accepts["*"]);for(i in d.headers)w.setRequestHeader(i,d.headers[i]);if(d.beforeSend&&(d.beforeSend.call(f,w,d)===!1||2===b))return w.abort();x="abort";for(i in{success:1,error:1,complete:1})w[i](d[i]);if(u=P(zn,d,t,w)){w.readyState=1,l&&p.trigger("ajaxSend",[w,d]),d.async&&d.timeout>0&&(s=setTimeout(function(){w.abort("timeout")},d.timeout));try{b=1,u.send(v,n)}catch(T){if(!(2>b))throw T;n(-1,T)}}else n(-1,"No Transport");return w},getJSON:function(e,t,n){return it.get(e,t,n,"json")},getScript:function(e,t){return it.get(e,void 0,t,"script")}}),it.each(["get","post"],function(e,t){it[t]=function(e,n,r,i){return it.isFunction(n)&&(i=i||r,r=n,n=void 0),it.ajax({url:e,type:t,dataType:i,data:n,success:r})}}),it.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){it.fn[t]=function(e){return this.on(t,e)}}),it._evalUrl=function(e){return it.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},it.fn.extend({wrapAll:function(e){if(it.isFunction(e))return this.each(function(t){it(this).wrapAll(e.call(this,t))});if(this[0]){var t=it(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return this.each(it.isFunction(e)?function(t){it(this).wrapInner(e.call(this,t))}:function(){var t=it(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=it.isFunction(e);return this.each(function(n){it(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){it.nodeName(this,"body")||it(this).replaceWith(this.childNodes)}).end()}}),it.expr.filters.hidden=function(e){return e.offsetWidth<=0&&e.offsetHeight<=0||!nt.reliableHiddenOffsets()&&"none"===(e.style&&e.style.display||it.css(e,"display"))},it.expr.filters.visible=function(e){return!it.expr.filters.hidden(e)};var Vn=/%20/g,Gn=/\[\]$/,Yn=/\r?\n/g,Jn=/^(?:submit|button|image|reset|file)$/i,Kn=/^(?:input|select|textarea|keygen)/i;it.param=function(e,t){var n,r=[],i=function(e,t){t=it.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=it.ajaxSettings&&it.ajaxSettings.traditional),it.isArray(e)||e.jquery&&!it.isPlainObject(e))it.each(e,function(){i(this.name,this.value)});else for(n in e)z(n,e[n],t,i);return r.join("&").replace(Vn,"+")},it.fn.extend({serialize:function(){return it.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=it.prop(this,"elements");return e?it.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!it(this).is(":disabled")&&Kn.test(this.nodeName)&&!Jn.test(e)&&(this.checked||!Dt.test(e))}).map(function(e,t){var n=it(this).val();return null==n?null:it.isArray(n)?it.map(n,function(e){return{name:t.name,value:e.replace(Yn,"\r\n")}}):{name:t.name,value:n.replace(Yn,"\r\n")}}).get()}}),it.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&X()||U()}:X;var Qn=0,Zn={},er=it.ajaxSettings.xhr();e.ActiveXObject&&it(e).on("unload",function(){for(var e in Zn)Zn[e](void 0,!0)}),nt.cors=!!er&&"withCredentials"in er,er=nt.ajax=!!er,er&&it.ajaxTransport(function(e){if(!e.crossDomain||nt.cors){var t;return{send:function(n,r){var i,o=e.xhr(),a=++Qn;if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)o[i]=e.xhrFields[i];e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest");for(i in n)void 0!==n[i]&&o.setRequestHeader(i,n[i]+"");o.send(e.hasContent&&e.data||null),t=function(n,i){var s,l,u;if(t&&(i||4===o.readyState))if(delete Zn[a],t=void 0,o.onreadystatechange=it.noop,i)4!==o.readyState&&o.abort();else{u={},s=o.status,"string"==typeof o.responseText&&(u.text=o.responseText);try{l=o.statusText}catch(c){l=""}s||!e.isLocal||e.crossDomain?1223===s&&(s=204):s=u.text?200:404}u&&r(s,l,u,o.getAllResponseHeaders())},e.async?4===o.readyState?setTimeout(t):o.onreadystatechange=Zn[a]=t:t()},abort:function(){t&&t(void 0,!0)}}}}),it.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return it.globalEval(e),e}}}),it.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),it.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=ht.head||it("head")[0]||ht.documentElement;return{send:function(r,i){t=ht.createElement("script"),t.async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||i(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var tr=[],nr=/(=)\?(?=&|$)|\?\?/;it.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=tr.pop()||it.expando+"_"+Ln++;return this[e]=!0,e}}),it.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=t.jsonp!==!1&&(nr.test(t.url)?"url":"string"==typeof t.data&&!(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&nr.test(t.data)&&"data");return s||"jsonp"===t.dataTypes[0]?(i=t.jsonpCallback=it.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(nr,"$1"+i):t.jsonp!==!1&&(t.url+=(Hn.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||it.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,tr.push(i)),a&&it.isFunction(o)&&o(a[0]),a=o=void 0}),"script"):void 0}),it.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||ht;var r=dt.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=it.buildFragment([e],t,i),i&&i.length&&it(i).remove(),it.merge([],r.childNodes))};var rr=it.fn.load;it.fn.load=function(e,t,n){if("string"!=typeof e&&rr)return rr.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>=0&&(r=it.trim(e.slice(s,e.length)),e=e.slice(0,s)),it.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(o="POST"),a.length>0&&it.ajax({url:e,type:o,dataType:"html",data:t}).done(function(e){i=arguments,a.html(r?it("<div>").append(it.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){a.each(n,i||[e.responseText,t,e])}),this},it.expr.filters.animated=function(e){return it.grep(it.timers,function(t){return e===t.elem}).length};var ir=e.document.documentElement;it.offset={setOffset:function(e,t,n){var r,i,o,a,s,l,u,c=it.css(e,"position"),d=it(e),f={};"static"===c&&(e.style.position="relative"),s=d.offset(),o=it.css(e,"top"),l=it.css(e,"left"),u=("absolute"===c||"fixed"===c)&&it.inArray("auto",[o,l])>-1,u?(r=d.position(),a=r.top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(l)||0),it.isFunction(t)&&(t=t.call(e,n,s)),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):d.css(f)}},it.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){it.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;if(o)return t=o.documentElement,it.contains(t,i)?(typeof i.getBoundingClientRect!==Et&&(r=i.getBoundingClientRect()),n=V(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===it.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),it.nodeName(e[0],"html")||(n=e.offset()),n.top+=it.css(e[0],"borderTopWidth",!0),n.left+=it.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-it.css(r,"marginTop",!0),left:t.left-n.left-it.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent||ir;e&&!it.nodeName(e,"html")&&"static"===it.css(e,"position");)e=e.offsetParent;return e||ir})}}),it.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);it.fn[e]=function(r){return At(this,function(e,r,i){var o=V(e);return void 0===i?o?t in o?o[t]:o.document.documentElement[r]:e[r]:void(o?o.scrollTo(n?it(o).scrollLeft():i,n?i:it(o).scrollTop()):e[r]=i)},e,r,arguments.length,null)}}),it.each(["top","left"],function(e,t){it.cssHooks[t]=N(nt.pixelPosition,function(e,n){return n?(n=tn(e,t),rn.test(n)?it(e).position()[t]+"px":n):void 0})}),it.each({Height:"height",Width:"width"},function(e,t){it.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){it.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(r===!0||i===!0?"margin":"border");return At(this,function(t,n,r){var i;return it.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?it.css(t,n,a):it.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),it.fn.size=function(){return this.length},it.fn.andSelf=it.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return it});var or=e.jQuery,ar=e.$;return it.noConflict=function(t){return e.$===it&&(e.$=ar),t&&e.jQuery===it&&(e.jQuery=or),it},typeof t===Et&&(e.jQuery=e.$=it),it}),function(e,t){e.rails!==t&&e.error("jquery-ujs has already been loaded!");var n,r=e(document);e.rails=n={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]",buttonClickSelector:"button[data-remote], button[data-confirm]",inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form",formInputClickSelector:"form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])",disableSelector:"input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled",enableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled",requiredInputSelector:"input[name][required]:not([disabled]),textarea[name][required]:not([disabled])",fileInputSelector:"input[type=file]",linkDisableSelector:"a[data-disable-with], a[data-disable]",buttonDisableSelector:"button[data-remote][data-disable-with], button[data-remote][data-disable]",CSRFProtection:function(t){var n=e('meta[name="csrf-token"]').attr("content");n&&t.setRequestHeader("X-CSRF-Token",n)},refreshCSRFTokens:function(){var t=e("meta[name=csrf-token]").attr("content"),n=e("meta[name=csrf-param]").attr("content");e('form input[name="'+n+'"]').val(t)},fire:function(t,n,r){var i=e.Event(n);return t.trigger(i,r),i.result!==!1},confirm:function(e){return confirm(e)},ajax:function(t){return e.ajax(t)},href:function(e){return e.attr("href")},handleRemote:function(r){var i,o,a,s,l,u,c,d;if(n.fire(r,"ajax:before")){if(s=r.data("cross-domain"),l=s===t?null:s,u=r.data("with-credentials")||null,c=r.data("type")||e.ajaxSettings&&e.ajaxSettings.dataType,r.is("form")){i=r.attr("method"),o=r.attr("action"),a=r.serializeArray();var f=r.data("ujs:submit-button");f&&(a.push(f),r.data("ujs:submit-button",null))}else r.is(n.inputChangeSelector)?(i=r.data("method"),o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):r.is(n.buttonClickSelector)?(i=r.data("method")||"get",o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):(i=r.data("method"),o=n.href(r),a=r.data("params")||null);return d={type:i||"GET",data:a,dataType:c,beforeSend:function(e,i){return i.dataType===t&&e.setRequestHeader("accept","*/*;q=0.5, "+i.accepts.script),n.fire(r,"ajax:beforeSend",[e,i])?void r.trigger("ajax:send",e):!1 +},success:function(e,t,n){r.trigger("ajax:success",[e,t,n])},complete:function(e,t){r.trigger("ajax:complete",[e,t])},error:function(e,t,n){r.trigger("ajax:error",[e,t,n])},crossDomain:l},u&&(d.xhrFields={withCredentials:u}),o&&(d.url=o),n.ajax(d)}return!1},handleMethod:function(r){var i=n.href(r),o=r.data("method"),a=r.attr("target"),s=e("meta[name=csrf-token]").attr("content"),l=e("meta[name=csrf-param]").attr("content"),u=e('<form method="post" action="'+i+'"></form>'),c='<input name="_method" value="'+o+'" type="hidden" />';l!==t&&s!==t&&(c+='<input name="'+l+'" value="'+s+'" type="hidden" />'),a&&u.attr("target",a),u.hide().append(c).appendTo("body"),u.submit()},formElements:function(t,n){return t.is("form")?e(t[0].elements).filter(n):t.find(n)},disableFormElements:function(t){n.formElements(t,n.disableSelector).each(function(){n.disableFormElement(e(this))})},disableFormElement:function(e){var n,r;n=e.is("button")?"html":"val",r=e.data("disable-with"),e.data("ujs:enable-with",e[n]()),r!==t&&e[n](r),e.prop("disabled",!0)},enableFormElements:function(t){n.formElements(t,n.enableSelector).each(function(){n.enableFormElement(e(this))})},enableFormElement:function(e){var t=e.is("button")?"html":"val";e.data("ujs:enable-with")&&e[t](e.data("ujs:enable-with")),e.prop("disabled",!1)},allowAction:function(e){var t,r=e.data("confirm"),i=!1;return r?(n.fire(e,"confirm")&&(i=n.confirm(r),t=n.fire(e,"confirm:complete",[i])),i&&t):!0},blankInputs:function(t,n,r){var i,o,a=e(),s=n||"input,textarea",l=t.find(s);return l.each(function(){if(i=e(this),o=i.is("input[type=checkbox],input[type=radio]")?i.is(":checked"):i.val(),!o==!r){if(i.is("input[type=radio]")&&l.filter('input[type=radio]:checked[name="'+i.attr("name")+'"]').length)return!0;a=a.add(i)}}),a.length?a:!1},nonBlankInputs:function(e,t){return n.blankInputs(e,t,!0)},stopEverything:function(t){return e(t.target).trigger("ujs:everythingStopped"),t.stopImmediatePropagation(),!1},disableElement:function(e){var r=e.data("disable-with");e.data("ujs:enable-with",e.html()),r!==t&&e.html(r),e.bind("click.railsDisable",function(e){return n.stopEverything(e)})},enableElement:function(e){e.data("ujs:enable-with")!==t&&(e.html(e.data("ujs:enable-with")),e.removeData("ujs:enable-with")),e.unbind("click.railsDisable")}},n.fire(r,"rails:attachBindings")&&(e.ajaxPrefilter(function(e,t,r){e.crossDomain||n.CSRFProtection(r)}),r.delegate(n.linkDisableSelector,"ajax:complete",function(){n.enableElement(e(this))}),r.delegate(n.buttonDisableSelector,"ajax:complete",function(){n.enableFormElement(e(this))}),r.delegate(n.linkClickSelector,"click.rails",function(r){var i=e(this),o=i.data("method"),a=i.data("params"),s=r.metaKey||r.ctrlKey;if(!n.allowAction(i))return n.stopEverything(r);if(!s&&i.is(n.linkDisableSelector)&&n.disableElement(i),i.data("remote")!==t){if(s&&(!o||"GET"===o)&&!a)return!0;var l=n.handleRemote(i);return l===!1?n.enableElement(i):l.error(function(){n.enableElement(i)}),!1}return i.data("method")?(n.handleMethod(i),!1):void 0}),r.delegate(n.buttonClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);r.is(n.buttonDisableSelector)&&n.disableFormElement(r);var i=n.handleRemote(r);return i===!1?n.enableFormElement(r):i.error(function(){n.enableFormElement(r)}),!1}),r.delegate(n.inputChangeSelector,"change.rails",function(t){var r=e(this);return n.allowAction(r)?(n.handleRemote(r),!1):n.stopEverything(t)}),r.delegate(n.formSubmitSelector,"submit.rails",function(r){var i,o,a=e(this),s=a.data("remote")!==t;if(!n.allowAction(a))return n.stopEverything(r);if(a.attr("novalidate")==t&&(i=n.blankInputs(a,n.requiredInputSelector),i&&n.fire(a,"ajax:aborted:required",[i])))return n.stopEverything(r);if(s){if(o=n.nonBlankInputs(a,n.fileInputSelector)){setTimeout(function(){n.disableFormElements(a)},13);var l=n.fire(a,"ajax:aborted:file",[o]);return l||setTimeout(function(){n.enableFormElements(a)},13),l}return n.handleRemote(a),!1}setTimeout(function(){n.disableFormElements(a)},13)}),r.delegate(n.formInputClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);var i=r.attr("name"),o=i?{name:i,value:r.val()}:null;r.closest("form").data("ujs:submit-button",o)}),r.delegate(n.formSubmitSelector,"ajax:send.rails",function(t){this==t.target&&n.disableFormElements(e(this))}),r.delegate(n.formSubmitSelector,"ajax:complete.rails",function(t){this==t.target&&n.enableFormElements(e(this))}),e(function(){n.refreshCSRFTokens()}))}(jQuery),function(){var e,t,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v,y,b,x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B,P,I,W,$,z,X,U,V,G,Y=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1},J={}.hasOwnProperty,K=function(e,t){function n(){this.constructor=e}for(var r in t)J.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},Q=[].slice;j={},d=10,$=!1,m=null,S=null,q=null,h=null,V=null,b=function(e){var t;return e=new n(e),B(),c(),F(e),$&&(t=z(e.absolute))?(x(t),w(e)):w(e,W)},z=function(e){var t;return t=j[e],t&&!t.transitionCacheDisabled?t:void 0},g=function(e){return null==e&&(e=!0),$=e},w=function(e,t){return null==t&&(t=function(){return function(){}}(this)),X("page:fetch",{url:e.absolute}),null!=V&&V.abort(),V=new XMLHttpRequest,V.open("GET",e.withoutHashForIE10compatibility(),!0),V.setRequestHeader("Accept","text/html, application/xhtml+xml, application/xml"),V.setRequestHeader("X-XHR-Referer",q),V.onload=function(){var n;return X("page:receive"),(n=H())?(f.apply(null,y(n)),M(),t(),X("page:load")):document.location.href=e.absolute},V.onloadend=function(){return V=null},V.onerror=function(){return document.location.href=e.absolute},V.send()},x=function(e){return null!=V&&V.abort(),f(e.title,e.body),_(e),X("page:restore")},c=function(){var e;return e=new n(m.url),j[e.absolute]={url:e.relative,body:document.body,title:document.title,positionY:window.pageYOffset,positionX:window.pageXOffset,cachedAt:(new Date).getTime(),transitionCacheDisabled:null!=document.querySelector("[data-no-transition-cache]")},p(d)},D=function(e){return null==e&&(e=d),/^[\d]+$/.test(e)?d=parseInt(e):void 0},p=function(e){var t,n,r,i,o,a;for(r=Object.keys(j),t=r.map(function(e){return j[e].cachedAt}).sort(function(e,t){return t-e}),a=[],i=0,o=r.length;o>i;i++)n=r[i],j[n].cachedAt<=t[e]&&(X("page:expire",j[n]),a.push(delete j[n]));return a},f=function(t,n,r,i){return document.title=t,document.documentElement.replaceChild(n,document.body),null!=r&&e.update(r),i&&v(),m=window.history.state,X("page:change"),X("page:update")},v=function(){var e,t,n,r,i,o,a,s,l,u,c,d;for(o=Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])')),a=0,l=o.length;l>a;a++)if(i=o[a],""===(c=i.type)||"text/javascript"===c){for(t=document.createElement("script"),d=i.attributes,s=0,u=d.length;u>s;s++)e=d[s],t.setAttribute(e.name,e.value);t.appendChild(document.createTextNode(i.innerHTML)),r=i.parentNode,n=i.nextSibling,r.removeChild(i),r.insertBefore(t,n)}},P=function(e){return e.innerHTML=e.innerHTML.replace(/<noscript[\S\s]*?<\/noscript>/gi,""),e},F=function(e){return(e=new n(e)).absolute!==q?window.history.pushState({turbolinks:!0,url:e.absolute},"",e.absolute):void 0},M=function(){var e,t;return(e=V.getResponseHeader("X-XHR-Redirected-To"))?(e=new n(e),t=e.hasNoHash()?document.location.hash:"",window.history.replaceState(m,"",e.href+t)):void 0},B=function(){return q=document.location.href},R=function(){return window.history.replaceState({turbolinks:!0,url:document.location.href},"",document.location.href)},O=function(){return m=window.history.state},_=function(e){return window.scrollTo(e.positionX,e.positionY)},W=function(){return document.location.hash?document.location.href=document.location.href:window.scrollTo(0,0)},L=function(e){var t,n;return t=(null!=(n=document.cookie.match(new RegExp(e+"=(\\w+)")))?n[1].toUpperCase():void 0)||"",document.cookie=e+"=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/",t},X=function(e,t){var n;return n=document.createEvent("Events"),t&&(n.data=t),n.initEvent(e,!0,!0),document.dispatchEvent(n)},A=function(){return!X("page:before-change")},H=function(){var e,t,n,r,i,o;return t=function(){var e;return 400<=(e=V.status)&&600>e},o=function(){return V.getResponseHeader("Content-Type").match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/)},r=function(e){var t,n,r,i,o;for(i=e.head.childNodes,o=[],n=0,r=i.length;r>n;n++)t=i[n],null!=("function"==typeof t.getAttribute?t.getAttribute("data-turbolinks-track"):void 0)&&o.push(t.getAttribute("src")||t.getAttribute("href"));return o},e=function(e){var t;return S||(S=r(document)),t=r(e),t.length!==S.length||i(t,S).length!==S.length},i=function(e,t){var n,r,i,o,a;for(e.length>t.length&&(o=[t,e],e=o[0],t=o[1]),a=[],r=0,i=e.length;i>r;r++)n=e[r],Y.call(t,n)>=0&&a.push(n);return a},!t()&&o()&&(n=h(V.responseText),n&&!e(n))?n:void 0},y=function(t){var n;return n=t.querySelector("title"),[null!=n?n.textContent:void 0,P(t.body),e.get(t).token,"runScripts"]},e={get:function(e){var t;return null==e&&(e=document),{node:t=e.querySelector('meta[name="csrf-token"]'),token:null!=t&&"function"==typeof t.getAttribute?t.getAttribute("content"):void 0}},update:function(e){var t;return t=this.get(),null!=t.token&&null!=e&&t.token!==e?t.node.setAttribute("content",e):void 0}},i=function(){var e,t,n,r,i,o;t=function(e){return(new DOMParser).parseFromString(e,"text/html")},e=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.documentElement.innerHTML=e,t},n=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.open("replace"),t.write(e),t.close(),t};try{if(window.DOMParser)return i=t("<html><body><p>test"),t}catch(a){return r=a,i=e("<html><body><p>test"),e}finally{if(1!==(null!=i&&null!=(o=i.body)?o.childNodes.length:void 0))return n}},n=function(){function e(t){return this.original=null!=t?t:document.location.href,this.original.constructor===e?this.original:void this._parse()}return e.prototype.withoutHash=function(){return this.href.replace(this.hash,"")},e.prototype.withoutHashForIE10compatibility=function(){return this.withoutHash()},e.prototype.hasNoHash=function(){return 0===this.hash.length},e.prototype._parse=function(){var e;return(null!=this.link?this.link:this.link=document.createElement("a")).href=this.original,e=this.link,this.href=e.href,this.protocol=e.protocol,this.host=e.host,this.hostname=e.hostname,this.port=e.port,this.pathname=e.pathname,this.search=e.search,this.hash=e.hash,this.origin=[this.protocol,"//",this.hostname].join(""),0!==this.port.length&&(this.origin+=":"+this.port),this.relative=[this.pathname,this.search,this.hash].join(""),this.absolute=this.href},e}(),r=function(e){function t(e){return this.link=e,this.link.constructor===t?this.link:(this.original=this.link.href,void t.__super__.constructor.apply(this,arguments))}return K(t,e),t.HTML_EXTENSIONS=["html"],t.allowExtensions=function(){var e,n,r,i;for(n=1<=arguments.length?Q.call(arguments,0):[],r=0,i=n.length;i>r;r++)e=n[r],t.HTML_EXTENSIONS.push(e);return t.HTML_EXTENSIONS},t.prototype.shouldIgnore=function(){return this._crossOrigin()||this._anchored()||this._nonHtml()||this._optOut()||this._target()},t.prototype._crossOrigin=function(){return this.origin!==(new n).origin},t.prototype._anchored=function(){var e;return(this.hash&&this.withoutHash())===(e=new n).withoutHash()||this.href===e.href+"#"},t.prototype._nonHtml=function(){return this.pathname.match(/\.[a-z]+$/g)&&!this.pathname.match(new RegExp("\\.(?:"+t.HTML_EXTENSIONS.join("|")+")?$","g"))},t.prototype._optOut=function(){var e,t;for(t=this.link;!e&&t!==document;)e=null!=t.getAttribute("data-no-turbolink"),t=t.parentNode;return e},t.prototype._target=function(){return 0!==this.link.target.length},t}(n),t=function(){function e(e){this.event=e,this.event.defaultPrevented||(this._extractLink(),this._validForTurbolinks()&&(A()||U(this.link.href),this.event.preventDefault()))}return e.installHandlerLast=function(t){return t.defaultPrevented?void 0:(document.removeEventListener("click",e.handle,!1),document.addEventListener("click",e.handle,!1))},e.handle=function(t){return new e(t)},e.prototype._extractLink=function(){var e;for(e=this.event.target;e.parentNode&&"A"!==e.nodeName;)e=e.parentNode;return"A"===e.nodeName&&0!==e.href.length?this.link=new r(e):void 0},e.prototype._validForTurbolinks=function(){return null!=this.link&&!(this.link.shouldIgnore()||this._nonStandardClick())},e.prototype._nonStandardClick=function(){return this.event.which>1||this.event.metaKey||this.event.ctrlKey||this.event.shiftKey||this.event.altKey},e}(),u=function(e){return setTimeout(e,500)},k=function(){return document.addEventListener("DOMContentLoaded",function(){return X("page:change"),X("page:update")},!0)},N=function(){return"undefined"!=typeof jQuery?jQuery(document).on("ajaxSuccess",function(e,t){return jQuery.trim(t.responseText)?X("page:update"):void 0}):void 0},C=function(e){var t,r;return(null!=(r=e.state)?r.turbolinks:void 0)?(t=j[new n(e.state.url).absolute])?(c(),x(t)):U(e.target.location.href):void 0},E=function(){return R(),O(),h=i(),document.addEventListener("click",t.installHandlerLast,!0),u(function(){return window.addEventListener("popstate",C,!1)})},T=void 0!==window.history.state||navigator.userAgent.match(/Firefox\/2[6|7]/),s=window.history&&window.history.pushState&&window.history.replaceState&&T,o=!navigator.userAgent.match(/CriOS\//),I="GET"===(G=L("request_method"))||""===G,l=s&&o&&I,a=document.addEventListener&&document.createEvent,a&&(k(),N()),l?(U=b,E()):U=function(e){return document.location.href=e},this.Turbolinks={visit:U,pagesCached:D,enableTransitionCache:g,allowLinkExtensions:r.allowExtensions,supported:l}}.call(this),function(){}.call(this);
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/gzip/foo.zoo.gz b/actionpack/test/fixtures/public/gzip/foo.zoo.gz Binary files differnew file mode 100644 index 0000000000..f62c656dc8 --- /dev/null +++ b/actionpack/test/fixtures/public/gzip/foo.zoo.gz diff --git a/actionpack/test/fixtures/public/index.html b/actionpack/test/fixtures/public/index.html new file mode 100644 index 0000000000..525950ba6b --- /dev/null +++ b/actionpack/test/fixtures/public/index.html @@ -0,0 +1 @@ +/index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/public/other-index.html b/actionpack/test/fixtures/public/other-index.html new file mode 100644 index 0000000000..0820dfcb6e --- /dev/null +++ b/actionpack/test/fixtures/public/other-index.html @@ -0,0 +1 @@ +/other-index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/all_types_with_layout.html.erb b/actionpack/test/fixtures/respond_to/all_types_with_layout.html.erb new file mode 100644 index 0000000000..84a84049f8 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/all_types_with_layout.html.erb @@ -0,0 +1 @@ +HTML for all_types_with_layout
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/custom_constant_handling_without_block.mobile.erb b/actionpack/test/fixtures/respond_to/custom_constant_handling_without_block.mobile.erb new file mode 100644 index 0000000000..0cdfa41494 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/custom_constant_handling_without_block.mobile.erb @@ -0,0 +1 @@ +Mobile
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/iphone_with_html_response_type.html.erb b/actionpack/test/fixtures/respond_to/iphone_with_html_response_type.html.erb new file mode 100644 index 0000000000..1f3f1c6516 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/iphone_with_html_response_type.html.erb @@ -0,0 +1 @@ +Hello future from <%= @type -%>!
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/iphone_with_html_response_type.iphone.erb b/actionpack/test/fixtures/respond_to/iphone_with_html_response_type.iphone.erb new file mode 100644 index 0000000000..17888ac303 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/iphone_with_html_response_type.iphone.erb @@ -0,0 +1 @@ +Hello iPhone future from <%= @type -%>!
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/layouts/missing.html.erb b/actionpack/test/fixtures/respond_to/layouts/missing.html.erb new file mode 100644 index 0000000000..d6f92a3120 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/layouts/missing.html.erb @@ -0,0 +1 @@ +<html><div id="html_missing"><%= yield %></div></html>
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/layouts/standard.html.erb b/actionpack/test/fixtures/respond_to/layouts/standard.html.erb new file mode 100644 index 0000000000..c6c1a586dd --- /dev/null +++ b/actionpack/test/fixtures/respond_to/layouts/standard.html.erb @@ -0,0 +1 @@ +<html><div id="html"><%= yield %></div></html>
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/layouts/standard.iphone.erb b/actionpack/test/fixtures/respond_to/layouts/standard.iphone.erb new file mode 100644 index 0000000000..84444517f0 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/layouts/standard.iphone.erb @@ -0,0 +1 @@ +<html><div id="iphone"><%= yield %></div></html>
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/using_defaults.html.erb b/actionpack/test/fixtures/respond_to/using_defaults.html.erb new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/actionpack/test/fixtures/respond_to/using_defaults.html.erb @@ -0,0 +1 @@ +Hello world!
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/using_defaults.xml.builder b/actionpack/test/fixtures/respond_to/using_defaults.xml.builder new file mode 100644 index 0000000000..15c8a7f5cf --- /dev/null +++ b/actionpack/test/fixtures/respond_to/using_defaults.xml.builder @@ -0,0 +1 @@ +xml.p "Hello world!" diff --git a/actionpack/test/fixtures/respond_to/using_defaults_with_all.html.erb b/actionpack/test/fixtures/respond_to/using_defaults_with_all.html.erb new file mode 100644 index 0000000000..9f1f855269 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/using_defaults_with_all.html.erb @@ -0,0 +1 @@ +HTML! diff --git a/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.html.erb b/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.html.erb new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.html.erb @@ -0,0 +1 @@ +Hello world!
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder b/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder new file mode 100644 index 0000000000..15c8a7f5cf --- /dev/null +++ b/actionpack/test/fixtures/respond_to/using_defaults_with_type_list.xml.builder @@ -0,0 +1 @@ +xml.p "Hello world!" diff --git a/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb new file mode 100644 index 0000000000..e905d051bf --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb @@ -0,0 +1 @@ +phablet
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb new file mode 100644 index 0000000000..65526af8cf --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb @@ -0,0 +1 @@ +tablet
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb b/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb new file mode 100644 index 0000000000..cd222a4a49 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb @@ -0,0 +1 @@ +phone
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb b/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb new file mode 100644 index 0000000000..c86c3f3551 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb @@ -0,0 +1 @@ +none
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb b/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb new file mode 100644 index 0000000000..317801ad30 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb @@ -0,0 +1 @@ +mobile
\ No newline at end of file diff --git a/actionpack/test/fixtures/ruby_template.ruby b/actionpack/test/fixtures/ruby_template.ruby new file mode 100644 index 0000000000..5097bce47c --- /dev/null +++ b/actionpack/test/fixtures/ruby_template.ruby @@ -0,0 +1,2 @@ +body = "" +body << ["Hello", "from", "Ruby", "code"].join(" ") diff --git a/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb b/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb new file mode 100644 index 0000000000..deb81c647d --- /dev/null +++ b/actionpack/test/fixtures/session_autoload_test/session_autoload_test/foo.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module SessionAutoloadTest + class Foo + def initialize(bar = "baz") + @bar = bar + end + def inspect + "#<#{self.class} bar:#{@bar.inspect}>" + end + end +end diff --git a/actionpack/test/fixtures/shared.html.erb b/actionpack/test/fixtures/shared.html.erb new file mode 100644 index 0000000000..af262fc9f8 --- /dev/null +++ b/actionpack/test/fixtures/shared.html.erb @@ -0,0 +1 @@ +Elastica
\ No newline at end of file diff --git a/actionpack/test/fixtures/star_star_mime/index.js.erb b/actionpack/test/fixtures/star_star_mime/index.js.erb new file mode 100644 index 0000000000..4da4181f56 --- /dev/null +++ b/actionpack/test/fixtures/star_star_mime/index.js.erb @@ -0,0 +1 @@ +function addition(a,b){ return a+b; } diff --git a/actionpack/test/fixtures/test/_partial.erb b/actionpack/test/fixtures/test/_partial.erb new file mode 100644 index 0000000000..e466dcbd8e --- /dev/null +++ b/actionpack/test/fixtures/test/_partial.erb @@ -0,0 +1 @@ +invalid
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial.html.erb b/actionpack/test/fixtures/test/_partial.html.erb new file mode 100644 index 0000000000..e39f6c9827 --- /dev/null +++ b/actionpack/test/fixtures/test/_partial.html.erb @@ -0,0 +1 @@ +partial html
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial.js.erb b/actionpack/test/fixtures/test/_partial.js.erb new file mode 100644 index 0000000000..b350cdd7ef --- /dev/null +++ b/actionpack/test/fixtures/test/_partial.js.erb @@ -0,0 +1 @@ +partial js
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/dot.directory/render_file_with_ivar.erb b/actionpack/test/fixtures/test/dot.directory/render_file_with_ivar.erb new file mode 100644 index 0000000000..8b8a449236 --- /dev/null +++ b/actionpack/test/fixtures/test/dot.directory/render_file_with_ivar.erb @@ -0,0 +1 @@ +The secret is <%= @secret %> diff --git a/actionpack/test/fixtures/test/formatted_xml_erb.builder b/actionpack/test/fixtures/test/formatted_xml_erb.builder new file mode 100644 index 0000000000..f98aaa34a5 --- /dev/null +++ b/actionpack/test/fixtures/test/formatted_xml_erb.builder @@ -0,0 +1 @@ +xml.test "failed" diff --git a/actionpack/test/fixtures/test/formatted_xml_erb.html.erb b/actionpack/test/fixtures/test/formatted_xml_erb.html.erb new file mode 100644 index 0000000000..0c855a604b --- /dev/null +++ b/actionpack/test/fixtures/test/formatted_xml_erb.html.erb @@ -0,0 +1 @@ +<test>passed formatted html erb</test>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/formatted_xml_erb.xml.erb b/actionpack/test/fixtures/test/formatted_xml_erb.xml.erb new file mode 100644 index 0000000000..6ca09d5304 --- /dev/null +++ b/actionpack/test/fixtures/test/formatted_xml_erb.xml.erb @@ -0,0 +1 @@ +<test>passed formatted xml erb</test>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello/hello.erb b/actionpack/test/fixtures/test/hello/hello.erb new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/actionpack/test/fixtures/test/hello/hello.erb @@ -0,0 +1 @@ +Hello world!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello_world.erb b/actionpack/test/fixtures/test/hello_world.erb new file mode 100644 index 0000000000..6769dd60bd --- /dev/null +++ b/actionpack/test/fixtures/test/hello_world.erb @@ -0,0 +1 @@ +Hello world!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello_world_with_partial.html.erb b/actionpack/test/fixtures/test/hello_world_with_partial.html.erb new file mode 100644 index 0000000000..ec31545356 --- /dev/null +++ b/actionpack/test/fixtures/test/hello_world_with_partial.html.erb @@ -0,0 +1,2 @@ +Hello world! +<%= render '/test/partial' %> diff --git a/actionpack/test/fixtures/test/hello_xml_world.builder b/actionpack/test/fixtures/test/hello_xml_world.builder new file mode 100644 index 0000000000..d16bb6b5cb --- /dev/null +++ b/actionpack/test/fixtures/test/hello_xml_world.builder @@ -0,0 +1,11 @@ +xml.html do + xml.head do + xml.title "Hello World" + end + + xml.body do + xml.p "abes" + xml.p "monks" + xml.p "wiseguys" + end +end diff --git a/actionpack/test/fixtures/test/implicit_content_type.atom.builder b/actionpack/test/fixtures/test/implicit_content_type.atom.builder new file mode 100644 index 0000000000..2fcb32d247 --- /dev/null +++ b/actionpack/test/fixtures/test/implicit_content_type.atom.builder @@ -0,0 +1,2 @@ +xml.atom do +end diff --git a/actionpack/test/fixtures/test/render_file_with_ivar.erb b/actionpack/test/fixtures/test/render_file_with_ivar.erb new file mode 100644 index 0000000000..8b8a449236 --- /dev/null +++ b/actionpack/test/fixtures/test/render_file_with_ivar.erb @@ -0,0 +1 @@ +The secret is <%= @secret %> diff --git a/actionpack/test/fixtures/test/render_file_with_locals.erb b/actionpack/test/fixtures/test/render_file_with_locals.erb new file mode 100644 index 0000000000..ebe09faee6 --- /dev/null +++ b/actionpack/test/fixtures/test/render_file_with_locals.erb @@ -0,0 +1 @@ +The secret is <%= secret %> diff --git a/actionpack/test/fixtures/test/with_implicit_template.erb b/actionpack/test/fixtures/test/with_implicit_template.erb new file mode 100644 index 0000000000..474488cd13 --- /dev/null +++ b/actionpack/test/fixtures/test/with_implicit_template.erb @@ -0,0 +1 @@ +Hello explicitly! diff --git a/actionpack/test/fixtures/公共/bar.html b/actionpack/test/fixtures/公共/bar.html new file mode 100644 index 0000000000..67fc57079b --- /dev/null +++ b/actionpack/test/fixtures/公共/bar.html @@ -0,0 +1 @@ +/bar.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/bar/index.html b/actionpack/test/fixtures/公共/bar/index.html new file mode 100644 index 0000000000..d5bb8f898d --- /dev/null +++ b/actionpack/test/fixtures/公共/bar/index.html @@ -0,0 +1 @@ +/bar/index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/foo/bar.html b/actionpack/test/fixtures/公共/foo/bar.html new file mode 100644 index 0000000000..9a35646205 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/bar.html @@ -0,0 +1 @@ +/foo/bar.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/foo/baz.css b/actionpack/test/fixtures/公共/foo/baz.css new file mode 100644 index 0000000000..b5173fbef2 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/baz.css @@ -0,0 +1,3 @@ +body { +background: #000; +} diff --git a/actionpack/test/fixtures/公共/foo/index.html b/actionpack/test/fixtures/公共/foo/index.html new file mode 100644 index 0000000000..497a2e898f --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/index.html @@ -0,0 +1 @@ +/foo/index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/foo/other-index.html b/actionpack/test/fixtures/公共/foo/other-index.html new file mode 100644 index 0000000000..51c90c26ea --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/other-index.html @@ -0,0 +1 @@ +/foo/other-index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/foo/こんにちは.html b/actionpack/test/fixtures/公共/foo/こんにちは.html new file mode 100644 index 0000000000..1df9166522 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/こんにちは.html @@ -0,0 +1 @@ +means hello in Japanese diff --git a/actionpack/test/fixtures/公共/foo/さようなら.html b/actionpack/test/fixtures/公共/foo/さようなら.html new file mode 100644 index 0000000000..627bb2469f --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/さようなら.html @@ -0,0 +1 @@ +means goodbye in Japanese diff --git a/actionpack/test/fixtures/公共/foo/さようなら.html.gz b/actionpack/test/fixtures/公共/foo/さようなら.html.gz Binary files differnew file mode 100644 index 0000000000..4f484cfe86 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/さようなら.html.gz diff --git a/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js b/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js new file mode 100644 index 0000000000..1826a7660e --- /dev/null +++ b/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js @@ -0,0 +1,4 @@ +!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){function n(e){var t=e.length,n=it.type(e);return"function"===n||it.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e}function r(e,t,n){if(it.isFunction(t))return it.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return it.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ft.test(t))return it.filter(t,e,n);t=it.filter(t,e)}return it.grep(e,function(e){return it.inArray(e,t)>=0!==n})}function i(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function o(e){var t=xt[e]={};return it.each(e.match(bt)||[],function(e,n){t[n]=!0}),t}function a(){ht.addEventListener?(ht.removeEventListener("DOMContentLoaded",s,!1),e.removeEventListener("load",s,!1)):(ht.detachEvent("onreadystatechange",s),e.detachEvent("onload",s))}function s(){(ht.addEventListener||"load"===event.type||"complete"===ht.readyState)&&(a(),it.ready())}function l(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(Ct,"-$1").toLowerCase();if(n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:kt.test(n)?it.parseJSON(n):n}catch(i){}it.data(e,t,n)}else n=void 0}return n}function u(e){var t;for(t in e)if(("data"!==t||!it.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function c(e,t,n,r){if(it.acceptData(e)){var i,o,a=it.expando,s=e.nodeType,l=s?it.cache:e,u=s?e[a]:e[a]&&a;if(u&&l[u]&&(r||l[u].data)||void 0!==n||"string"!=typeof t)return u||(u=s?e[a]=G.pop()||it.guid++:a),l[u]||(l[u]=s?{}:{toJSON:it.noop}),("object"==typeof t||"function"==typeof t)&&(r?l[u]=it.extend(l[u],t):l[u].data=it.extend(l[u].data,t)),o=l[u],r||(o.data||(o.data={}),o=o.data),void 0!==n&&(o[it.camelCase(t)]=n),"string"==typeof t?(i=o[t],null==i&&(i=o[it.camelCase(t)])):i=o,i}}function d(e,t,n){if(it.acceptData(e)){var r,i,o=e.nodeType,a=o?it.cache:e,s=o?e[it.expando]:it.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){it.isArray(t)?t=t.concat(it.map(t,it.camelCase)):t in r?t=[t]:(t=it.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;for(;i--;)delete r[t[i]];if(n?!u(r):!it.isEmptyObject(r))return}(n||(delete a[s].data,u(a[s])))&&(o?it.cleanData([e],!0):nt.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}function f(){return!0}function p(){return!1}function h(){try{return ht.activeElement}catch(e){}}function m(e){var t=Mt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function g(e,t){var n,r,i=0,o=typeof e.getElementsByTagName!==Et?e.getElementsByTagName(t||"*"):typeof e.querySelectorAll!==Et?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||it.nodeName(r,t)?o.push(r):it.merge(o,g(r,t));return void 0===t||t&&it.nodeName(e,t)?it.merge([e],o):o}function v(e){Dt.test(e.type)&&(e.defaultChecked=e.checked)}function y(e,t){return it.nodeName(e,"table")&&it.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function b(e){return e.type=(null!==it.find.attr(e,"type"))+"/"+e.type,e}function x(e){var t=Vt.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function w(e,t){for(var n,r=0;null!=(n=e[r]);r++)it._data(n,"globalEval",!t||it._data(t[r],"globalEval"))}function T(e,t){if(1===t.nodeType&&it.hasData(e)){var n,r,i,o=it._data(e),a=it._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)it.event.add(t,n,s[n][r])}a.data&&(a.data=it.extend({},a.data))}}function E(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!nt.noCloneEvent&&t[it.expando]){i=it._data(t);for(r in i.events)it.removeEvent(t,r,i.handle);t.removeAttribute(it.expando)}"script"===n&&t.text!==e.text?(b(t).text=e.text,x(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),nt.html5Clone&&e.innerHTML&&!it.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Dt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function k(t,n){var r,i=it(n.createElement(t)).appendTo(n.body),o=e.getDefaultComputedStyle&&(r=e.getDefaultComputedStyle(i[0]))?r.display:it.css(i[0],"display");return i.detach(),o}function C(e){var t=ht,n=Zt[e];return n||(n=k(e,t),"none"!==n&&n||(Qt=(Qt||it("<iframe frameborder='0' width='0' height='0'/>")).appendTo(t.documentElement),t=(Qt[0].contentWindow||Qt[0].contentDocument).document,t.write(),t.close(),n=k(e,t),Qt.detach()),Zt[e]=n),n}function N(e,t){return{get:function(){var n=e();if(null!=n)return n?void delete this.get:(this.get=t).apply(this,arguments)}}}function S(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=pn.length;i--;)if(t=pn[i]+n,t in e)return t;return r}function j(e,t){for(var n,r,i,o=[],a=0,s=e.length;s>a;a++)r=e[a],r.style&&(o[a]=it._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&jt(r)&&(o[a]=it._data(r,"olddisplay",C(r.nodeName)))):(i=jt(r),(n&&"none"!==n||!i)&&it._data(r,"olddisplay",i?n:it.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}function A(e,t,n){var r=un.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function D(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=it.css(e,n+St[o],!0,i)),r?("content"===n&&(a-=it.css(e,"padding"+St[o],!0,i)),"margin"!==n&&(a-=it.css(e,"border"+St[o]+"Width",!0,i))):(a+=it.css(e,"padding"+St[o],!0,i),"padding"!==n&&(a+=it.css(e,"border"+St[o]+"Width",!0,i)));return a}function L(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=en(e),a=nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=tn(e,t,o),(0>i||null==i)&&(i=e.style[t]),rn.test(i))return i;r=a&&(nt.boxSizingReliable()||i===e.style[t]),i=parseFloat(i)||0}return i+D(e,t,n||(a?"border":"content"),r,o)+"px"}function H(e,t,n,r,i){return new H.prototype.init(e,t,n,r,i)}function _(){return setTimeout(function(){hn=void 0}),hn=it.now()}function q(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=St[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function F(e,t,n){for(var r,i=(xn[t]||[]).concat(xn["*"]),o=0,a=i.length;a>o;o++)if(r=i[o].call(n,t,e))return r}function M(e,t,n){var r,i,o,a,s,l,u,c,d=this,f={},p=e.style,h=e.nodeType&&jt(e),m=it._data(e,"fxshow");n.queue||(s=it._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,d.always(function(){d.always(function(){s.unqueued--,it.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],u=it.css(e,"display"),c="none"===u?it._data(e,"olddisplay")||C(e.nodeName):u,"inline"===c&&"none"===it.css(e,"float")&&(nt.inlineBlockNeedsLayout&&"inline"!==C(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",nt.shrinkWrapBlocks()||d.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],gn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(h?"hide":"show")){if("show"!==i||!m||void 0===m[r])continue;h=!0}f[r]=m&&m[r]||it.style(e,r)}else u=void 0;if(it.isEmptyObject(f))"inline"===("none"===u?C(e.nodeName):u)&&(p.display=u);else{m?"hidden"in m&&(h=m.hidden):m=it._data(e,"fxshow",{}),o&&(m.hidden=!h),h?it(e).show():d.done(function(){it(e).hide()}),d.done(function(){var t;it._removeData(e,"fxshow");for(t in f)it.style(e,t,f[t])});for(r in f)a=F(h?m[r]:0,r,d),r in m||(m[r]=a.start,h&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function O(e,t){var n,r,i,o,a;for(n in e)if(r=it.camelCase(n),i=t[r],o=e[n],it.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=it.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function R(e,t,n){var r,i,o=0,a=bn.length,s=it.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;for(var t=hn||_(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:it.extend({},t),opts:it.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:hn||_(),duration:n.duration,tweens:[],createTween:function(t,n){var r=it.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(O(c,u.opts.specialEasing);a>o;o++)if(r=bn[o].call(u,e,c,u.opts))return r;return it.map(c,F,u),it.isFunction(u.opts.start)&&u.opts.start.call(e,u),it.fx.timer(it.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function B(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(bt)||[];if(it.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function P(e,t,n,r){function i(s){var l;return o[s]=!0,it.each(e[s]||[],function(e,s){var u=s(t,n,r);return"string"!=typeof u||a||o[u]?a?!(l=u):void 0:(t.dataTypes.unshift(u),i(u),!1)}),l}var o={},a=e===zn;return i(t.dataTypes[0])||!o["*"]&&i("*")}function I(e,t){var n,r,i=it.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&it.extend(!0,e,n),e}function W(e,t,n){for(var r,i,o,a,s=e.contents,l=e.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){l.unshift(a);break}if(l[0]in n)o=l[0];else{for(a in n){if(!l[0]||e.converters[a+" "+l[0]]){o=a;break}r||(r=a)}o=o||r}return o?(o!==l[0]&&l.unshift(o),n[o]):void 0}function $(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(d){return{state:"parsererror",error:a?d:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}function z(e,t,n,r){var i;if(it.isArray(t))it.each(t,function(t,i){n||Gn.test(e)?r(e,i):z(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==it.type(t))r(e,t);else for(i in t)z(e+"["+i+"]",t[i],n,r)}function X(){try{return new e.XMLHttpRequest}catch(t){}}function U(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function V(e){return it.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}var G=[],Y=G.slice,J=G.concat,K=G.push,Q=G.indexOf,Z={},et=Z.toString,tt=Z.hasOwnProperty,nt={},rt="1.11.1",it=function(e,t){return new it.fn.init(e,t)},ot=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,at=/^-ms-/,st=/-([\da-z])/gi,lt=function(e,t){return t.toUpperCase()};it.fn=it.prototype={jquery:rt,constructor:it,selector:"",length:0,toArray:function(){return Y.call(this)},get:function(e){return null!=e?0>e?this[e+this.length]:this[e]:Y.call(this)},pushStack:function(e){var t=it.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return it.each(this,e,t)},map:function(e){return this.pushStack(it.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(Y.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:K,sort:G.sort,splice:G.splice},it.extend=it.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,l=arguments.length,u=!1;for("boolean"==typeof a&&(u=a,a=arguments[s]||{},s++),"object"==typeof a||it.isFunction(a)||(a={}),s===l&&(a=this,s--);l>s;s++)if(null!=(i=arguments[s]))for(r in i)e=a[r],n=i[r],a!==n&&(u&&n&&(it.isPlainObject(n)||(t=it.isArray(n)))?(t?(t=!1,o=e&&it.isArray(e)?e:[]):o=e&&it.isPlainObject(e)?e:{},a[r]=it.extend(u,o,n)):void 0!==n&&(a[r]=n));return a},it.extend({expando:"jQuery"+(rt+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isFunction:function(e){return"function"===it.type(e)},isArray:Array.isArray||function(e){return"array"===it.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!it.isArray(e)&&e-parseFloat(e)>=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==it.type(e)||e.nodeType||it.isWindow(e))return!1;try{if(e.constructor&&!tt.call(e,"constructor")&&!tt.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}if(nt.ownLast)for(t in e)return tt.call(e,t);for(t in e);return void 0===t||tt.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?Z[et.call(e)]||"object":typeof e},globalEval:function(t){t&&it.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(at,"ms-").replace(st,lt)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,r){var i,o=0,a=e.length,s=n(e);if(r){if(s)for(;a>o&&(i=t.apply(e[o],r),i!==!1);o++);else for(o in e)if(i=t.apply(e[o],r),i===!1)break}else if(s)for(;a>o&&(i=t.call(e[o],o,e[o]),i!==!1);o++);else for(o in e)if(i=t.call(e[o],o,e[o]),i===!1)break;return e},trim:function(e){return null==e?"":(e+"").replace(ot,"")},makeArray:function(e,t){var r=t||[];return null!=e&&(n(Object(e))?it.merge(r,"string"==typeof e?[e]:e):K.call(r,e)),r},inArray:function(e,t,n){var r;if(t){if(Q)return Q.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;n>r;)e[i++]=t[r++];if(n!==n)for(;void 0!==t[r];)e[i++]=t[r++];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;a>o;o++)r=!t(e[o],o),r!==s&&i.push(e[o]);return i},map:function(e,t,r){var i,o=0,a=e.length,s=n(e),l=[];if(s)for(;a>o;o++)i=t(e[o],o,r),null!=i&&l.push(i);else for(o in e)i=t(e[o],o,r),null!=i&&l.push(i);return J.apply([],l)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(i=e[t],t=e,e=i),it.isFunction(e)?(n=Y.call(arguments,2),r=function(){return e.apply(t||this,n.concat(Y.call(arguments)))},r.guid=e.guid=e.guid||it.guid++,r):void 0},now:function(){return+new Date},support:nt}),it.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){Z["[object "+t+"]"]=t.toLowerCase()});var ut=function(e){function t(e,t,n,r){var i,o,a,s,l,u,d,p,h,m;if((t?t.ownerDocument||t:P)!==H&&L(t),t=t||H,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(q&&!r){if(i=yt.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&R(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return Z.apply(n,t.getElementsByTagName(e)),n;if((a=i[3])&&w.getElementsByClassName&&t.getElementsByClassName)return Z.apply(n,t.getElementsByClassName(a)),n}if(w.qsa&&(!F||!F.test(e))){if(p=d=B,h=t,m=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){for(u=C(e),(d=t.getAttribute("id"))?p=d.replace(xt,"\\$&"):t.setAttribute("id",p),p="[id='"+p+"'] ",l=u.length;l--;)u[l]=p+f(u[l]);h=bt.test(e)&&c(t.parentNode)||t,m=u.join(",")}if(m)try{return Z.apply(n,h.querySelectorAll(m)),n}catch(g){}finally{d||t.removeAttribute("id")}}}return S(e.replace(lt,"$1"),t,n,r)}function n(){function e(n,r){return t.push(n+" ")>T.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[B]=!0,e}function i(e){var t=H.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=e.length;r--;)T.attrHandle[n[r]]=t}function a(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||G)-(~e.sourceIndex||G);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function s(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function l(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function u(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function c(e){return e&&typeof e.getElementsByTagName!==V&&e}function d(){}function f(e){for(var t=0,n=e.length,r="";n>t;t++)r+=e[t].value;return r}function p(e,t,n){var r=t.dir,i=n&&"parentNode"===r,o=W++;return t.first?function(t,n,o){for(;t=t[r];)if(1===t.nodeType||i)return e(t,n,o)}:function(t,n,a){var s,l,u=[I,o];if(a){for(;t=t[r];)if((1===t.nodeType||i)&&e(t,n,a))return!0}else for(;t=t[r];)if(1===t.nodeType||i){if(l=t[B]||(t[B]={}),(s=l[r])&&s[0]===I&&s[1]===o)return u[2]=s[2];if(l[r]=u,u[2]=e(t,n,a))return!0}}}function h(e){return e.length>1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function m(e,n,r){for(var i=0,o=n.length;o>i;i++)t(e,n[i],r);return r}function g(e,t,n,r,i){for(var o,a=[],s=0,l=e.length,u=null!=t;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function v(e,t,n,i,o,a){return i&&!i[B]&&(i=v(i)),o&&!o[B]&&(o=v(o,a)),r(function(r,a,s,l){var u,c,d,f=[],p=[],h=a.length,v=r||m(t||"*",s.nodeType?[s]:s,[]),y=!e||!r&&t?v:g(v,f,e,s,l),b=n?o||(r?e:h||i)?[]:a:y;if(n&&n(y,b,s,l),i)for(u=g(b,p),i(u,[],s,l),c=u.length;c--;)(d=u[c])&&(b[p[c]]=!(y[p[c]]=d));if(r){if(o||e){if(o){for(u=[],c=b.length;c--;)(d=b[c])&&u.push(y[c]=d);o(null,b=[],u,l)}for(c=b.length;c--;)(d=b[c])&&(u=o?tt.call(r,d):f[c])>-1&&(r[u]=!(a[u]=d))}}else b=g(b===a?b.splice(h,b.length):b),o?o(null,a,b,l):Z.apply(a,b)})}function y(e){for(var t,n,r,i=e.length,o=T.relative[e[0].type],a=o||T.relative[" "],s=o?1:0,l=p(function(e){return e===t},a,!0),u=p(function(e){return tt.call(t,e)>-1},a,!0),c=[function(e,n,r){return!o&&(r||n!==j)||((t=n).nodeType?l(e,n,r):u(e,n,r))}];i>s;s++)if(n=T.relative[e[s].type])c=[p(h(c),n)];else{if(n=T.filter[e[s].type].apply(null,e[s].matches),n[B]){for(r=++s;i>r&&!T.relative[e[r].type];r++);return v(s>1&&h(c),s>1&&f(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(lt,"$1"),n,r>s&&y(e.slice(s,r)),i>r&&y(e=e.slice(r)),i>r&&f(e))}c.push(n)}return h(c)}function b(e,n){var i=n.length>0,o=e.length>0,a=function(r,a,s,l,u){var c,d,f,p=0,h="0",m=r&&[],v=[],y=j,b=r||o&&T.find.TAG("*",u),x=I+=null==y?1:Math.random()||.1,w=b.length;for(u&&(j=a!==H&&a);h!==w&&null!=(c=b[h]);h++){if(o&&c){for(d=0;f=e[d++];)if(f(c,a,s)){l.push(c);break}u&&(I=x)}i&&((c=!f&&c)&&p--,r&&m.push(c))}if(p+=h,i&&h!==p){for(d=0;f=n[d++];)f(m,v,a,s);if(r){if(p>0)for(;h--;)m[h]||v[h]||(v[h]=K.call(l));v=g(v)}Z.apply(l,v),u&&!r&&v.length>0&&p+n.length>1&&t.uniqueSort(l)}return u&&(I=x,j=y),m};return i?r(a):a}var x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B="sizzle"+-new Date,P=e.document,I=0,W=0,$=n(),z=n(),X=n(),U=function(e,t){return e===t&&(D=!0),0},V="undefined",G=1<<31,Y={}.hasOwnProperty,J=[],K=J.pop,Q=J.push,Z=J.push,et=J.slice,tt=J.indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(this[t]===e)return t;return-1},nt="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",rt="[\\x20\\t\\r\\n\\f]",it="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",ot=it.replace("w","w#"),at="\\["+rt+"*("+it+")(?:"+rt+"*([*^$|!~]?=)"+rt+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+ot+"))|)"+rt+"*\\]",st=":("+it+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+at+")*)|.*)\\)|)",lt=new RegExp("^"+rt+"+|((?:^|[^\\\\])(?:\\\\.)*)"+rt+"+$","g"),ut=new RegExp("^"+rt+"*,"+rt+"*"),ct=new RegExp("^"+rt+"*([>+~]|"+rt+")"+rt+"*"),dt=new RegExp("="+rt+"*([^\\]'\"]*?)"+rt+"*\\]","g"),ft=new RegExp(st),pt=new RegExp("^"+ot+"$"),ht={ID:new RegExp("^#("+it+")"),CLASS:new RegExp("^\\.("+it+")"),TAG:new RegExp("^("+it.replace("w","w*")+")"),ATTR:new RegExp("^"+at),PSEUDO:new RegExp("^"+st),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+rt+"*(even|odd|(([+-]|)(\\d*)n|)"+rt+"*(?:([+-]|)"+rt+"*(\\d+)|))"+rt+"*\\)|)","i"),bool:new RegExp("^(?:"+nt+")$","i"),needsContext:new RegExp("^"+rt+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+rt+"*((?:-\\d)?\\d*)"+rt+"*\\)|)(?=[^-]|$)","i")},mt=/^(?:input|select|textarea|button)$/i,gt=/^h\d$/i,vt=/^[^{]+\{\s*\[native \w/,yt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,bt=/[+~]/,xt=/'|\\/g,wt=new RegExp("\\\\([\\da-f]{1,6}"+rt+"?|("+rt+")|.)","ig"),Tt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{Z.apply(J=et.call(P.childNodes),P.childNodes),J[P.childNodes.length].nodeType}catch(Et){Z={apply:J.length?function(e,t){Q.apply(e,et.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}w=t.support={},k=t.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},L=t.setDocument=function(e){var t,n=e?e.ownerDocument||e:P,r=n.defaultView;return n!==H&&9===n.nodeType&&n.documentElement?(H=n,_=n.documentElement,q=!k(n),r&&r!==r.top&&(r.addEventListener?r.addEventListener("unload",function(){L()},!1):r.attachEvent&&r.attachEvent("onunload",function(){L()})),w.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),w.getElementsByTagName=i(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),w.getElementsByClassName=vt.test(n.getElementsByClassName)&&i(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),w.getById=i(function(e){return _.appendChild(e).id=B,!n.getElementsByName||!n.getElementsByName(B).length}),w.getById?(T.find.ID=function(e,t){if(typeof t.getElementById!==V&&q){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){return e.getAttribute("id")===t}}):(delete T.find.ID,T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){var n=typeof e.getAttributeNode!==V&&e.getAttributeNode("id");return n&&n.value===t}}),T.find.TAG=w.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==V?t.getElementsByTagName(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},T.find.CLASS=w.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==V&&q?t.getElementsByClassName(e):void 0},M=[],F=[],(w.qsa=vt.test(n.querySelectorAll))&&(i(function(e){e.innerHTML="<select msallowclip=''><option selected=''></option></select>",e.querySelectorAll("[msallowclip^='']").length&&F.push("[*^$]="+rt+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||F.push("\\["+rt+"*(?:value|"+nt+")"),e.querySelectorAll(":checked").length||F.push(":checked")}),i(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&F.push("name"+rt+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||F.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),F.push(",.*:")})),(w.matchesSelector=vt.test(O=_.matches||_.webkitMatchesSelector||_.mozMatchesSelector||_.oMatchesSelector||_.msMatchesSelector))&&i(function(e){w.disconnectedMatch=O.call(e,"div"),O.call(e,"[s!='']:x"),M.push("!=",st)}),F=F.length&&new RegExp(F.join("|")),M=M.length&&new RegExp(M.join("|")),t=vt.test(_.compareDocumentPosition),R=t||vt.test(_.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},U=t?function(e,t){if(e===t)return D=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r?r:(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&r||!w.sortDetached&&t.compareDocumentPosition(e)===r?e===n||e.ownerDocument===P&&R(P,e)?-1:t===n||t.ownerDocument===P&&R(P,t)?1:A?tt.call(A,e)-tt.call(A,t):0:4&r?-1:1)}:function(e,t){if(e===t)return D=!0,0;var r,i=0,o=e.parentNode,s=t.parentNode,l=[e],u=[t];if(!o||!s)return e===n?-1:t===n?1:o?-1:s?1:A?tt.call(A,e)-tt.call(A,t):0;if(o===s)return a(e,t);for(r=e;r=r.parentNode;)l.unshift(r);for(r=t;r=r.parentNode;)u.unshift(r);for(;l[i]===u[i];)i++;return i?a(l[i],u[i]):l[i]===P?-1:u[i]===P?1:0},n):H},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==H&&L(e),n=n.replace(dt,"='$1']"),!(!w.matchesSelector||!q||M&&M.test(n)||F&&F.test(n)))try{var r=O.call(e,n);if(r||w.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return t(n,H,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==H&&L(e),R(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==H&&L(e);var n=T.attrHandle[t.toLowerCase()],r=n&&Y.call(T.attrHandle,t.toLowerCase())?n(e,t,!q):void 0;return void 0!==r?r:w.attributes||!q?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(D=!w.detectDuplicates,A=!w.sortStable&&e.slice(0),e.sort(U),D){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return A=null,e},E=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=E(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=E(t);return n},T=t.selectors={cacheLength:50,createPseudo:r,match:ht,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(wt,Tt),e[3]=(e[3]||e[4]||e[5]||"").replace(wt,Tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return ht.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&ft.test(n)&&(t=C(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(wt,Tt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=$[e+" "];return t||(t=new RegExp("(^|"+rt+")"+e+"("+rt+"|$)"))&&$(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==V&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:n?(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o+" ").indexOf(r)>-1:"|="===n?o===r||o.slice(0,r.length+1)===r+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,d,f,p,h,m=o!==a?"nextSibling":"previousSibling",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!l&&!s;if(g){if(o){for(;m;){for(d=t;d=d[m];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=m="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?g.firstChild:g.lastChild],a&&y){for(c=g[B]||(g[B]={}),u=c[e]||[],p=u[0]===I&&u[1],f=u[0]===I&&u[2],d=p&&g.childNodes[p];d=++p&&d&&d[m]||(f=p=0)||h.pop();)if(1===d.nodeType&&++f&&d===t){c[e]=[I,p,f];break}}else if(y&&(u=(t[B]||(t[B]={}))[e])&&u[0]===I)f=u[1];else for(;(d=++p&&d&&d[m]||(f=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++f||(y&&((d[B]||(d[B]={}))[e]=[I,f]),d!==t)););return f-=i,f===r||f%r===0&&f/r>=0}}},PSEUDO:function(e,n){var i,o=T.pseudos[e]||T.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[B]?o(n):o.length>1?(i=[e,e,"",n],T.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),a=i.length;a--;)r=tt.call(e,i[a]),e[r]=!(t[r]=i[a])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=N(e.replace(lt,"$1"));return i[B]?r(function(e,t,n,r){for(var o,a=i(e,null,r,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return function(t){return(t.textContent||t.innerText||E(t)).indexOf(e)>-1}}),lang:r(function(e){return pt.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(wt,Tt).toLowerCase(),function(t){var n;do if(n=q?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===_},focus:function(e){return e===H.activeElement&&(!H.hasFocus||H.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.pseudos.empty(e)},header:function(e){return gt.test(e.nodeName)},input:function(e){return mt.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:u(function(){return[0]}),last:u(function(e,t){return[t-1]}),eq:u(function(e,t,n){return[0>n?n+t:n]}),even:u(function(e,t){for(var n=0;t>n;n+=2)e.push(n);return e}),odd:u(function(e,t){for(var n=1;t>n;n+=2)e.push(n);return e}),lt:u(function(e,t,n){for(var r=0>n?n+t:n;--r>=0;)e.push(r);return e}),gt:u(function(e,t,n){for(var r=0>n?n+t:n;++r<t;)e.push(r);return e})}},T.pseudos.nth=T.pseudos.eq;for(x in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})T.pseudos[x]=s(x);for(x in{submit:!0,reset:!0})T.pseudos[x]=l(x);return d.prototype=T.filters=T.pseudos,T.setFilters=new d,C=t.tokenize=function(e,n){var r,i,o,a,s,l,u,c=z[e+" "];if(c)return n?0:c.slice(0);for(s=e,l=[],u=T.preFilter;s;){(!r||(i=ut.exec(s)))&&(i&&(s=s.slice(i[0].length)||s),l.push(o=[])),r=!1,(i=ct.exec(s))&&(r=i.shift(),o.push({value:r,type:i[0].replace(lt," ")}),s=s.slice(r.length));for(a in T.filter)!(i=ht[a].exec(s))||u[a]&&!(i=u[a](i))||(r=i.shift(),o.push({value:r,type:a,matches:i}),s=s.slice(r.length));if(!r)break}return n?s.length:s?t.error(e):z(e,l).slice(0)},N=t.compile=function(e,t){var n,r=[],i=[],o=X[e+" "];if(!o){for(t||(t=C(e)),n=t.length;n--;)o=y(t[n]),o[B]?r.push(o):i.push(o);o=X(e,b(i,r)),o.selector=e}return o},S=t.select=function(e,t,n,r){var i,o,a,s,l,u="function"==typeof e&&e,d=!r&&C(e=u.selector||e);if(n=n||[],1===d.length){if(o=d[0]=d[0].slice(0),o.length>2&&"ID"===(a=o[0]).type&&w.getById&&9===t.nodeType&&q&&T.relative[o[1].type]){if(t=(T.find.ID(a.matches[0].replace(wt,Tt),t)||[])[0],!t)return n;u&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=ht.needsContext.test(e)?0:o.length;i--&&(a=o[i],!T.relative[s=a.type]);)if((l=T.find[s])&&(r=l(a.matches[0].replace(wt,Tt),bt.test(o[0].type)&&c(t.parentNode)||t))){if(o.splice(i,1),e=r.length&&f(o),!e)return Z.apply(n,r),n; +break}}return(u||N(e,d))(r,t,!q,n,bt.test(e)&&c(t.parentNode)||t),n},w.sortStable=B.split("").sort(U).join("")===B,w.detectDuplicates=!!D,L(),w.sortDetached=i(function(e){return 1&e.compareDocumentPosition(H.createElement("div"))}),i(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){return n?void 0:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),w.attributes&&i(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?void 0:e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(nt,function(e,t,n){var r;return n?void 0:e[t]===!0?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(e);it.find=ut,it.expr=ut.selectors,it.expr[":"]=it.expr.pseudos,it.unique=ut.uniqueSort,it.text=ut.getText,it.isXMLDoc=ut.isXML,it.contains=ut.contains;var ct=it.expr.match.needsContext,dt=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,ft=/^.[^:#\[\.,]*$/;it.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?it.find.matchesSelector(r,e)?[r]:[]:it.find.matches(e,it.grep(t,function(e){return 1===e.nodeType}))},it.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(it(e).filter(function(){for(t=0;i>t;t++)if(it.contains(r[t],this))return!0}));for(t=0;i>t;t++)it.find(e,r[t],n);return n=this.pushStack(i>1?it.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(r(this,e||[],!1))},not:function(e){return this.pushStack(r(this,e||[],!0))},is:function(e){return!!r(this,"string"==typeof e&&ct.test(e)?it(e):e||[],!1).length}});var pt,ht=e.document,mt=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,gt=it.fn.init=function(e,t){var n,r;if(!e)return this;if("string"==typeof e){if(n="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:mt.exec(e),!n||!n[1]&&t)return!t||t.jquery?(t||pt).find(e):this.constructor(t).find(e);if(n[1]){if(t=t instanceof it?t[0]:t,it.merge(this,it.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:ht,!0)),dt.test(n[1])&&it.isPlainObject(t))for(n in t)it.isFunction(this[n])?this[n](t[n]):this.attr(n,t[n]);return this}if(r=ht.getElementById(n[2]),r&&r.parentNode){if(r.id!==n[2])return pt.find(e);this.length=1,this[0]=r}return this.context=ht,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):it.isFunction(e)?"undefined"!=typeof pt.ready?pt.ready(e):e(it):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),it.makeArray(e,this))};gt.prototype=it.fn,pt=it(ht);var vt=/^(?:parents|prev(?:Until|All))/,yt={children:!0,contents:!0,next:!0,prev:!0};it.extend({dir:function(e,t,n){for(var r=[],i=e[t];i&&9!==i.nodeType&&(void 0===n||1!==i.nodeType||!it(i).is(n));)1===i.nodeType&&r.push(i),i=i[t];return r},sibling:function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}}),it.fn.extend({has:function(e){var t,n=it(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(it.contains(this,n[t]))return!0})},closest:function(e,t){for(var n,r=0,i=this.length,o=[],a=ct.test(e)||"string"!=typeof e?it(e,t||this.context):0;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&it.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?it.unique(o):o)},index:function(e){return e?"string"==typeof e?it.inArray(this[0],it(e)):it.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(it.unique(it.merge(this.get(),it(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),it.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return it.dir(e,"parentNode")},parentsUntil:function(e,t,n){return it.dir(e,"parentNode",n)},next:function(e){return i(e,"nextSibling")},prev:function(e){return i(e,"previousSibling")},nextAll:function(e){return it.dir(e,"nextSibling")},prevAll:function(e){return it.dir(e,"previousSibling")},nextUntil:function(e,t,n){return it.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return it.dir(e,"previousSibling",n)},siblings:function(e){return it.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return it.sibling(e.firstChild)},contents:function(e){return it.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:it.merge([],e.childNodes)}},function(e,t){it.fn[e]=function(n,r){var i=it.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=it.filter(r,i)),this.length>1&&(yt[e]||(i=it.unique(i)),vt.test(e)&&(i=i.reverse())),this.pushStack(i)}});var bt=/\S+/g,xt={};it.Callbacks=function(e){e="string"==typeof e?xt[e]||o(e):it.extend({},e);var t,n,r,i,a,s,l=[],u=!e.once&&[],c=function(o){for(n=e.memory&&o,r=!0,a=s||0,s=0,i=l.length,t=!0;l&&i>a;a++)if(l[a].apply(o[0],o[1])===!1&&e.stopOnFalse){n=!1;break}t=!1,l&&(u?u.length&&c(u.shift()):n?l=[]:d.disable())},d={add:function(){if(l){var r=l.length;!function o(t){it.each(t,function(t,n){var r=it.type(n);"function"===r?e.unique&&d.has(n)||l.push(n):n&&n.length&&"string"!==r&&o(n)})}(arguments),t?i=l.length:n&&(s=r,c(n))}return this},remove:function(){return l&&it.each(arguments,function(e,n){for(var r;(r=it.inArray(n,l,r))>-1;)l.splice(r,1),t&&(i>=r&&i--,a>=r&&a--)}),this},has:function(e){return e?it.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],i=0,this},disable:function(){return l=u=n=void 0,this},disabled:function(){return!l},lock:function(){return u=void 0,n||d.disable(),this},locked:function(){return!u},fireWith:function(e,n){return!l||r&&!u||(n=n||[],n=[e,n.slice?n.slice():n],t?u.push(n):c(n)),this},fire:function(){return d.fireWith(this,arguments),this},fired:function(){return!!r}};return d},it.extend({Deferred:function(e){var t=[["resolve","done",it.Callbacks("once memory"),"resolved"],["reject","fail",it.Callbacks("once memory"),"rejected"],["notify","progress",it.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return it.Deferred(function(n){it.each(t,function(t,o){var a=it.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&it.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?it.extend(e,r):r}},i={};return r.pipe=r.then,it.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,i=0,o=Y.call(arguments),a=o.length,s=1!==a||e&&it.isFunction(e.promise)?a:0,l=1===s?e:it.Deferred(),u=function(e,n,r){return function(i){n[e]=this,r[e]=arguments.length>1?Y.call(arguments):i,r===t?l.notifyWith(n,r):--s||l.resolveWith(n,r)}};if(a>1)for(t=new Array(a),n=new Array(a),r=new Array(a);a>i;i++)o[i]&&it.isFunction(o[i].promise)?o[i].promise().done(u(i,r,o)).fail(l.reject).progress(u(i,n,t)):--s;return s||l.resolveWith(r,o),l.promise()}});var wt;it.fn.ready=function(e){return it.ready.promise().done(e),this},it.extend({isReady:!1,readyWait:1,holdReady:function(e){e?it.readyWait++:it.ready(!0)},ready:function(e){if(e===!0?!--it.readyWait:!it.isReady){if(!ht.body)return setTimeout(it.ready);it.isReady=!0,e!==!0&&--it.readyWait>0||(wt.resolveWith(ht,[it]),it.fn.triggerHandler&&(it(ht).triggerHandler("ready"),it(ht).off("ready")))}}}),it.ready.promise=function(t){if(!wt)if(wt=it.Deferred(),"complete"===ht.readyState)setTimeout(it.ready);else if(ht.addEventListener)ht.addEventListener("DOMContentLoaded",s,!1),e.addEventListener("load",s,!1);else{ht.attachEvent("onreadystatechange",s),e.attachEvent("onload",s);var n=!1;try{n=null==e.frameElement&&ht.documentElement}catch(r){}n&&n.doScroll&&!function i(){if(!it.isReady){try{n.doScroll("left")}catch(e){return setTimeout(i,50)}a(),it.ready()}}()}return wt.promise(t)};var Tt,Et="undefined";for(Tt in it(nt))break;nt.ownLast="0"!==Tt,nt.inlineBlockNeedsLayout=!1,it(function(){var e,t,n,r;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",nt.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(r))}),function(){var e=ht.createElement("div");if(null==nt.deleteExpando){nt.deleteExpando=!0;try{delete e.test}catch(t){nt.deleteExpando=!1}}e=null}(),it.acceptData=function(e){var t=it.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return 1!==n&&9!==n?!1:!t||t!==!0&&e.getAttribute("classid")===t};var kt=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Ct=/([A-Z])/g;it.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?it.cache[e[it.expando]]:e[it.expando],!!e&&!u(e)},data:function(e,t,n){return c(e,t,n)},removeData:function(e,t){return d(e,t)},_data:function(e,t,n){return c(e,t,n,!0)},_removeData:function(e,t){return d(e,t,!0)}}),it.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=it.data(o),1===o.nodeType&&!it._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&(r=a[n].name,0===r.indexOf("data-")&&(r=it.camelCase(r.slice(5)),l(o,r,i[r])));it._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){it.data(this,e)}):arguments.length>1?this.each(function(){it.data(this,e,t)}):o?l(o,e,it.data(o,e)):void 0},removeData:function(e){return this.each(function(){it.removeData(this,e)})}}),it.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=it._data(e,t),n&&(!r||it.isArray(n)?r=it._data(e,t,it.makeArray(n)):r.push(n)),r||[]):void 0},dequeue:function(e,t){t=t||"fx";var n=it.queue(e,t),r=n.length,i=n.shift(),o=it._queueHooks(e,t),a=function(){it.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return it._data(e,n)||it._data(e,n,{empty:it.Callbacks("once memory").add(function(){it._removeData(e,t+"queue"),it._removeData(e,n)})})}}),it.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?it.queue(this[0],e):void 0===t?this:this.each(function(){var n=it.queue(this,e,t);it._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&it.dequeue(this,e)})},dequeue:function(e){return this.each(function(){it.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=it.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};for("string"!=typeof e&&(t=e,e=void 0),e=e||"fx";a--;)n=it._data(o[a],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var Nt=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,St=["Top","Right","Bottom","Left"],jt=function(e,t){return e=t||e,"none"===it.css(e,"display")||!it.contains(e.ownerDocument,e)},At=it.access=function(e,t,n,r,i,o,a){var s=0,l=e.length,u=null==n;if("object"===it.type(n)){i=!0;for(s in n)it.access(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,it.isFunction(r)||(a=!0),u&&(a?(t.call(e,r),t=null):(u=t,t=function(e,t,n){return u.call(it(e),n)})),t))for(;l>s;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:u?t.call(e):l?t(e[0],n):o},Dt=/^(?:checkbox|radio)$/i;!function(){var e=ht.createElement("input"),t=ht.createElement("div"),n=ht.createDocumentFragment();if(t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",nt.leadingWhitespace=3===t.firstChild.nodeType,nt.tbody=!t.getElementsByTagName("tbody").length,nt.htmlSerialize=!!t.getElementsByTagName("link").length,nt.html5Clone="<:nav></:nav>"!==ht.createElement("nav").cloneNode(!0).outerHTML,e.type="checkbox",e.checked=!0,n.appendChild(e),nt.appendChecked=e.checked,t.innerHTML="<textarea>x</textarea>",nt.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,n.appendChild(t),t.innerHTML="<input type='radio' checked='checked' name='t'/>",nt.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,nt.noCloneEvent=!0,t.attachEvent&&(t.attachEvent("onclick",function(){nt.noCloneEvent=!1}),t.cloneNode(!0).click()),null==nt.deleteExpando){nt.deleteExpando=!0;try{delete t.test}catch(r){nt.deleteExpando=!1}}}(),function(){var t,n,r=ht.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(nt[t+"Bubbles"]=n in e)||(r.setAttribute(n,"t"),nt[t+"Bubbles"]=r.attributes[n].expando===!1);r=null}();var Lt=/^(?:input|select|textarea)$/i,Ht=/^key/,_t=/^(?:mouse|pointer|contextmenu)|click/,qt=/^(?:focusinfocus|focusoutblur)$/,Ft=/^([^.]*)(?:\.(.+)|)$/;it.event={global:{},add:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it._data(e);if(g){for(n.handler&&(l=n,n=l.handler,i=l.selector),n.guid||(n.guid=it.guid++),(a=g.events)||(a=g.events={}),(c=g.handle)||(c=g.handle=function(e){return typeof it===Et||e&&it.event.triggered===e.type?void 0:it.event.dispatch.apply(c.elem,arguments)},c.elem=e),t=(t||"").match(bt)||[""],s=t.length;s--;)o=Ft.exec(t[s])||[],p=m=o[1],h=(o[2]||"").split(".").sort(),p&&(u=it.event.special[p]||{},p=(i?u.delegateType:u.bindType)||p,u=it.event.special[p]||{},d=it.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&it.expr.match.needsContext.test(i),namespace:h.join(".")},l),(f=a[p])||(f=a[p]=[],f.delegateCount=0,u.setup&&u.setup.call(e,r,h,c)!==!1||(e.addEventListener?e.addEventListener(p,c,!1):e.attachEvent&&e.attachEvent("on"+p,c))),u.add&&(u.add.call(e,d),d.handler.guid||(d.handler.guid=n.guid)),i?f.splice(f.delegateCount++,0,d):f.push(d),it.event.global[p]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it.hasData(e)&&it._data(e);if(g&&(c=g.events)){for(t=(t||"").match(bt)||[""],u=t.length;u--;)if(s=Ft.exec(t[u])||[],p=m=s[1],h=(s[2]||"").split(".").sort(),p){for(d=it.event.special[p]||{},p=(r?d.delegateType:d.bindType)||p,f=c[p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;o--;)a=f[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,d.remove&&d.remove.call(e,a));l&&!f.length&&(d.teardown&&d.teardown.call(e,h,g.handle)!==!1||it.removeEvent(e,p,g.handle),delete c[p])}else for(p in c)it.event.remove(e,p+t[u],n,r,!0);it.isEmptyObject(c)&&(delete g.handle,it._removeData(e,"events"))}},trigger:function(t,n,r,i){var o,a,s,l,u,c,d,f=[r||ht],p=tt.call(t,"type")?t.type:t,h=tt.call(t,"namespace")?t.namespace.split("."):[];if(s=c=r=r||ht,3!==r.nodeType&&8!==r.nodeType&&!qt.test(p+it.event.triggered)&&(p.indexOf(".")>=0&&(h=p.split("."),p=h.shift(),h.sort()),a=p.indexOf(":")<0&&"on"+p,t=t[it.expando]?t:new it.Event(p,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=h.join("."),t.namespace_re=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:it.makeArray(n,[t]),u=it.event.special[p]||{},i||!u.trigger||u.trigger.apply(r,n)!==!1)){if(!i&&!u.noBubble&&!it.isWindow(r)){for(l=u.delegateType||p,qt.test(l+p)||(s=s.parentNode);s;s=s.parentNode)f.push(s),c=s;c===(r.ownerDocument||ht)&&f.push(c.defaultView||c.parentWindow||e)}for(d=0;(s=f[d++])&&!t.isPropagationStopped();)t.type=d>1?l:u.bindType||p,o=(it._data(s,"events")||{})[t.type]&&it._data(s,"handle"),o&&o.apply(s,n),o=a&&s[a],o&&o.apply&&it.acceptData(s)&&(t.result=o.apply(s,n),t.result===!1&&t.preventDefault());if(t.type=p,!i&&!t.isDefaultPrevented()&&(!u._default||u._default.apply(f.pop(),n)===!1)&&it.acceptData(r)&&a&&r[p]&&!it.isWindow(r)){c=r[a],c&&(r[a]=null),it.event.triggered=p;try{r[p]()}catch(m){}it.event.triggered=void 0,c&&(r[a]=c)}return t.result}},dispatch:function(e){e=it.event.fix(e);var t,n,r,i,o,a=[],s=Y.call(arguments),l=(it._data(this,"events")||{})[e.type]||[],u=it.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!u.preDispatch||u.preDispatch.call(this,e)!==!1){for(a=it.event.handlers.call(this,e,l),t=0;(i=a[t++])&&!e.isPropagationStopped();)for(e.currentTarget=i.elem,o=0;(r=i.handlers[o++])&&!e.isImmediatePropagationStopped();)(!e.namespace_re||e.namespace_re.test(r.namespace))&&(e.handleObj=r,e.data=r.data,n=((it.event.special[r.origType]||{}).handle||r.handler).apply(i.elem,s),void 0!==n&&(e.result=n)===!1&&(e.preventDefault(),e.stopPropagation()));return u.postDispatch&&u.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,l=e.target;if(s&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(i=[],o=0;s>o;o++)r=t[o],n=r.selector+" ",void 0===i[n]&&(i[n]=r.needsContext?it(n,this).index(l)>=0:it.find(n,this,null,[l]).length),i[n]&&i.push(r);i.length&&a.push({elem:l,handlers:i})}return s<t.length&&a.push({elem:this,handlers:t.slice(s)}),a},fix:function(e){if(e[it.expando])return e;var t,n,r,i=e.type,o=e,a=this.fixHooks[i];for(a||(this.fixHooks[i]=a=_t.test(i)?this.mouseHooks:Ht.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new it.Event(o),t=r.length;t--;)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||ht),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,a.filter?a.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,o=t.button,a=t.fromElement;return null==e.pageX&&null!=t.clientX&&(r=e.target.ownerDocument||ht,i=r.documentElement,n=r.body,e.pageX=t.clientX+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0),e.pageY=t.clientY+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?t.toElement:a),e.which||void 0===o||(e.which=1&o?1:2&o?3:4&o?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==h()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===h()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return it.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(e){return it.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=it.extend(new it.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?it.event.trigger(i,null,t):it.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},it.removeEvent=ht.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===Et&&(e[r]=null),e.detachEvent(r,n))},it.Event=function(e,t){return this instanceof it.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&e.returnValue===!1?f:p):this.type=e,t&&it.extend(this,t),this.timeStamp=e&&e.timeStamp||it.now(),void(this[it.expando]=!0)):new it.Event(e,t)},it.Event.prototype={isDefaultPrevented:p,isPropagationStopped:p,isImmediatePropagationStopped:p,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=f,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=f,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=f,e&&e.stopImmediatePropagation&&e.stopImmediatePropagation(),this.stopPropagation()}},it.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){it.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!it.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),nt.submitBubbles||(it.event.special.submit={setup:function(){return it.nodeName(this,"form")?!1:void it.event.add(this,"click._submit keypress._submit",function(e){var t=e.target,n=it.nodeName(t,"input")||it.nodeName(t,"button")?t.form:void 0;n&&!it._data(n,"submitBubbles")&&(it.event.add(n,"submit._submit",function(e){e._submit_bubble=!0}),it._data(n,"submitBubbles",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&it.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return it.nodeName(this,"form")?!1:void it.event.remove(this,"._submit")}}),nt.changeBubbles||(it.event.special.change={setup:function(){return Lt.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(it.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),it.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),it.event.simulate("change",this,e,!0)})),!1):void it.event.add(this,"beforeactivate._change",function(e){var t=e.target;Lt.test(t.nodeName)&&!it._data(t,"changeBubbles")&&(it.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||it.event.simulate("change",this.parentNode,e,!0)}),it._data(t,"changeBubbles",!0))})},handle:function(e){var t=e.target;return this!==t||e.isSimulated||e.isTrigger||"radio"!==t.type&&"checkbox"!==t.type?e.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return it.event.remove(this,"._change"),!Lt.test(this.nodeName)}}),nt.focusinBubbles||it.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){it.event.simulate(t,e.target,it.event.fix(e),!0)};it.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=it._data(r,t);i||r.addEventListener(e,n,!0),it._data(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=it._data(r,t)-1;i?it._data(r,t,i):(r.removeEventListener(e,n,!0),it._removeData(r,t))}}}),it.fn.extend({on:function(e,t,n,r,i){var o,a;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=void 0);for(o in e)this.on(o,t,n,e[o],i);return this}if(null==n&&null==r?(r=t,n=t=void 0):null==r&&("string"==typeof t?(r=n,n=void 0):(r=n,n=t,t=void 0)),r===!1)r=p;else if(!r)return this;return 1===i&&(a=r,r=function(e){return it().off(e),a.apply(this,arguments)},r.guid=a.guid||(a.guid=it.guid++)),this.each(function(){it.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,it(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=void 0),n===!1&&(n=p),this.each(function(){it.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){it.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?it.event.trigger(e,t,n,!0):void 0}});var Mt="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",Ot=/ jQuery\d+="(?:null|\d+)"/g,Rt=new RegExp("<(?:"+Mt+")[\\s/>]","i"),Bt=/^\s+/,Pt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,It=/<([\w:]+)/,Wt=/<tbody/i,$t=/<|&#?\w+;/,zt=/<(?:script|style|link)/i,Xt=/checked\s*(?:[^=]|=\s*.checked.)/i,Ut=/^$|\/(?:java|ecma)script/i,Vt=/^true\/(.*)/,Gt=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Yt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:nt.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},Jt=m(ht),Kt=Jt.appendChild(ht.createElement("div"));Yt.optgroup=Yt.option,Yt.tbody=Yt.tfoot=Yt.colgroup=Yt.caption=Yt.thead,Yt.th=Yt.td,it.extend({clone:function(e,t,n){var r,i,o,a,s,l=it.contains(e.ownerDocument,e);if(nt.html5Clone||it.isXMLDoc(e)||!Rt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Kt.innerHTML=e.outerHTML,Kt.removeChild(o=Kt.firstChild)),!(nt.noCloneEvent&&nt.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||it.isXMLDoc(e)))for(r=g(o),s=g(e),a=0;null!=(i=s[a]);++a)r[a]&&E(i,r[a]);if(t)if(n)for(s=s||g(e),r=r||g(o),a=0;null!=(i=s[a]);a++)T(i,r[a]);else T(e,o);return r=g(o,"script"),r.length>0&&w(r,!l&&g(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){for(var i,o,a,s,l,u,c,d=e.length,f=m(t),p=[],h=0;d>h;h++)if(o=e[h],o||0===o)if("object"===it.type(o))it.merge(p,o.nodeType?[o]:o);else if($t.test(o)){for(s=s||f.appendChild(t.createElement("div")),l=(It.exec(o)||["",""])[1].toLowerCase(),c=Yt[l]||Yt._default,s.innerHTML=c[1]+o.replace(Pt,"<$1></$2>")+c[2],i=c[0];i--;)s=s.lastChild;if(!nt.leadingWhitespace&&Bt.test(o)&&p.push(t.createTextNode(Bt.exec(o)[0])),!nt.tbody)for(o="table"!==l||Wt.test(o)?"<table>"!==c[1]||Wt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;i--;)it.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u);for(it.merge(p,s.childNodes),s.textContent="";s.firstChild;)s.removeChild(s.firstChild);s=f.lastChild}else p.push(t.createTextNode(o));for(s&&f.removeChild(s),nt.appendChecked||it.grep(g(p,"input"),v),h=0;o=p[h++];)if((!r||-1===it.inArray(o,r))&&(a=it.contains(o.ownerDocument,o),s=g(f.appendChild(o),"script"),a&&w(s),n))for(i=0;o=s[i++];)Ut.test(o.type||"")&&n.push(o);return s=null,f},cleanData:function(e,t){for(var n,r,i,o,a=0,s=it.expando,l=it.cache,u=nt.deleteExpando,c=it.event.special;null!=(n=e[a]);a++)if((t||it.acceptData(n))&&(i=n[s],o=i&&l[i])){if(o.events)for(r in o.events)c[r]?it.event.remove(n,r):it.removeEvent(n,r,o.handle);l[i]&&(delete l[i],u?delete n[s]:typeof n.removeAttribute!==Et?n.removeAttribute(s):n[s]=null,G.push(i))}}}),it.fn.extend({text:function(e){return At(this,function(e){return void 0===e?it.text(this):this.empty().append((this[0]&&this[0].ownerDocument||ht).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){for(var n,r=e?it.filter(e,this):this,i=0;null!=(n=r[i]);i++)t||1!==n.nodeType||it.cleanData(g(n)),n.parentNode&&(t&&it.contains(n.ownerDocument,n)&&w(g(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&it.cleanData(g(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&it.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return it.clone(this,e,t)})},html:function(e){return At(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(Ot,""):void 0;if(!("string"!=typeof e||zt.test(e)||!nt.htmlSerialize&&Rt.test(e)||!nt.leadingWhitespace&&Bt.test(e)||Yt[(It.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(Pt,"<$1></$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(it.cleanData(g(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=arguments[0];return this.domManip(arguments,function(t){e=this.parentNode,it.cleanData(g(this)),e&&e.replaceChild(t,this)}),e&&(e.length||e.nodeType)?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t){e=J.apply([],e);var n,r,i,o,a,s,l=0,u=this.length,c=this,d=u-1,f=e[0],p=it.isFunction(f);if(p||u>1&&"string"==typeof f&&!nt.checkClone&&Xt.test(f))return this.each(function(n){var r=c.eq(n);p&&(e[0]=f.call(this,n,r.html())),r.domManip(e,t)});if(u&&(s=it.buildFragment(e,this[0].ownerDocument,!1,this),n=s.firstChild,1===s.childNodes.length&&(s=n),n)){for(o=it.map(g(s,"script"),b),i=o.length;u>l;l++)r=s,l!==d&&(r=it.clone(r,!0,!0),i&&it.merge(o,g(r,"script"))),t.call(this[l],r,l);if(i)for(a=o[o.length-1].ownerDocument,it.map(o,x),l=0;i>l;l++)r=o[l],Ut.test(r.type||"")&&!it._data(r,"globalEval")&&it.contains(a,r)&&(r.src?it._evalUrl&&it._evalUrl(r.src):it.globalEval((r.text||r.textContent||r.innerHTML||"").replace(Gt,"")));s=n=null}return this}}),it.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){it.fn[e]=function(e){for(var n,r=0,i=[],o=it(e),a=o.length-1;a>=r;r++)n=r===a?this:this.clone(!0),it(o[r])[t](n),K.apply(i,n.get());return this.pushStack(i)}});var Qt,Zt={};!function(){var e;nt.shrinkWrapBlocks=function(){if(null!=e)return e;e=!1;var t,n,r;return n=ht.getElementsByTagName("body")[0],n&&n.style?(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",t.appendChild(ht.createElement("div")).style.width="5px",e=3!==t.offsetWidth),n.removeChild(r),e):void 0}}();var en,tn,nn=/^margin/,rn=new RegExp("^("+Nt+")(?!px)[a-z%]+$","i"),on=/^(top|right|bottom|left)$/;e.getComputedStyle?(en=function(e){return e.ownerDocument.defaultView.getComputedStyle(e,null)},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n.getPropertyValue(t)||n[t]:void 0,n&&(""!==a||it.contains(e.ownerDocument,e)||(a=it.style(e,t)),rn.test(a)&&nn.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0===a?a:a+""}):ht.documentElement.currentStyle&&(en=function(e){return e.currentStyle},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n[t]:void 0,null==a&&s&&s[t]&&(a=s[t]),rn.test(a)&&!on.test(t)&&(r=s.left,i=e.runtimeStyle,o=i&&i.left,o&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"}),function(){function t(){var t,n,r,i;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),t.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",o=a=!1,l=!0,e.getComputedStyle&&(o="1%"!==(e.getComputedStyle(t,null)||{}).top,a="4px"===(e.getComputedStyle(t,null)||{width:"4px"}).width,i=t.appendChild(ht.createElement("div")),i.style.cssText=t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",t.style.width="1px",l=!parseFloat((e.getComputedStyle(i,null)||{}).marginRight)),t.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=t.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",s=0===i[0].offsetHeight,s&&(i[0].style.display="",i[1].style.display="none",s=0===i[0].offsetHeight),n.removeChild(r))}var n,r,i,o,a,s,l;n=ht.createElement("div"),n.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",i=n.getElementsByTagName("a")[0],r=i&&i.style,r&&(r.cssText="float:left;opacity:.5",nt.opacity="0.5"===r.opacity,nt.cssFloat=!!r.cssFloat,n.style.backgroundClip="content-box",n.cloneNode(!0).style.backgroundClip="",nt.clearCloneStyle="content-box"===n.style.backgroundClip,nt.boxSizing=""===r.boxSizing||""===r.MozBoxSizing||""===r.WebkitBoxSizing,it.extend(nt,{reliableHiddenOffsets:function(){return null==s&&t(),s +},boxSizingReliable:function(){return null==a&&t(),a},pixelPosition:function(){return null==o&&t(),o},reliableMarginRight:function(){return null==l&&t(),l}}))}(),it.swap=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};var an=/alpha\([^)]*\)/i,sn=/opacity\s*=\s*([^)]*)/,ln=/^(none|table(?!-c[ea]).+)/,un=new RegExp("^("+Nt+")(.*)$","i"),cn=new RegExp("^([+-])=("+Nt+")","i"),dn={position:"absolute",visibility:"hidden",display:"block"},fn={letterSpacing:"0",fontWeight:"400"},pn=["Webkit","O","Moz","ms"];it.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=tn(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":nt.cssFloat?"cssFloat":"styleFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=it.camelCase(t),l=e.style;if(t=it.cssProps[s]||(it.cssProps[s]=S(l,s)),a=it.cssHooks[t]||it.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];if(o=typeof n,"string"===o&&(i=cn.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(it.css(e,t)),o="number"),null!=n&&n===n&&("number"!==o||it.cssNumber[s]||(n+="px"),nt.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),!(a&&"set"in a&&void 0===(n=a.set(e,n,r)))))try{l[t]=n}catch(u){}}},css:function(e,t,n,r){var i,o,a,s=it.camelCase(t);return t=it.cssProps[s]||(it.cssProps[s]=S(e.style,s)),a=it.cssHooks[t]||it.cssHooks[s],a&&"get"in a&&(o=a.get(e,!0,n)),void 0===o&&(o=tn(e,t,r)),"normal"===o&&t in fn&&(o=fn[t]),""===n||n?(i=parseFloat(o),n===!0||it.isNumeric(i)?i||0:o):o}}),it.each(["height","width"],function(e,t){it.cssHooks[t]={get:function(e,n,r){return n?ln.test(it.css(e,"display"))&&0===e.offsetWidth?it.swap(e,dn,function(){return L(e,t,r)}):L(e,t,r):void 0},set:function(e,n,r){var i=r&&en(e);return A(e,n,r?D(e,t,r,nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,i),i):0)}}}),nt.opacity||(it.cssHooks.opacity={get:function(e,t){return sn.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=it.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===it.trim(o.replace(an,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=an.test(o)?o.replace(an,i):o+" "+i)}}),it.cssHooks.marginRight=N(nt.reliableMarginRight,function(e,t){return t?it.swap(e,{display:"inline-block"},tn,[e,"marginRight"]):void 0}),it.each({margin:"",padding:"",border:"Width"},function(e,t){it.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];4>r;r++)i[e+St[r]+t]=o[r]||o[r-2]||o[0];return i}},nn.test(e)||(it.cssHooks[e+t].set=A)}),it.fn.extend({css:function(e,t){return At(this,function(e,t,n){var r,i,o={},a=0;if(it.isArray(t)){for(r=en(e),i=t.length;i>a;a++)o[t[a]]=it.css(e,t[a],!1,r);return o}return void 0!==n?it.style(e,t,n):it.css(e,t)},e,t,arguments.length>1)},show:function(){return j(this,!0)},hide:function(){return j(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){jt(this)?it(this).show():it(this).hide()})}}),it.Tween=H,H.prototype={constructor:H,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(it.cssNumber[n]?"":"px")},cur:function(){var e=H.propHooks[this.prop];return e&&e.get?e.get(this):H.propHooks._default.get(this)},run:function(e){var t,n=H.propHooks[this.prop];return this.pos=t=this.options.duration?it.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):H.propHooks._default.set(this),this}},H.prototype.init.prototype=H.prototype,H.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=it.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){it.fx.step[e.prop]?it.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[it.cssProps[e.prop]]||it.cssHooks[e.prop])?it.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},H.propHooks.scrollTop=H.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},it.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},it.fx=H.prototype.init,it.fx.step={};var hn,mn,gn=/^(?:toggle|show|hide)$/,vn=new RegExp("^(?:([+-])=|)("+Nt+")([a-z%]*)$","i"),yn=/queueHooks$/,bn=[M],xn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=vn.exec(t),o=i&&i[3]||(it.cssNumber[e]?"":"px"),a=(it.cssNumber[e]||"px"!==o&&+r)&&vn.exec(it.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,it.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};it.Animation=it.extend(R,{tweener:function(e,t){it.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");for(var n,r=0,i=e.length;i>r;r++)n=e[r],xn[n]=xn[n]||[],xn[n].unshift(t)},prefilter:function(e,t){t?bn.unshift(e):bn.push(e)}}),it.speed=function(e,t,n){var r=e&&"object"==typeof e?it.extend({},e):{complete:n||!n&&t||it.isFunction(e)&&e,duration:e,easing:n&&t||t&&!it.isFunction(t)&&t};return r.duration=it.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in it.fx.speeds?it.fx.speeds[r.duration]:it.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){it.isFunction(r.old)&&r.old.call(this),r.queue&&it.dequeue(this,r.queue)},r},it.fn.extend({fadeTo:function(e,t,n,r){return this.filter(jt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=it.isEmptyObject(e),o=it.speed(t,n,r),a=function(){var t=R(this,it.extend({},e),o);(i||it._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=it.timers,a=it._data(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&yn.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));(t||!n)&&it.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=it._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=it.timers,a=r?r.length:0;for(n.finish=!0,it.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),it.each(["toggle","show","hide"],function(e,t){var n=it.fn[t];it.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(q(t,!0),e,r,i)}}),it.each({slideDown:q("show"),slideUp:q("hide"),slideToggle:q("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){it.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),it.timers=[],it.fx.tick=function(){var e,t=it.timers,n=0;for(hn=it.now();n<t.length;n++)e=t[n],e()||t[n]!==e||t.splice(n--,1);t.length||it.fx.stop(),hn=void 0},it.fx.timer=function(e){it.timers.push(e),e()?it.fx.start():it.timers.pop()},it.fx.interval=13,it.fx.start=function(){mn||(mn=setInterval(it.fx.tick,it.fx.interval))},it.fx.stop=function(){clearInterval(mn),mn=null},it.fx.speeds={slow:600,fast:200,_default:400},it.fn.delay=function(e,t){return e=it.fx?it.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},function(){var e,t,n,r,i;t=ht.createElement("div"),t.setAttribute("className","t"),t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",r=t.getElementsByTagName("a")[0],n=ht.createElement("select"),i=n.appendChild(ht.createElement("option")),e=t.getElementsByTagName("input")[0],r.style.cssText="top:1px",nt.getSetAttribute="t"!==t.className,nt.style=/top/.test(r.getAttribute("style")),nt.hrefNormalized="/a"===r.getAttribute("href"),nt.checkOn=!!e.value,nt.optSelected=i.selected,nt.enctype=!!ht.createElement("form").enctype,n.disabled=!0,nt.optDisabled=!i.disabled,e=ht.createElement("input"),e.setAttribute("value",""),nt.input=""===e.getAttribute("value"),e.value="t",e.setAttribute("type","radio"),nt.radioValue="t"===e.value}();var wn=/\r/g;it.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=it.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,it(this).val()):e,null==i?i="":"number"==typeof i?i+="":it.isArray(i)&&(i=it.map(i,function(e){return null==e?"":e+""})),t=it.valHooks[this.type]||it.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return t=it.valHooks[i.type]||it.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(wn,""):null==n?"":n)}}}),it.extend({valHooks:{option:{get:function(e){var t=it.find.attr(e,"value");return null!=t?t:it.trim(it.text(e))}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(nt.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&it.nodeName(n.parentNode,"optgroup"))){if(t=it(n).val(),o)return t;a.push(t)}return a},set:function(e,t){for(var n,r,i=e.options,o=it.makeArray(t),a=i.length;a--;)if(r=i[a],it.inArray(it.valHooks.option.get(r),o)>=0)try{r.selected=n=!0}catch(s){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),it.each(["radio","checkbox"],function(){it.valHooks[this]={set:function(e,t){return it.isArray(t)?e.checked=it.inArray(it(e).val(),t)>=0:void 0}},nt.checkOn||(it.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Tn,En,kn=it.expr.attrHandle,Cn=/^(?:checked|selected)$/i,Nn=nt.getSetAttribute,Sn=nt.input;it.fn.extend({attr:function(e,t){return At(this,it.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){it.removeAttr(this,e)})}}),it.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(e&&3!==o&&8!==o&&2!==o)return typeof e.getAttribute===Et?it.prop(e,t,n):(1===o&&it.isXMLDoc(e)||(t=t.toLowerCase(),r=it.attrHooks[t]||(it.expr.match.bool.test(t)?En:Tn)),void 0===n?r&&"get"in r&&null!==(i=r.get(e,t))?i:(i=it.find.attr(e,t),null==i?void 0:i):null!==n?r&&"set"in r&&void 0!==(i=r.set(e,n,t))?i:(e.setAttribute(t,n+""),n):void it.removeAttr(e,t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(bt);if(o&&1===e.nodeType)for(;n=o[i++];)r=it.propFix[n]||n,it.expr.match.bool.test(n)?Sn&&Nn||!Cn.test(n)?e[r]=!1:e[it.camelCase("default-"+n)]=e[r]=!1:it.attr(e,n,""),e.removeAttribute(Nn?n:r)},attrHooks:{type:{set:function(e,t){if(!nt.radioValue&&"radio"===t&&it.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}}}),En={set:function(e,t,n){return t===!1?it.removeAttr(e,n):Sn&&Nn||!Cn.test(n)?e.setAttribute(!Nn&&it.propFix[n]||n,n):e[it.camelCase("default-"+n)]=e[n]=!0,n}},it.each(it.expr.match.bool.source.match(/\w+/g),function(e,t){var n=kn[t]||it.find.attr;kn[t]=Sn&&Nn||!Cn.test(t)?function(e,t,r){var i,o;return r||(o=kn[t],kn[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,kn[t]=o),i}:function(e,t,n){return n?void 0:e[it.camelCase("default-"+t)]?t.toLowerCase():null}}),Sn&&Nn||(it.attrHooks.value={set:function(e,t,n){return it.nodeName(e,"input")?void(e.defaultValue=t):Tn&&Tn.set(e,t,n)}}),Nn||(Tn={set:function(e,t,n){var r=e.getAttributeNode(n);return r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n)?t:void 0}},kn.id=kn.name=kn.coords=function(e,t,n){var r;return n?void 0:(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},it.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:void 0},set:Tn.set},it.attrHooks.contenteditable={set:function(e,t,n){Tn.set(e,""===t?!1:t,n)}},it.each(["width","height"],function(e,t){it.attrHooks[t]={set:function(e,n){return""===n?(e.setAttribute(t,"auto"),n):void 0}}})),nt.style||(it.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var jn=/^(?:input|select|textarea|button|object)$/i,An=/^(?:a|area)$/i;it.fn.extend({prop:function(e,t){return At(this,it.prop,e,t,arguments.length>1)},removeProp:function(e){return e=it.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(t){}})}}),it.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,a=e.nodeType;if(e&&3!==a&&8!==a&&2!==a)return o=1!==a||!it.isXMLDoc(e),o&&(t=it.propFix[t]||t,i=it.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=it.find.attr(e,"tabindex");return t?parseInt(t,10):jn.test(e.nodeName)||An.test(e.nodeName)&&e.href?0:-1}}}}),nt.hrefNormalized||it.each(["href","src"],function(e,t){it.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),nt.optSelected||(it.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),it.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){it.propFix[this.toLowerCase()]=this}),nt.enctype||(it.propFix.enctype="encoding");var Dn=/[\t\r\n\f]/g;it.fn.extend({addClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u="string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):" ")){for(o=0;i=t[o++];)r.indexOf(" "+i+" ")<0&&(r+=i+" ");a=it.trim(r),n.className!==a&&(n.className=a)}return this},removeClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u=0===arguments.length||"string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):"")){for(o=0;i=t[o++];)for(;r.indexOf(" "+i+" ")>=0;)r=r.replace(" "+i+" "," ");a=e?it.trim(r):"",n.className!==a&&(n.className=a)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):this.each(it.isFunction(e)?function(n){it(this).toggleClass(e.call(this,n,this.className,t),t)}:function(){if("string"===n)for(var t,r=0,i=it(this),o=e.match(bt)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else(n===Et||"boolean"===n)&&(this.className&&it._data(this,"__className__",this.className),this.className=this.className||e===!1?"":it._data(this,"__className__")||"")})},hasClass:function(e){for(var t=" "+e+" ",n=0,r=this.length;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(Dn," ").indexOf(t)>=0)return!0;return!1}}),it.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){it.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),it.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var Ln=it.now(),Hn=/\?/,_n=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;it.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=it.trim(t+"");return i&&!it.trim(i.replace(_n,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():it.error("Invalid JSON: "+t)},it.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{e.DOMParser?(r=new DOMParser,n=r.parseFromString(t,"text/xml")):(n=new ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(t))}catch(i){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||it.error("Invalid XML: "+t),n};var qn,Fn,Mn=/#.*$/,On=/([?&])_=[^&]*/,Rn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Bn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Pn=/^(?:GET|HEAD)$/,In=/^\/\//,Wn=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,$n={},zn={},Xn="*/".concat("*");try{Fn=location.href}catch(Un){Fn=ht.createElement("a"),Fn.href="",Fn=Fn.href}qn=Wn.exec(Fn.toLowerCase())||[],it.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Fn,type:"GET",isLocal:Bn.test(qn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Xn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":it.parseJSON,"text xml":it.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?I(I(e,it.ajaxSettings),t):I(it.ajaxSettings,e)},ajaxPrefilter:B($n),ajaxTransport:B(zn),ajax:function(e,t){function n(e,t,n,r){var i,c,v,y,x,T=t;2!==b&&(b=2,s&&clearTimeout(s),u=void 0,a=r||"",w.readyState=e>0?4:0,i=e>=200&&300>e||304===e,n&&(y=W(d,w,n)),y=$(d,y,w,i),i?(d.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(it.lastModified[o]=x),x=w.getResponseHeader("etag"),x&&(it.etag[o]=x)),204===e||"HEAD"===d.type?T="nocontent":304===e?T="notmodified":(T=y.state,c=y.data,v=y.error,i=!v)):(v=T,(e||!T)&&(T="error",0>e&&(e=0))),w.status=e,w.statusText=(t||T)+"",i?h.resolveWith(f,[c,T,w]):h.rejectWith(f,[w,T,v]),w.statusCode(g),g=void 0,l&&p.trigger(i?"ajaxSuccess":"ajaxError",[w,d,i?c:v]),m.fireWith(f,[w,T]),l&&(p.trigger("ajaxComplete",[w,d]),--it.active||it.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,i,o,a,s,l,u,c,d=it.ajaxSetup({},t),f=d.context||d,p=d.context&&(f.nodeType||f.jquery)?it(f):it.event,h=it.Deferred(),m=it.Callbacks("once memory"),g=d.statusCode||{},v={},y={},b=0,x="canceled",w={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=Rn.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=y[n]=y[n]||e,v[e]=t),this},overrideMimeType:function(e){return b||(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)g[t]=[g[t],e[t]];else w.always(e[w.status]);return this},abort:function(e){var t=e||x;return u&&u.abort(t),n(0,t),this}};if(h.promise(w).complete=m.add,w.success=w.done,w.error=w.fail,d.url=((e||d.url||Fn)+"").replace(Mn,"").replace(In,qn[1]+"//"),d.type=t.method||t.type||d.method||d.type,d.dataTypes=it.trim(d.dataType||"*").toLowerCase().match(bt)||[""],null==d.crossDomain&&(r=Wn.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]===qn[1]&&r[2]===qn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(qn[3]||("http:"===qn[1]?"80":"443")))),d.data&&d.processData&&"string"!=typeof d.data&&(d.data=it.param(d.data,d.traditional)),P($n,d,t,w),2===b)return w;l=d.global,l&&0===it.active++&&it.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!Pn.test(d.type),o=d.url,d.hasContent||(d.data&&(o=d.url+=(Hn.test(o)?"&":"?")+d.data,delete d.data),d.cache===!1&&(d.url=On.test(o)?o.replace(On,"$1_="+Ln++):o+(Hn.test(o)?"&":"?")+"_="+Ln++)),d.ifModified&&(it.lastModified[o]&&w.setRequestHeader("If-Modified-Since",it.lastModified[o]),it.etag[o]&&w.setRequestHeader("If-None-Match",it.etag[o])),(d.data&&d.hasContent&&d.contentType!==!1||t.contentType)&&w.setRequestHeader("Content-Type",d.contentType),w.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+Xn+"; q=0.01":""):d.accepts["*"]);for(i in d.headers)w.setRequestHeader(i,d.headers[i]);if(d.beforeSend&&(d.beforeSend.call(f,w,d)===!1||2===b))return w.abort();x="abort";for(i in{success:1,error:1,complete:1})w[i](d[i]);if(u=P(zn,d,t,w)){w.readyState=1,l&&p.trigger("ajaxSend",[w,d]),d.async&&d.timeout>0&&(s=setTimeout(function(){w.abort("timeout")},d.timeout));try{b=1,u.send(v,n)}catch(T){if(!(2>b))throw T;n(-1,T)}}else n(-1,"No Transport");return w},getJSON:function(e,t,n){return it.get(e,t,n,"json")},getScript:function(e,t){return it.get(e,void 0,t,"script")}}),it.each(["get","post"],function(e,t){it[t]=function(e,n,r,i){return it.isFunction(n)&&(i=i||r,r=n,n=void 0),it.ajax({url:e,type:t,dataType:i,data:n,success:r})}}),it.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){it.fn[t]=function(e){return this.on(t,e)}}),it._evalUrl=function(e){return it.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},it.fn.extend({wrapAll:function(e){if(it.isFunction(e))return this.each(function(t){it(this).wrapAll(e.call(this,t))});if(this[0]){var t=it(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return this.each(it.isFunction(e)?function(t){it(this).wrapInner(e.call(this,t))}:function(){var t=it(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=it.isFunction(e);return this.each(function(n){it(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){it.nodeName(this,"body")||it(this).replaceWith(this.childNodes)}).end()}}),it.expr.filters.hidden=function(e){return e.offsetWidth<=0&&e.offsetHeight<=0||!nt.reliableHiddenOffsets()&&"none"===(e.style&&e.style.display||it.css(e,"display"))},it.expr.filters.visible=function(e){return!it.expr.filters.hidden(e)};var Vn=/%20/g,Gn=/\[\]$/,Yn=/\r?\n/g,Jn=/^(?:submit|button|image|reset|file)$/i,Kn=/^(?:input|select|textarea|keygen)/i;it.param=function(e,t){var n,r=[],i=function(e,t){t=it.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=it.ajaxSettings&&it.ajaxSettings.traditional),it.isArray(e)||e.jquery&&!it.isPlainObject(e))it.each(e,function(){i(this.name,this.value)});else for(n in e)z(n,e[n],t,i);return r.join("&").replace(Vn,"+")},it.fn.extend({serialize:function(){return it.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=it.prop(this,"elements");return e?it.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!it(this).is(":disabled")&&Kn.test(this.nodeName)&&!Jn.test(e)&&(this.checked||!Dt.test(e))}).map(function(e,t){var n=it(this).val();return null==n?null:it.isArray(n)?it.map(n,function(e){return{name:t.name,value:e.replace(Yn,"\r\n")}}):{name:t.name,value:n.replace(Yn,"\r\n")}}).get()}}),it.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&X()||U()}:X;var Qn=0,Zn={},er=it.ajaxSettings.xhr();e.ActiveXObject&&it(e).on("unload",function(){for(var e in Zn)Zn[e](void 0,!0)}),nt.cors=!!er&&"withCredentials"in er,er=nt.ajax=!!er,er&&it.ajaxTransport(function(e){if(!e.crossDomain||nt.cors){var t;return{send:function(n,r){var i,o=e.xhr(),a=++Qn;if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)o[i]=e.xhrFields[i];e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest");for(i in n)void 0!==n[i]&&o.setRequestHeader(i,n[i]+"");o.send(e.hasContent&&e.data||null),t=function(n,i){var s,l,u;if(t&&(i||4===o.readyState))if(delete Zn[a],t=void 0,o.onreadystatechange=it.noop,i)4!==o.readyState&&o.abort();else{u={},s=o.status,"string"==typeof o.responseText&&(u.text=o.responseText);try{l=o.statusText}catch(c){l=""}s||!e.isLocal||e.crossDomain?1223===s&&(s=204):s=u.text?200:404}u&&r(s,l,u,o.getAllResponseHeaders())},e.async?4===o.readyState?setTimeout(t):o.onreadystatechange=Zn[a]=t:t()},abort:function(){t&&t(void 0,!0)}}}}),it.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return it.globalEval(e),e}}}),it.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),it.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=ht.head||it("head")[0]||ht.documentElement;return{send:function(r,i){t=ht.createElement("script"),t.async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||i(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var tr=[],nr=/(=)\?(?=&|$)|\?\?/;it.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=tr.pop()||it.expando+"_"+Ln++;return this[e]=!0,e}}),it.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=t.jsonp!==!1&&(nr.test(t.url)?"url":"string"==typeof t.data&&!(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&nr.test(t.data)&&"data");return s||"jsonp"===t.dataTypes[0]?(i=t.jsonpCallback=it.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(nr,"$1"+i):t.jsonp!==!1&&(t.url+=(Hn.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||it.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,tr.push(i)),a&&it.isFunction(o)&&o(a[0]),a=o=void 0}),"script"):void 0}),it.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||ht;var r=dt.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=it.buildFragment([e],t,i),i&&i.length&&it(i).remove(),it.merge([],r.childNodes))};var rr=it.fn.load;it.fn.load=function(e,t,n){if("string"!=typeof e&&rr)return rr.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>=0&&(r=it.trim(e.slice(s,e.length)),e=e.slice(0,s)),it.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(o="POST"),a.length>0&&it.ajax({url:e,type:o,dataType:"html",data:t}).done(function(e){i=arguments,a.html(r?it("<div>").append(it.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){a.each(n,i||[e.responseText,t,e])}),this},it.expr.filters.animated=function(e){return it.grep(it.timers,function(t){return e===t.elem}).length};var ir=e.document.documentElement;it.offset={setOffset:function(e,t,n){var r,i,o,a,s,l,u,c=it.css(e,"position"),d=it(e),f={};"static"===c&&(e.style.position="relative"),s=d.offset(),o=it.css(e,"top"),l=it.css(e,"left"),u=("absolute"===c||"fixed"===c)&&it.inArray("auto",[o,l])>-1,u?(r=d.position(),a=r.top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(l)||0),it.isFunction(t)&&(t=t.call(e,n,s)),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):d.css(f)}},it.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){it.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;if(o)return t=o.documentElement,it.contains(t,i)?(typeof i.getBoundingClientRect!==Et&&(r=i.getBoundingClientRect()),n=V(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===it.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),it.nodeName(e[0],"html")||(n=e.offset()),n.top+=it.css(e[0],"borderTopWidth",!0),n.left+=it.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-it.css(r,"marginTop",!0),left:t.left-n.left-it.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent||ir;e&&!it.nodeName(e,"html")&&"static"===it.css(e,"position");)e=e.offsetParent;return e||ir})}}),it.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);it.fn[e]=function(r){return At(this,function(e,r,i){var o=V(e);return void 0===i?o?t in o?o[t]:o.document.documentElement[r]:e[r]:void(o?o.scrollTo(n?it(o).scrollLeft():i,n?i:it(o).scrollTop()):e[r]=i)},e,r,arguments.length,null)}}),it.each(["top","left"],function(e,t){it.cssHooks[t]=N(nt.pixelPosition,function(e,n){return n?(n=tn(e,t),rn.test(n)?it(e).position()[t]+"px":n):void 0})}),it.each({Height:"height",Width:"width"},function(e,t){it.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){it.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(r===!0||i===!0?"margin":"border");return At(this,function(t,n,r){var i;return it.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?it.css(t,n,a):it.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),it.fn.size=function(){return this.length},it.fn.andSelf=it.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return it});var or=e.jQuery,ar=e.$;return it.noConflict=function(t){return e.$===it&&(e.$=ar),t&&e.jQuery===it&&(e.jQuery=or),it},typeof t===Et&&(e.jQuery=e.$=it),it}),function(e,t){e.rails!==t&&e.error("jquery-ujs has already been loaded!");var n,r=e(document);e.rails=n={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]",buttonClickSelector:"button[data-remote], button[data-confirm]",inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form",formInputClickSelector:"form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])",disableSelector:"input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled",enableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled",requiredInputSelector:"input[name][required]:not([disabled]),textarea[name][required]:not([disabled])",fileInputSelector:"input[type=file]",linkDisableSelector:"a[data-disable-with], a[data-disable]",buttonDisableSelector:"button[data-remote][data-disable-with], button[data-remote][data-disable]",CSRFProtection:function(t){var n=e('meta[name="csrf-token"]').attr("content");n&&t.setRequestHeader("X-CSRF-Token",n)},refreshCSRFTokens:function(){var t=e("meta[name=csrf-token]").attr("content"),n=e("meta[name=csrf-param]").attr("content");e('form input[name="'+n+'"]').val(t)},fire:function(t,n,r){var i=e.Event(n);return t.trigger(i,r),i.result!==!1},confirm:function(e){return confirm(e)},ajax:function(t){return e.ajax(t)},href:function(e){return e.attr("href")},handleRemote:function(r){var i,o,a,s,l,u,c,d;if(n.fire(r,"ajax:before")){if(s=r.data("cross-domain"),l=s===t?null:s,u=r.data("with-credentials")||null,c=r.data("type")||e.ajaxSettings&&e.ajaxSettings.dataType,r.is("form")){i=r.attr("method"),o=r.attr("action"),a=r.serializeArray();var f=r.data("ujs:submit-button");f&&(a.push(f),r.data("ujs:submit-button",null))}else r.is(n.inputChangeSelector)?(i=r.data("method"),o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):r.is(n.buttonClickSelector)?(i=r.data("method")||"get",o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):(i=r.data("method"),o=n.href(r),a=r.data("params")||null);return d={type:i||"GET",data:a,dataType:c,beforeSend:function(e,i){return i.dataType===t&&e.setRequestHeader("accept","*/*;q=0.5, "+i.accepts.script),n.fire(r,"ajax:beforeSend",[e,i])?void r.trigger("ajax:send",e):!1 +},success:function(e,t,n){r.trigger("ajax:success",[e,t,n])},complete:function(e,t){r.trigger("ajax:complete",[e,t])},error:function(e,t,n){r.trigger("ajax:error",[e,t,n])},crossDomain:l},u&&(d.xhrFields={withCredentials:u}),o&&(d.url=o),n.ajax(d)}return!1},handleMethod:function(r){var i=n.href(r),o=r.data("method"),a=r.attr("target"),s=e("meta[name=csrf-token]").attr("content"),l=e("meta[name=csrf-param]").attr("content"),u=e('<form method="post" action="'+i+'"></form>'),c='<input name="_method" value="'+o+'" type="hidden" />';l!==t&&s!==t&&(c+='<input name="'+l+'" value="'+s+'" type="hidden" />'),a&&u.attr("target",a),u.hide().append(c).appendTo("body"),u.submit()},formElements:function(t,n){return t.is("form")?e(t[0].elements).filter(n):t.find(n)},disableFormElements:function(t){n.formElements(t,n.disableSelector).each(function(){n.disableFormElement(e(this))})},disableFormElement:function(e){var n,r;n=e.is("button")?"html":"val",r=e.data("disable-with"),e.data("ujs:enable-with",e[n]()),r!==t&&e[n](r),e.prop("disabled",!0)},enableFormElements:function(t){n.formElements(t,n.enableSelector).each(function(){n.enableFormElement(e(this))})},enableFormElement:function(e){var t=e.is("button")?"html":"val";e.data("ujs:enable-with")&&e[t](e.data("ujs:enable-with")),e.prop("disabled",!1)},allowAction:function(e){var t,r=e.data("confirm"),i=!1;return r?(n.fire(e,"confirm")&&(i=n.confirm(r),t=n.fire(e,"confirm:complete",[i])),i&&t):!0},blankInputs:function(t,n,r){var i,o,a=e(),s=n||"input,textarea",l=t.find(s);return l.each(function(){if(i=e(this),o=i.is("input[type=checkbox],input[type=radio]")?i.is(":checked"):i.val(),!o==!r){if(i.is("input[type=radio]")&&l.filter('input[type=radio]:checked[name="'+i.attr("name")+'"]').length)return!0;a=a.add(i)}}),a.length?a:!1},nonBlankInputs:function(e,t){return n.blankInputs(e,t,!0)},stopEverything:function(t){return e(t.target).trigger("ujs:everythingStopped"),t.stopImmediatePropagation(),!1},disableElement:function(e){var r=e.data("disable-with");e.data("ujs:enable-with",e.html()),r!==t&&e.html(r),e.bind("click.railsDisable",function(e){return n.stopEverything(e)})},enableElement:function(e){e.data("ujs:enable-with")!==t&&(e.html(e.data("ujs:enable-with")),e.removeData("ujs:enable-with")),e.unbind("click.railsDisable")}},n.fire(r,"rails:attachBindings")&&(e.ajaxPrefilter(function(e,t,r){e.crossDomain||n.CSRFProtection(r)}),r.delegate(n.linkDisableSelector,"ajax:complete",function(){n.enableElement(e(this))}),r.delegate(n.buttonDisableSelector,"ajax:complete",function(){n.enableFormElement(e(this))}),r.delegate(n.linkClickSelector,"click.rails",function(r){var i=e(this),o=i.data("method"),a=i.data("params"),s=r.metaKey||r.ctrlKey;if(!n.allowAction(i))return n.stopEverything(r);if(!s&&i.is(n.linkDisableSelector)&&n.disableElement(i),i.data("remote")!==t){if(s&&(!o||"GET"===o)&&!a)return!0;var l=n.handleRemote(i);return l===!1?n.enableElement(i):l.error(function(){n.enableElement(i)}),!1}return i.data("method")?(n.handleMethod(i),!1):void 0}),r.delegate(n.buttonClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);r.is(n.buttonDisableSelector)&&n.disableFormElement(r);var i=n.handleRemote(r);return i===!1?n.enableFormElement(r):i.error(function(){n.enableFormElement(r)}),!1}),r.delegate(n.inputChangeSelector,"change.rails",function(t){var r=e(this);return n.allowAction(r)?(n.handleRemote(r),!1):n.stopEverything(t)}),r.delegate(n.formSubmitSelector,"submit.rails",function(r){var i,o,a=e(this),s=a.data("remote")!==t;if(!n.allowAction(a))return n.stopEverything(r);if(a.attr("novalidate")==t&&(i=n.blankInputs(a,n.requiredInputSelector),i&&n.fire(a,"ajax:aborted:required",[i])))return n.stopEverything(r);if(s){if(o=n.nonBlankInputs(a,n.fileInputSelector)){setTimeout(function(){n.disableFormElements(a)},13);var l=n.fire(a,"ajax:aborted:file",[o]);return l||setTimeout(function(){n.enableFormElements(a)},13),l}return n.handleRemote(a),!1}setTimeout(function(){n.disableFormElements(a)},13)}),r.delegate(n.formInputClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);var i=r.attr("name"),o=i?{name:i,value:r.val()}:null;r.closest("form").data("ujs:submit-button",o)}),r.delegate(n.formSubmitSelector,"ajax:send.rails",function(t){this==t.target&&n.disableFormElements(e(this))}),r.delegate(n.formSubmitSelector,"ajax:complete.rails",function(t){this==t.target&&n.enableFormElements(e(this))}),e(function(){n.refreshCSRFTokens()}))}(jQuery),function(){var e,t,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v,y,b,x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B,P,I,W,$,z,X,U,V,G,Y=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1},J={}.hasOwnProperty,K=function(e,t){function n(){this.constructor=e}for(var r in t)J.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},Q=[].slice;j={},d=10,$=!1,m=null,S=null,q=null,h=null,V=null,b=function(e){var t;return e=new n(e),B(),c(),F(e),$&&(t=z(e.absolute))?(x(t),w(e)):w(e,W)},z=function(e){var t;return t=j[e],t&&!t.transitionCacheDisabled?t:void 0},g=function(e){return null==e&&(e=!0),$=e},w=function(e,t){return null==t&&(t=function(){return function(){}}(this)),X("page:fetch",{url:e.absolute}),null!=V&&V.abort(),V=new XMLHttpRequest,V.open("GET",e.withoutHashForIE10compatibility(),!0),V.setRequestHeader("Accept","text/html, application/xhtml+xml, application/xml"),V.setRequestHeader("X-XHR-Referer",q),V.onload=function(){var n;return X("page:receive"),(n=H())?(f.apply(null,y(n)),M(),t(),X("page:load")):document.location.href=e.absolute},V.onloadend=function(){return V=null},V.onerror=function(){return document.location.href=e.absolute},V.send()},x=function(e){return null!=V&&V.abort(),f(e.title,e.body),_(e),X("page:restore")},c=function(){var e;return e=new n(m.url),j[e.absolute]={url:e.relative,body:document.body,title:document.title,positionY:window.pageYOffset,positionX:window.pageXOffset,cachedAt:(new Date).getTime(),transitionCacheDisabled:null!=document.querySelector("[data-no-transition-cache]")},p(d)},D=function(e){return null==e&&(e=d),/^[\d]+$/.test(e)?d=parseInt(e):void 0},p=function(e){var t,n,r,i,o,a;for(r=Object.keys(j),t=r.map(function(e){return j[e].cachedAt}).sort(function(e,t){return t-e}),a=[],i=0,o=r.length;o>i;i++)n=r[i],j[n].cachedAt<=t[e]&&(X("page:expire",j[n]),a.push(delete j[n]));return a},f=function(t,n,r,i){return document.title=t,document.documentElement.replaceChild(n,document.body),null!=r&&e.update(r),i&&v(),m=window.history.state,X("page:change"),X("page:update")},v=function(){var e,t,n,r,i,o,a,s,l,u,c,d;for(o=Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])')),a=0,l=o.length;l>a;a++)if(i=o[a],""===(c=i.type)||"text/javascript"===c){for(t=document.createElement("script"),d=i.attributes,s=0,u=d.length;u>s;s++)e=d[s],t.setAttribute(e.name,e.value);t.appendChild(document.createTextNode(i.innerHTML)),r=i.parentNode,n=i.nextSibling,r.removeChild(i),r.insertBefore(t,n)}},P=function(e){return e.innerHTML=e.innerHTML.replace(/<noscript[\S\s]*?<\/noscript>/gi,""),e},F=function(e){return(e=new n(e)).absolute!==q?window.history.pushState({turbolinks:!0,url:e.absolute},"",e.absolute):void 0},M=function(){var e,t;return(e=V.getResponseHeader("X-XHR-Redirected-To"))?(e=new n(e),t=e.hasNoHash()?document.location.hash:"",window.history.replaceState(m,"",e.href+t)):void 0},B=function(){return q=document.location.href},R=function(){return window.history.replaceState({turbolinks:!0,url:document.location.href},"",document.location.href)},O=function(){return m=window.history.state},_=function(e){return window.scrollTo(e.positionX,e.positionY)},W=function(){return document.location.hash?document.location.href=document.location.href:window.scrollTo(0,0)},L=function(e){var t,n;return t=(null!=(n=document.cookie.match(new RegExp(e+"=(\\w+)")))?n[1].toUpperCase():void 0)||"",document.cookie=e+"=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/",t},X=function(e,t){var n;return n=document.createEvent("Events"),t&&(n.data=t),n.initEvent(e,!0,!0),document.dispatchEvent(n)},A=function(){return!X("page:before-change")},H=function(){var e,t,n,r,i,o;return t=function(){var e;return 400<=(e=V.status)&&600>e},o=function(){return V.getResponseHeader("Content-Type").match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/)},r=function(e){var t,n,r,i,o;for(i=e.head.childNodes,o=[],n=0,r=i.length;r>n;n++)t=i[n],null!=("function"==typeof t.getAttribute?t.getAttribute("data-turbolinks-track"):void 0)&&o.push(t.getAttribute("src")||t.getAttribute("href"));return o},e=function(e){var t;return S||(S=r(document)),t=r(e),t.length!==S.length||i(t,S).length!==S.length},i=function(e,t){var n,r,i,o,a;for(e.length>t.length&&(o=[t,e],e=o[0],t=o[1]),a=[],r=0,i=e.length;i>r;r++)n=e[r],Y.call(t,n)>=0&&a.push(n);return a},!t()&&o()&&(n=h(V.responseText),n&&!e(n))?n:void 0},y=function(t){var n;return n=t.querySelector("title"),[null!=n?n.textContent:void 0,P(t.body),e.get(t).token,"runScripts"]},e={get:function(e){var t;return null==e&&(e=document),{node:t=e.querySelector('meta[name="csrf-token"]'),token:null!=t&&"function"==typeof t.getAttribute?t.getAttribute("content"):void 0}},update:function(e){var t;return t=this.get(),null!=t.token&&null!=e&&t.token!==e?t.node.setAttribute("content",e):void 0}},i=function(){var e,t,n,r,i,o;t=function(e){return(new DOMParser).parseFromString(e,"text/html")},e=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.documentElement.innerHTML=e,t},n=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.open("replace"),t.write(e),t.close(),t};try{if(window.DOMParser)return i=t("<html><body><p>test"),t}catch(a){return r=a,i=e("<html><body><p>test"),e}finally{if(1!==(null!=i&&null!=(o=i.body)?o.childNodes.length:void 0))return n}},n=function(){function e(t){return this.original=null!=t?t:document.location.href,this.original.constructor===e?this.original:void this._parse()}return e.prototype.withoutHash=function(){return this.href.replace(this.hash,"")},e.prototype.withoutHashForIE10compatibility=function(){return this.withoutHash()},e.prototype.hasNoHash=function(){return 0===this.hash.length},e.prototype._parse=function(){var e;return(null!=this.link?this.link:this.link=document.createElement("a")).href=this.original,e=this.link,this.href=e.href,this.protocol=e.protocol,this.host=e.host,this.hostname=e.hostname,this.port=e.port,this.pathname=e.pathname,this.search=e.search,this.hash=e.hash,this.origin=[this.protocol,"//",this.hostname].join(""),0!==this.port.length&&(this.origin+=":"+this.port),this.relative=[this.pathname,this.search,this.hash].join(""),this.absolute=this.href},e}(),r=function(e){function t(e){return this.link=e,this.link.constructor===t?this.link:(this.original=this.link.href,void t.__super__.constructor.apply(this,arguments))}return K(t,e),t.HTML_EXTENSIONS=["html"],t.allowExtensions=function(){var e,n,r,i;for(n=1<=arguments.length?Q.call(arguments,0):[],r=0,i=n.length;i>r;r++)e=n[r],t.HTML_EXTENSIONS.push(e);return t.HTML_EXTENSIONS},t.prototype.shouldIgnore=function(){return this._crossOrigin()||this._anchored()||this._nonHtml()||this._optOut()||this._target()},t.prototype._crossOrigin=function(){return this.origin!==(new n).origin},t.prototype._anchored=function(){var e;return(this.hash&&this.withoutHash())===(e=new n).withoutHash()||this.href===e.href+"#"},t.prototype._nonHtml=function(){return this.pathname.match(/\.[a-z]+$/g)&&!this.pathname.match(new RegExp("\\.(?:"+t.HTML_EXTENSIONS.join("|")+")?$","g"))},t.prototype._optOut=function(){var e,t;for(t=this.link;!e&&t!==document;)e=null!=t.getAttribute("data-no-turbolink"),t=t.parentNode;return e},t.prototype._target=function(){return 0!==this.link.target.length},t}(n),t=function(){function e(e){this.event=e,this.event.defaultPrevented||(this._extractLink(),this._validForTurbolinks()&&(A()||U(this.link.href),this.event.preventDefault()))}return e.installHandlerLast=function(t){return t.defaultPrevented?void 0:(document.removeEventListener("click",e.handle,!1),document.addEventListener("click",e.handle,!1))},e.handle=function(t){return new e(t)},e.prototype._extractLink=function(){var e;for(e=this.event.target;e.parentNode&&"A"!==e.nodeName;)e=e.parentNode;return"A"===e.nodeName&&0!==e.href.length?this.link=new r(e):void 0},e.prototype._validForTurbolinks=function(){return null!=this.link&&!(this.link.shouldIgnore()||this._nonStandardClick())},e.prototype._nonStandardClick=function(){return this.event.which>1||this.event.metaKey||this.event.ctrlKey||this.event.shiftKey||this.event.altKey},e}(),u=function(e){return setTimeout(e,500)},k=function(){return document.addEventListener("DOMContentLoaded",function(){return X("page:change"),X("page:update")},!0)},N=function(){return"undefined"!=typeof jQuery?jQuery(document).on("ajaxSuccess",function(e,t){return jQuery.trim(t.responseText)?X("page:update"):void 0}):void 0},C=function(e){var t,r;return(null!=(r=e.state)?r.turbolinks:void 0)?(t=j[new n(e.state.url).absolute])?(c(),x(t)):U(e.target.location.href):void 0},E=function(){return R(),O(),h=i(),document.addEventListener("click",t.installHandlerLast,!0),u(function(){return window.addEventListener("popstate",C,!1)})},T=void 0!==window.history.state||navigator.userAgent.match(/Firefox\/2[6|7]/),s=window.history&&window.history.pushState&&window.history.replaceState&&T,o=!navigator.userAgent.match(/CriOS\//),I="GET"===(G=L("request_method"))||""===G,l=s&&o&&I,a=document.addEventListener&&document.createEvent,a&&(k(),N()),l?(U=b,E()):U=function(e){return document.location.href=e},this.Turbolinks={visit:U,pagesCached:D,enableTransitionCache:g,allowLinkExtensions:r.allowExtensions,supported:l}}.call(this),function(){}.call(this);
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz b/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz Binary files differnew file mode 100644 index 0000000000..f62c656dc8 --- /dev/null +++ b/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz diff --git a/actionpack/test/fixtures/公共/gzip/foo.zoo b/actionpack/test/fixtures/公共/gzip/foo.zoo new file mode 100644 index 0000000000..1826a7660e --- /dev/null +++ b/actionpack/test/fixtures/公共/gzip/foo.zoo @@ -0,0 +1,4 @@ +!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){function n(e){var t=e.length,n=it.type(e);return"function"===n||it.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e}function r(e,t,n){if(it.isFunction(t))return it.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return it.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ft.test(t))return it.filter(t,e,n);t=it.filter(t,e)}return it.grep(e,function(e){return it.inArray(e,t)>=0!==n})}function i(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function o(e){var t=xt[e]={};return it.each(e.match(bt)||[],function(e,n){t[n]=!0}),t}function a(){ht.addEventListener?(ht.removeEventListener("DOMContentLoaded",s,!1),e.removeEventListener("load",s,!1)):(ht.detachEvent("onreadystatechange",s),e.detachEvent("onload",s))}function s(){(ht.addEventListener||"load"===event.type||"complete"===ht.readyState)&&(a(),it.ready())}function l(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(Ct,"-$1").toLowerCase();if(n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:kt.test(n)?it.parseJSON(n):n}catch(i){}it.data(e,t,n)}else n=void 0}return n}function u(e){var t;for(t in e)if(("data"!==t||!it.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function c(e,t,n,r){if(it.acceptData(e)){var i,o,a=it.expando,s=e.nodeType,l=s?it.cache:e,u=s?e[a]:e[a]&&a;if(u&&l[u]&&(r||l[u].data)||void 0!==n||"string"!=typeof t)return u||(u=s?e[a]=G.pop()||it.guid++:a),l[u]||(l[u]=s?{}:{toJSON:it.noop}),("object"==typeof t||"function"==typeof t)&&(r?l[u]=it.extend(l[u],t):l[u].data=it.extend(l[u].data,t)),o=l[u],r||(o.data||(o.data={}),o=o.data),void 0!==n&&(o[it.camelCase(t)]=n),"string"==typeof t?(i=o[t],null==i&&(i=o[it.camelCase(t)])):i=o,i}}function d(e,t,n){if(it.acceptData(e)){var r,i,o=e.nodeType,a=o?it.cache:e,s=o?e[it.expando]:it.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){it.isArray(t)?t=t.concat(it.map(t,it.camelCase)):t in r?t=[t]:(t=it.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;for(;i--;)delete r[t[i]];if(n?!u(r):!it.isEmptyObject(r))return}(n||(delete a[s].data,u(a[s])))&&(o?it.cleanData([e],!0):nt.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}function f(){return!0}function p(){return!1}function h(){try{return ht.activeElement}catch(e){}}function m(e){var t=Mt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function g(e,t){var n,r,i=0,o=typeof e.getElementsByTagName!==Et?e.getElementsByTagName(t||"*"):typeof e.querySelectorAll!==Et?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||it.nodeName(r,t)?o.push(r):it.merge(o,g(r,t));return void 0===t||t&&it.nodeName(e,t)?it.merge([e],o):o}function v(e){Dt.test(e.type)&&(e.defaultChecked=e.checked)}function y(e,t){return it.nodeName(e,"table")&&it.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function b(e){return e.type=(null!==it.find.attr(e,"type"))+"/"+e.type,e}function x(e){var t=Vt.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function w(e,t){for(var n,r=0;null!=(n=e[r]);r++)it._data(n,"globalEval",!t||it._data(t[r],"globalEval"))}function T(e,t){if(1===t.nodeType&&it.hasData(e)){var n,r,i,o=it._data(e),a=it._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)it.event.add(t,n,s[n][r])}a.data&&(a.data=it.extend({},a.data))}}function E(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!nt.noCloneEvent&&t[it.expando]){i=it._data(t);for(r in i.events)it.removeEvent(t,r,i.handle);t.removeAttribute(it.expando)}"script"===n&&t.text!==e.text?(b(t).text=e.text,x(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),nt.html5Clone&&e.innerHTML&&!it.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Dt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function k(t,n){var r,i=it(n.createElement(t)).appendTo(n.body),o=e.getDefaultComputedStyle&&(r=e.getDefaultComputedStyle(i[0]))?r.display:it.css(i[0],"display");return i.detach(),o}function C(e){var t=ht,n=Zt[e];return n||(n=k(e,t),"none"!==n&&n||(Qt=(Qt||it("<iframe frameborder='0' width='0' height='0'/>")).appendTo(t.documentElement),t=(Qt[0].contentWindow||Qt[0].contentDocument).document,t.write(),t.close(),n=k(e,t),Qt.detach()),Zt[e]=n),n}function N(e,t){return{get:function(){var n=e();if(null!=n)return n?void delete this.get:(this.get=t).apply(this,arguments)}}}function S(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=pn.length;i--;)if(t=pn[i]+n,t in e)return t;return r}function j(e,t){for(var n,r,i,o=[],a=0,s=e.length;s>a;a++)r=e[a],r.style&&(o[a]=it._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&jt(r)&&(o[a]=it._data(r,"olddisplay",C(r.nodeName)))):(i=jt(r),(n&&"none"!==n||!i)&&it._data(r,"olddisplay",i?n:it.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}function A(e,t,n){var r=un.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function D(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=it.css(e,n+St[o],!0,i)),r?("content"===n&&(a-=it.css(e,"padding"+St[o],!0,i)),"margin"!==n&&(a-=it.css(e,"border"+St[o]+"Width",!0,i))):(a+=it.css(e,"padding"+St[o],!0,i),"padding"!==n&&(a+=it.css(e,"border"+St[o]+"Width",!0,i)));return a}function L(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=en(e),a=nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=tn(e,t,o),(0>i||null==i)&&(i=e.style[t]),rn.test(i))return i;r=a&&(nt.boxSizingReliable()||i===e.style[t]),i=parseFloat(i)||0}return i+D(e,t,n||(a?"border":"content"),r,o)+"px"}function H(e,t,n,r,i){return new H.prototype.init(e,t,n,r,i)}function _(){return setTimeout(function(){hn=void 0}),hn=it.now()}function q(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=St[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function F(e,t,n){for(var r,i=(xn[t]||[]).concat(xn["*"]),o=0,a=i.length;a>o;o++)if(r=i[o].call(n,t,e))return r}function M(e,t,n){var r,i,o,a,s,l,u,c,d=this,f={},p=e.style,h=e.nodeType&&jt(e),m=it._data(e,"fxshow");n.queue||(s=it._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,d.always(function(){d.always(function(){s.unqueued--,it.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],u=it.css(e,"display"),c="none"===u?it._data(e,"olddisplay")||C(e.nodeName):u,"inline"===c&&"none"===it.css(e,"float")&&(nt.inlineBlockNeedsLayout&&"inline"!==C(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",nt.shrinkWrapBlocks()||d.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],gn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(h?"hide":"show")){if("show"!==i||!m||void 0===m[r])continue;h=!0}f[r]=m&&m[r]||it.style(e,r)}else u=void 0;if(it.isEmptyObject(f))"inline"===("none"===u?C(e.nodeName):u)&&(p.display=u);else{m?"hidden"in m&&(h=m.hidden):m=it._data(e,"fxshow",{}),o&&(m.hidden=!h),h?it(e).show():d.done(function(){it(e).hide()}),d.done(function(){var t;it._removeData(e,"fxshow");for(t in f)it.style(e,t,f[t])});for(r in f)a=F(h?m[r]:0,r,d),r in m||(m[r]=a.start,h&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function O(e,t){var n,r,i,o,a;for(n in e)if(r=it.camelCase(n),i=t[r],o=e[n],it.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=it.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function R(e,t,n){var r,i,o=0,a=bn.length,s=it.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;for(var t=hn||_(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:it.extend({},t),opts:it.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:hn||_(),duration:n.duration,tweens:[],createTween:function(t,n){var r=it.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(O(c,u.opts.specialEasing);a>o;o++)if(r=bn[o].call(u,e,c,u.opts))return r;return it.map(c,F,u),it.isFunction(u.opts.start)&&u.opts.start.call(e,u),it.fx.timer(it.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function B(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(bt)||[];if(it.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function P(e,t,n,r){function i(s){var l;return o[s]=!0,it.each(e[s]||[],function(e,s){var u=s(t,n,r);return"string"!=typeof u||a||o[u]?a?!(l=u):void 0:(t.dataTypes.unshift(u),i(u),!1)}),l}var o={},a=e===zn;return i(t.dataTypes[0])||!o["*"]&&i("*")}function I(e,t){var n,r,i=it.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&it.extend(!0,e,n),e}function W(e,t,n){for(var r,i,o,a,s=e.contents,l=e.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){l.unshift(a);break}if(l[0]in n)o=l[0];else{for(a in n){if(!l[0]||e.converters[a+" "+l[0]]){o=a;break}r||(r=a)}o=o||r}return o?(o!==l[0]&&l.unshift(o),n[o]):void 0}function $(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(d){return{state:"parsererror",error:a?d:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}function z(e,t,n,r){var i;if(it.isArray(t))it.each(t,function(t,i){n||Gn.test(e)?r(e,i):z(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==it.type(t))r(e,t);else for(i in t)z(e+"["+i+"]",t[i],n,r)}function X(){try{return new e.XMLHttpRequest}catch(t){}}function U(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function V(e){return it.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}var G=[],Y=G.slice,J=G.concat,K=G.push,Q=G.indexOf,Z={},et=Z.toString,tt=Z.hasOwnProperty,nt={},rt="1.11.1",it=function(e,t){return new it.fn.init(e,t)},ot=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,at=/^-ms-/,st=/-([\da-z])/gi,lt=function(e,t){return t.toUpperCase()};it.fn=it.prototype={jquery:rt,constructor:it,selector:"",length:0,toArray:function(){return Y.call(this)},get:function(e){return null!=e?0>e?this[e+this.length]:this[e]:Y.call(this)},pushStack:function(e){var t=it.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return it.each(this,e,t)},map:function(e){return this.pushStack(it.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(Y.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:K,sort:G.sort,splice:G.splice},it.extend=it.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,l=arguments.length,u=!1;for("boolean"==typeof a&&(u=a,a=arguments[s]||{},s++),"object"==typeof a||it.isFunction(a)||(a={}),s===l&&(a=this,s--);l>s;s++)if(null!=(i=arguments[s]))for(r in i)e=a[r],n=i[r],a!==n&&(u&&n&&(it.isPlainObject(n)||(t=it.isArray(n)))?(t?(t=!1,o=e&&it.isArray(e)?e:[]):o=e&&it.isPlainObject(e)?e:{},a[r]=it.extend(u,o,n)):void 0!==n&&(a[r]=n));return a},it.extend({expando:"jQuery"+(rt+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isFunction:function(e){return"function"===it.type(e)},isArray:Array.isArray||function(e){return"array"===it.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!it.isArray(e)&&e-parseFloat(e)>=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==it.type(e)||e.nodeType||it.isWindow(e))return!1;try{if(e.constructor&&!tt.call(e,"constructor")&&!tt.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}if(nt.ownLast)for(t in e)return tt.call(e,t);for(t in e);return void 0===t||tt.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?Z[et.call(e)]||"object":typeof e},globalEval:function(t){t&&it.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(at,"ms-").replace(st,lt)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,r){var i,o=0,a=e.length,s=n(e);if(r){if(s)for(;a>o&&(i=t.apply(e[o],r),i!==!1);o++);else for(o in e)if(i=t.apply(e[o],r),i===!1)break}else if(s)for(;a>o&&(i=t.call(e[o],o,e[o]),i!==!1);o++);else for(o in e)if(i=t.call(e[o],o,e[o]),i===!1)break;return e},trim:function(e){return null==e?"":(e+"").replace(ot,"")},makeArray:function(e,t){var r=t||[];return null!=e&&(n(Object(e))?it.merge(r,"string"==typeof e?[e]:e):K.call(r,e)),r},inArray:function(e,t,n){var r;if(t){if(Q)return Q.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;n>r;)e[i++]=t[r++];if(n!==n)for(;void 0!==t[r];)e[i++]=t[r++];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;a>o;o++)r=!t(e[o],o),r!==s&&i.push(e[o]);return i},map:function(e,t,r){var i,o=0,a=e.length,s=n(e),l=[];if(s)for(;a>o;o++)i=t(e[o],o,r),null!=i&&l.push(i);else for(o in e)i=t(e[o],o,r),null!=i&&l.push(i);return J.apply([],l)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(i=e[t],t=e,e=i),it.isFunction(e)?(n=Y.call(arguments,2),r=function(){return e.apply(t||this,n.concat(Y.call(arguments)))},r.guid=e.guid=e.guid||it.guid++,r):void 0},now:function(){return+new Date},support:nt}),it.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){Z["[object "+t+"]"]=t.toLowerCase()});var ut=function(e){function t(e,t,n,r){var i,o,a,s,l,u,d,p,h,m;if((t?t.ownerDocument||t:P)!==H&&L(t),t=t||H,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(q&&!r){if(i=yt.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&R(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return Z.apply(n,t.getElementsByTagName(e)),n;if((a=i[3])&&w.getElementsByClassName&&t.getElementsByClassName)return Z.apply(n,t.getElementsByClassName(a)),n}if(w.qsa&&(!F||!F.test(e))){if(p=d=B,h=t,m=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){for(u=C(e),(d=t.getAttribute("id"))?p=d.replace(xt,"\\$&"):t.setAttribute("id",p),p="[id='"+p+"'] ",l=u.length;l--;)u[l]=p+f(u[l]);h=bt.test(e)&&c(t.parentNode)||t,m=u.join(",")}if(m)try{return Z.apply(n,h.querySelectorAll(m)),n}catch(g){}finally{d||t.removeAttribute("id")}}}return S(e.replace(lt,"$1"),t,n,r)}function n(){function e(n,r){return t.push(n+" ")>T.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[B]=!0,e}function i(e){var t=H.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=e.length;r--;)T.attrHandle[n[r]]=t}function a(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||G)-(~e.sourceIndex||G);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function s(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function l(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function u(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function c(e){return e&&typeof e.getElementsByTagName!==V&&e}function d(){}function f(e){for(var t=0,n=e.length,r="";n>t;t++)r+=e[t].value;return r}function p(e,t,n){var r=t.dir,i=n&&"parentNode"===r,o=W++;return t.first?function(t,n,o){for(;t=t[r];)if(1===t.nodeType||i)return e(t,n,o)}:function(t,n,a){var s,l,u=[I,o];if(a){for(;t=t[r];)if((1===t.nodeType||i)&&e(t,n,a))return!0}else for(;t=t[r];)if(1===t.nodeType||i){if(l=t[B]||(t[B]={}),(s=l[r])&&s[0]===I&&s[1]===o)return u[2]=s[2];if(l[r]=u,u[2]=e(t,n,a))return!0}}}function h(e){return e.length>1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function m(e,n,r){for(var i=0,o=n.length;o>i;i++)t(e,n[i],r);return r}function g(e,t,n,r,i){for(var o,a=[],s=0,l=e.length,u=null!=t;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function v(e,t,n,i,o,a){return i&&!i[B]&&(i=v(i)),o&&!o[B]&&(o=v(o,a)),r(function(r,a,s,l){var u,c,d,f=[],p=[],h=a.length,v=r||m(t||"*",s.nodeType?[s]:s,[]),y=!e||!r&&t?v:g(v,f,e,s,l),b=n?o||(r?e:h||i)?[]:a:y;if(n&&n(y,b,s,l),i)for(u=g(b,p),i(u,[],s,l),c=u.length;c--;)(d=u[c])&&(b[p[c]]=!(y[p[c]]=d));if(r){if(o||e){if(o){for(u=[],c=b.length;c--;)(d=b[c])&&u.push(y[c]=d);o(null,b=[],u,l)}for(c=b.length;c--;)(d=b[c])&&(u=o?tt.call(r,d):f[c])>-1&&(r[u]=!(a[u]=d))}}else b=g(b===a?b.splice(h,b.length):b),o?o(null,a,b,l):Z.apply(a,b)})}function y(e){for(var t,n,r,i=e.length,o=T.relative[e[0].type],a=o||T.relative[" "],s=o?1:0,l=p(function(e){return e===t},a,!0),u=p(function(e){return tt.call(t,e)>-1},a,!0),c=[function(e,n,r){return!o&&(r||n!==j)||((t=n).nodeType?l(e,n,r):u(e,n,r))}];i>s;s++)if(n=T.relative[e[s].type])c=[p(h(c),n)];else{if(n=T.filter[e[s].type].apply(null,e[s].matches),n[B]){for(r=++s;i>r&&!T.relative[e[r].type];r++);return v(s>1&&h(c),s>1&&f(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(lt,"$1"),n,r>s&&y(e.slice(s,r)),i>r&&y(e=e.slice(r)),i>r&&f(e))}c.push(n)}return h(c)}function b(e,n){var i=n.length>0,o=e.length>0,a=function(r,a,s,l,u){var c,d,f,p=0,h="0",m=r&&[],v=[],y=j,b=r||o&&T.find.TAG("*",u),x=I+=null==y?1:Math.random()||.1,w=b.length;for(u&&(j=a!==H&&a);h!==w&&null!=(c=b[h]);h++){if(o&&c){for(d=0;f=e[d++];)if(f(c,a,s)){l.push(c);break}u&&(I=x)}i&&((c=!f&&c)&&p--,r&&m.push(c))}if(p+=h,i&&h!==p){for(d=0;f=n[d++];)f(m,v,a,s);if(r){if(p>0)for(;h--;)m[h]||v[h]||(v[h]=K.call(l));v=g(v)}Z.apply(l,v),u&&!r&&v.length>0&&p+n.length>1&&t.uniqueSort(l)}return u&&(I=x,j=y),m};return i?r(a):a}var x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B="sizzle"+-new Date,P=e.document,I=0,W=0,$=n(),z=n(),X=n(),U=function(e,t){return e===t&&(D=!0),0},V="undefined",G=1<<31,Y={}.hasOwnProperty,J=[],K=J.pop,Q=J.push,Z=J.push,et=J.slice,tt=J.indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(this[t]===e)return t;return-1},nt="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",rt="[\\x20\\t\\r\\n\\f]",it="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",ot=it.replace("w","w#"),at="\\["+rt+"*("+it+")(?:"+rt+"*([*^$|!~]?=)"+rt+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+ot+"))|)"+rt+"*\\]",st=":("+it+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+at+")*)|.*)\\)|)",lt=new RegExp("^"+rt+"+|((?:^|[^\\\\])(?:\\\\.)*)"+rt+"+$","g"),ut=new RegExp("^"+rt+"*,"+rt+"*"),ct=new RegExp("^"+rt+"*([>+~]|"+rt+")"+rt+"*"),dt=new RegExp("="+rt+"*([^\\]'\"]*?)"+rt+"*\\]","g"),ft=new RegExp(st),pt=new RegExp("^"+ot+"$"),ht={ID:new RegExp("^#("+it+")"),CLASS:new RegExp("^\\.("+it+")"),TAG:new RegExp("^("+it.replace("w","w*")+")"),ATTR:new RegExp("^"+at),PSEUDO:new RegExp("^"+st),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+rt+"*(even|odd|(([+-]|)(\\d*)n|)"+rt+"*(?:([+-]|)"+rt+"*(\\d+)|))"+rt+"*\\)|)","i"),bool:new RegExp("^(?:"+nt+")$","i"),needsContext:new RegExp("^"+rt+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+rt+"*((?:-\\d)?\\d*)"+rt+"*\\)|)(?=[^-]|$)","i")},mt=/^(?:input|select|textarea|button)$/i,gt=/^h\d$/i,vt=/^[^{]+\{\s*\[native \w/,yt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,bt=/[+~]/,xt=/'|\\/g,wt=new RegExp("\\\\([\\da-f]{1,6}"+rt+"?|("+rt+")|.)","ig"),Tt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{Z.apply(J=et.call(P.childNodes),P.childNodes),J[P.childNodes.length].nodeType}catch(Et){Z={apply:J.length?function(e,t){Q.apply(e,et.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}w=t.support={},k=t.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},L=t.setDocument=function(e){var t,n=e?e.ownerDocument||e:P,r=n.defaultView;return n!==H&&9===n.nodeType&&n.documentElement?(H=n,_=n.documentElement,q=!k(n),r&&r!==r.top&&(r.addEventListener?r.addEventListener("unload",function(){L()},!1):r.attachEvent&&r.attachEvent("onunload",function(){L()})),w.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),w.getElementsByTagName=i(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),w.getElementsByClassName=vt.test(n.getElementsByClassName)&&i(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),w.getById=i(function(e){return _.appendChild(e).id=B,!n.getElementsByName||!n.getElementsByName(B).length}),w.getById?(T.find.ID=function(e,t){if(typeof t.getElementById!==V&&q){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){return e.getAttribute("id")===t}}):(delete T.find.ID,T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){var n=typeof e.getAttributeNode!==V&&e.getAttributeNode("id");return n&&n.value===t}}),T.find.TAG=w.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==V?t.getElementsByTagName(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},T.find.CLASS=w.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==V&&q?t.getElementsByClassName(e):void 0},M=[],F=[],(w.qsa=vt.test(n.querySelectorAll))&&(i(function(e){e.innerHTML="<select msallowclip=''><option selected=''></option></select>",e.querySelectorAll("[msallowclip^='']").length&&F.push("[*^$]="+rt+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||F.push("\\["+rt+"*(?:value|"+nt+")"),e.querySelectorAll(":checked").length||F.push(":checked")}),i(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&F.push("name"+rt+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||F.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),F.push(",.*:")})),(w.matchesSelector=vt.test(O=_.matches||_.webkitMatchesSelector||_.mozMatchesSelector||_.oMatchesSelector||_.msMatchesSelector))&&i(function(e){w.disconnectedMatch=O.call(e,"div"),O.call(e,"[s!='']:x"),M.push("!=",st)}),F=F.length&&new RegExp(F.join("|")),M=M.length&&new RegExp(M.join("|")),t=vt.test(_.compareDocumentPosition),R=t||vt.test(_.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},U=t?function(e,t){if(e===t)return D=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r?r:(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&r||!w.sortDetached&&t.compareDocumentPosition(e)===r?e===n||e.ownerDocument===P&&R(P,e)?-1:t===n||t.ownerDocument===P&&R(P,t)?1:A?tt.call(A,e)-tt.call(A,t):0:4&r?-1:1)}:function(e,t){if(e===t)return D=!0,0;var r,i=0,o=e.parentNode,s=t.parentNode,l=[e],u=[t];if(!o||!s)return e===n?-1:t===n?1:o?-1:s?1:A?tt.call(A,e)-tt.call(A,t):0;if(o===s)return a(e,t);for(r=e;r=r.parentNode;)l.unshift(r);for(r=t;r=r.parentNode;)u.unshift(r);for(;l[i]===u[i];)i++;return i?a(l[i],u[i]):l[i]===P?-1:u[i]===P?1:0},n):H},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==H&&L(e),n=n.replace(dt,"='$1']"),!(!w.matchesSelector||!q||M&&M.test(n)||F&&F.test(n)))try{var r=O.call(e,n);if(r||w.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return t(n,H,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==H&&L(e),R(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==H&&L(e);var n=T.attrHandle[t.toLowerCase()],r=n&&Y.call(T.attrHandle,t.toLowerCase())?n(e,t,!q):void 0;return void 0!==r?r:w.attributes||!q?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(D=!w.detectDuplicates,A=!w.sortStable&&e.slice(0),e.sort(U),D){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return A=null,e},E=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=E(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=E(t);return n},T=t.selectors={cacheLength:50,createPseudo:r,match:ht,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(wt,Tt),e[3]=(e[3]||e[4]||e[5]||"").replace(wt,Tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return ht.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&ft.test(n)&&(t=C(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(wt,Tt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=$[e+" "];return t||(t=new RegExp("(^|"+rt+")"+e+"("+rt+"|$)"))&&$(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==V&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:n?(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o+" ").indexOf(r)>-1:"|="===n?o===r||o.slice(0,r.length+1)===r+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,d,f,p,h,m=o!==a?"nextSibling":"previousSibling",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!l&&!s;if(g){if(o){for(;m;){for(d=t;d=d[m];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=m="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?g.firstChild:g.lastChild],a&&y){for(c=g[B]||(g[B]={}),u=c[e]||[],p=u[0]===I&&u[1],f=u[0]===I&&u[2],d=p&&g.childNodes[p];d=++p&&d&&d[m]||(f=p=0)||h.pop();)if(1===d.nodeType&&++f&&d===t){c[e]=[I,p,f];break}}else if(y&&(u=(t[B]||(t[B]={}))[e])&&u[0]===I)f=u[1];else for(;(d=++p&&d&&d[m]||(f=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++f||(y&&((d[B]||(d[B]={}))[e]=[I,f]),d!==t)););return f-=i,f===r||f%r===0&&f/r>=0}}},PSEUDO:function(e,n){var i,o=T.pseudos[e]||T.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[B]?o(n):o.length>1?(i=[e,e,"",n],T.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),a=i.length;a--;)r=tt.call(e,i[a]),e[r]=!(t[r]=i[a])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=N(e.replace(lt,"$1"));return i[B]?r(function(e,t,n,r){for(var o,a=i(e,null,r,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return function(t){return(t.textContent||t.innerText||E(t)).indexOf(e)>-1}}),lang:r(function(e){return pt.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(wt,Tt).toLowerCase(),function(t){var n;do if(n=q?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===_},focus:function(e){return e===H.activeElement&&(!H.hasFocus||H.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.pseudos.empty(e)},header:function(e){return gt.test(e.nodeName)},input:function(e){return mt.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:u(function(){return[0]}),last:u(function(e,t){return[t-1]}),eq:u(function(e,t,n){return[0>n?n+t:n]}),even:u(function(e,t){for(var n=0;t>n;n+=2)e.push(n);return e}),odd:u(function(e,t){for(var n=1;t>n;n+=2)e.push(n);return e}),lt:u(function(e,t,n){for(var r=0>n?n+t:n;--r>=0;)e.push(r);return e}),gt:u(function(e,t,n){for(var r=0>n?n+t:n;++r<t;)e.push(r);return e})}},T.pseudos.nth=T.pseudos.eq;for(x in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})T.pseudos[x]=s(x);for(x in{submit:!0,reset:!0})T.pseudos[x]=l(x);return d.prototype=T.filters=T.pseudos,T.setFilters=new d,C=t.tokenize=function(e,n){var r,i,o,a,s,l,u,c=z[e+" "];if(c)return n?0:c.slice(0);for(s=e,l=[],u=T.preFilter;s;){(!r||(i=ut.exec(s)))&&(i&&(s=s.slice(i[0].length)||s),l.push(o=[])),r=!1,(i=ct.exec(s))&&(r=i.shift(),o.push({value:r,type:i[0].replace(lt," ")}),s=s.slice(r.length));for(a in T.filter)!(i=ht[a].exec(s))||u[a]&&!(i=u[a](i))||(r=i.shift(),o.push({value:r,type:a,matches:i}),s=s.slice(r.length));if(!r)break}return n?s.length:s?t.error(e):z(e,l).slice(0)},N=t.compile=function(e,t){var n,r=[],i=[],o=X[e+" "];if(!o){for(t||(t=C(e)),n=t.length;n--;)o=y(t[n]),o[B]?r.push(o):i.push(o);o=X(e,b(i,r)),o.selector=e}return o},S=t.select=function(e,t,n,r){var i,o,a,s,l,u="function"==typeof e&&e,d=!r&&C(e=u.selector||e);if(n=n||[],1===d.length){if(o=d[0]=d[0].slice(0),o.length>2&&"ID"===(a=o[0]).type&&w.getById&&9===t.nodeType&&q&&T.relative[o[1].type]){if(t=(T.find.ID(a.matches[0].replace(wt,Tt),t)||[])[0],!t)return n;u&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=ht.needsContext.test(e)?0:o.length;i--&&(a=o[i],!T.relative[s=a.type]);)if((l=T.find[s])&&(r=l(a.matches[0].replace(wt,Tt),bt.test(o[0].type)&&c(t.parentNode)||t))){if(o.splice(i,1),e=r.length&&f(o),!e)return Z.apply(n,r),n; +break}}return(u||N(e,d))(r,t,!q,n,bt.test(e)&&c(t.parentNode)||t),n},w.sortStable=B.split("").sort(U).join("")===B,w.detectDuplicates=!!D,L(),w.sortDetached=i(function(e){return 1&e.compareDocumentPosition(H.createElement("div"))}),i(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){return n?void 0:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),w.attributes&&i(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?void 0:e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(nt,function(e,t,n){var r;return n?void 0:e[t]===!0?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(e);it.find=ut,it.expr=ut.selectors,it.expr[":"]=it.expr.pseudos,it.unique=ut.uniqueSort,it.text=ut.getText,it.isXMLDoc=ut.isXML,it.contains=ut.contains;var ct=it.expr.match.needsContext,dt=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,ft=/^.[^:#\[\.,]*$/;it.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?it.find.matchesSelector(r,e)?[r]:[]:it.find.matches(e,it.grep(t,function(e){return 1===e.nodeType}))},it.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(it(e).filter(function(){for(t=0;i>t;t++)if(it.contains(r[t],this))return!0}));for(t=0;i>t;t++)it.find(e,r[t],n);return n=this.pushStack(i>1?it.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(r(this,e||[],!1))},not:function(e){return this.pushStack(r(this,e||[],!0))},is:function(e){return!!r(this,"string"==typeof e&&ct.test(e)?it(e):e||[],!1).length}});var pt,ht=e.document,mt=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,gt=it.fn.init=function(e,t){var n,r;if(!e)return this;if("string"==typeof e){if(n="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:mt.exec(e),!n||!n[1]&&t)return!t||t.jquery?(t||pt).find(e):this.constructor(t).find(e);if(n[1]){if(t=t instanceof it?t[0]:t,it.merge(this,it.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:ht,!0)),dt.test(n[1])&&it.isPlainObject(t))for(n in t)it.isFunction(this[n])?this[n](t[n]):this.attr(n,t[n]);return this}if(r=ht.getElementById(n[2]),r&&r.parentNode){if(r.id!==n[2])return pt.find(e);this.length=1,this[0]=r}return this.context=ht,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):it.isFunction(e)?"undefined"!=typeof pt.ready?pt.ready(e):e(it):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),it.makeArray(e,this))};gt.prototype=it.fn,pt=it(ht);var vt=/^(?:parents|prev(?:Until|All))/,yt={children:!0,contents:!0,next:!0,prev:!0};it.extend({dir:function(e,t,n){for(var r=[],i=e[t];i&&9!==i.nodeType&&(void 0===n||1!==i.nodeType||!it(i).is(n));)1===i.nodeType&&r.push(i),i=i[t];return r},sibling:function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}}),it.fn.extend({has:function(e){var t,n=it(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(it.contains(this,n[t]))return!0})},closest:function(e,t){for(var n,r=0,i=this.length,o=[],a=ct.test(e)||"string"!=typeof e?it(e,t||this.context):0;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&it.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?it.unique(o):o)},index:function(e){return e?"string"==typeof e?it.inArray(this[0],it(e)):it.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(it.unique(it.merge(this.get(),it(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),it.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return it.dir(e,"parentNode")},parentsUntil:function(e,t,n){return it.dir(e,"parentNode",n)},next:function(e){return i(e,"nextSibling")},prev:function(e){return i(e,"previousSibling")},nextAll:function(e){return it.dir(e,"nextSibling")},prevAll:function(e){return it.dir(e,"previousSibling")},nextUntil:function(e,t,n){return it.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return it.dir(e,"previousSibling",n)},siblings:function(e){return it.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return it.sibling(e.firstChild)},contents:function(e){return it.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:it.merge([],e.childNodes)}},function(e,t){it.fn[e]=function(n,r){var i=it.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=it.filter(r,i)),this.length>1&&(yt[e]||(i=it.unique(i)),vt.test(e)&&(i=i.reverse())),this.pushStack(i)}});var bt=/\S+/g,xt={};it.Callbacks=function(e){e="string"==typeof e?xt[e]||o(e):it.extend({},e);var t,n,r,i,a,s,l=[],u=!e.once&&[],c=function(o){for(n=e.memory&&o,r=!0,a=s||0,s=0,i=l.length,t=!0;l&&i>a;a++)if(l[a].apply(o[0],o[1])===!1&&e.stopOnFalse){n=!1;break}t=!1,l&&(u?u.length&&c(u.shift()):n?l=[]:d.disable())},d={add:function(){if(l){var r=l.length;!function o(t){it.each(t,function(t,n){var r=it.type(n);"function"===r?e.unique&&d.has(n)||l.push(n):n&&n.length&&"string"!==r&&o(n)})}(arguments),t?i=l.length:n&&(s=r,c(n))}return this},remove:function(){return l&&it.each(arguments,function(e,n){for(var r;(r=it.inArray(n,l,r))>-1;)l.splice(r,1),t&&(i>=r&&i--,a>=r&&a--)}),this},has:function(e){return e?it.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],i=0,this},disable:function(){return l=u=n=void 0,this},disabled:function(){return!l},lock:function(){return u=void 0,n||d.disable(),this},locked:function(){return!u},fireWith:function(e,n){return!l||r&&!u||(n=n||[],n=[e,n.slice?n.slice():n],t?u.push(n):c(n)),this},fire:function(){return d.fireWith(this,arguments),this},fired:function(){return!!r}};return d},it.extend({Deferred:function(e){var t=[["resolve","done",it.Callbacks("once memory"),"resolved"],["reject","fail",it.Callbacks("once memory"),"rejected"],["notify","progress",it.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return it.Deferred(function(n){it.each(t,function(t,o){var a=it.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&it.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?it.extend(e,r):r}},i={};return r.pipe=r.then,it.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,i=0,o=Y.call(arguments),a=o.length,s=1!==a||e&&it.isFunction(e.promise)?a:0,l=1===s?e:it.Deferred(),u=function(e,n,r){return function(i){n[e]=this,r[e]=arguments.length>1?Y.call(arguments):i,r===t?l.notifyWith(n,r):--s||l.resolveWith(n,r)}};if(a>1)for(t=new Array(a),n=new Array(a),r=new Array(a);a>i;i++)o[i]&&it.isFunction(o[i].promise)?o[i].promise().done(u(i,r,o)).fail(l.reject).progress(u(i,n,t)):--s;return s||l.resolveWith(r,o),l.promise()}});var wt;it.fn.ready=function(e){return it.ready.promise().done(e),this},it.extend({isReady:!1,readyWait:1,holdReady:function(e){e?it.readyWait++:it.ready(!0)},ready:function(e){if(e===!0?!--it.readyWait:!it.isReady){if(!ht.body)return setTimeout(it.ready);it.isReady=!0,e!==!0&&--it.readyWait>0||(wt.resolveWith(ht,[it]),it.fn.triggerHandler&&(it(ht).triggerHandler("ready"),it(ht).off("ready")))}}}),it.ready.promise=function(t){if(!wt)if(wt=it.Deferred(),"complete"===ht.readyState)setTimeout(it.ready);else if(ht.addEventListener)ht.addEventListener("DOMContentLoaded",s,!1),e.addEventListener("load",s,!1);else{ht.attachEvent("onreadystatechange",s),e.attachEvent("onload",s);var n=!1;try{n=null==e.frameElement&&ht.documentElement}catch(r){}n&&n.doScroll&&!function i(){if(!it.isReady){try{n.doScroll("left")}catch(e){return setTimeout(i,50)}a(),it.ready()}}()}return wt.promise(t)};var Tt,Et="undefined";for(Tt in it(nt))break;nt.ownLast="0"!==Tt,nt.inlineBlockNeedsLayout=!1,it(function(){var e,t,n,r;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",nt.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(r))}),function(){var e=ht.createElement("div");if(null==nt.deleteExpando){nt.deleteExpando=!0;try{delete e.test}catch(t){nt.deleteExpando=!1}}e=null}(),it.acceptData=function(e){var t=it.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return 1!==n&&9!==n?!1:!t||t!==!0&&e.getAttribute("classid")===t};var kt=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Ct=/([A-Z])/g;it.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?it.cache[e[it.expando]]:e[it.expando],!!e&&!u(e)},data:function(e,t,n){return c(e,t,n)},removeData:function(e,t){return d(e,t)},_data:function(e,t,n){return c(e,t,n,!0)},_removeData:function(e,t){return d(e,t,!0)}}),it.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=it.data(o),1===o.nodeType&&!it._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&(r=a[n].name,0===r.indexOf("data-")&&(r=it.camelCase(r.slice(5)),l(o,r,i[r])));it._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){it.data(this,e)}):arguments.length>1?this.each(function(){it.data(this,e,t)}):o?l(o,e,it.data(o,e)):void 0},removeData:function(e){return this.each(function(){it.removeData(this,e)})}}),it.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=it._data(e,t),n&&(!r||it.isArray(n)?r=it._data(e,t,it.makeArray(n)):r.push(n)),r||[]):void 0},dequeue:function(e,t){t=t||"fx";var n=it.queue(e,t),r=n.length,i=n.shift(),o=it._queueHooks(e,t),a=function(){it.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return it._data(e,n)||it._data(e,n,{empty:it.Callbacks("once memory").add(function(){it._removeData(e,t+"queue"),it._removeData(e,n)})})}}),it.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?it.queue(this[0],e):void 0===t?this:this.each(function(){var n=it.queue(this,e,t);it._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&it.dequeue(this,e)})},dequeue:function(e){return this.each(function(){it.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=it.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};for("string"!=typeof e&&(t=e,e=void 0),e=e||"fx";a--;)n=it._data(o[a],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var Nt=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,St=["Top","Right","Bottom","Left"],jt=function(e,t){return e=t||e,"none"===it.css(e,"display")||!it.contains(e.ownerDocument,e)},At=it.access=function(e,t,n,r,i,o,a){var s=0,l=e.length,u=null==n;if("object"===it.type(n)){i=!0;for(s in n)it.access(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,it.isFunction(r)||(a=!0),u&&(a?(t.call(e,r),t=null):(u=t,t=function(e,t,n){return u.call(it(e),n)})),t))for(;l>s;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:u?t.call(e):l?t(e[0],n):o},Dt=/^(?:checkbox|radio)$/i;!function(){var e=ht.createElement("input"),t=ht.createElement("div"),n=ht.createDocumentFragment();if(t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",nt.leadingWhitespace=3===t.firstChild.nodeType,nt.tbody=!t.getElementsByTagName("tbody").length,nt.htmlSerialize=!!t.getElementsByTagName("link").length,nt.html5Clone="<:nav></:nav>"!==ht.createElement("nav").cloneNode(!0).outerHTML,e.type="checkbox",e.checked=!0,n.appendChild(e),nt.appendChecked=e.checked,t.innerHTML="<textarea>x</textarea>",nt.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,n.appendChild(t),t.innerHTML="<input type='radio' checked='checked' name='t'/>",nt.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,nt.noCloneEvent=!0,t.attachEvent&&(t.attachEvent("onclick",function(){nt.noCloneEvent=!1}),t.cloneNode(!0).click()),null==nt.deleteExpando){nt.deleteExpando=!0;try{delete t.test}catch(r){nt.deleteExpando=!1}}}(),function(){var t,n,r=ht.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(nt[t+"Bubbles"]=n in e)||(r.setAttribute(n,"t"),nt[t+"Bubbles"]=r.attributes[n].expando===!1);r=null}();var Lt=/^(?:input|select|textarea)$/i,Ht=/^key/,_t=/^(?:mouse|pointer|contextmenu)|click/,qt=/^(?:focusinfocus|focusoutblur)$/,Ft=/^([^.]*)(?:\.(.+)|)$/;it.event={global:{},add:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it._data(e);if(g){for(n.handler&&(l=n,n=l.handler,i=l.selector),n.guid||(n.guid=it.guid++),(a=g.events)||(a=g.events={}),(c=g.handle)||(c=g.handle=function(e){return typeof it===Et||e&&it.event.triggered===e.type?void 0:it.event.dispatch.apply(c.elem,arguments)},c.elem=e),t=(t||"").match(bt)||[""],s=t.length;s--;)o=Ft.exec(t[s])||[],p=m=o[1],h=(o[2]||"").split(".").sort(),p&&(u=it.event.special[p]||{},p=(i?u.delegateType:u.bindType)||p,u=it.event.special[p]||{},d=it.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&it.expr.match.needsContext.test(i),namespace:h.join(".")},l),(f=a[p])||(f=a[p]=[],f.delegateCount=0,u.setup&&u.setup.call(e,r,h,c)!==!1||(e.addEventListener?e.addEventListener(p,c,!1):e.attachEvent&&e.attachEvent("on"+p,c))),u.add&&(u.add.call(e,d),d.handler.guid||(d.handler.guid=n.guid)),i?f.splice(f.delegateCount++,0,d):f.push(d),it.event.global[p]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it.hasData(e)&&it._data(e);if(g&&(c=g.events)){for(t=(t||"").match(bt)||[""],u=t.length;u--;)if(s=Ft.exec(t[u])||[],p=m=s[1],h=(s[2]||"").split(".").sort(),p){for(d=it.event.special[p]||{},p=(r?d.delegateType:d.bindType)||p,f=c[p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;o--;)a=f[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,d.remove&&d.remove.call(e,a));l&&!f.length&&(d.teardown&&d.teardown.call(e,h,g.handle)!==!1||it.removeEvent(e,p,g.handle),delete c[p])}else for(p in c)it.event.remove(e,p+t[u],n,r,!0);it.isEmptyObject(c)&&(delete g.handle,it._removeData(e,"events"))}},trigger:function(t,n,r,i){var o,a,s,l,u,c,d,f=[r||ht],p=tt.call(t,"type")?t.type:t,h=tt.call(t,"namespace")?t.namespace.split("."):[];if(s=c=r=r||ht,3!==r.nodeType&&8!==r.nodeType&&!qt.test(p+it.event.triggered)&&(p.indexOf(".")>=0&&(h=p.split("."),p=h.shift(),h.sort()),a=p.indexOf(":")<0&&"on"+p,t=t[it.expando]?t:new it.Event(p,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=h.join("."),t.namespace_re=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:it.makeArray(n,[t]),u=it.event.special[p]||{},i||!u.trigger||u.trigger.apply(r,n)!==!1)){if(!i&&!u.noBubble&&!it.isWindow(r)){for(l=u.delegateType||p,qt.test(l+p)||(s=s.parentNode);s;s=s.parentNode)f.push(s),c=s;c===(r.ownerDocument||ht)&&f.push(c.defaultView||c.parentWindow||e)}for(d=0;(s=f[d++])&&!t.isPropagationStopped();)t.type=d>1?l:u.bindType||p,o=(it._data(s,"events")||{})[t.type]&&it._data(s,"handle"),o&&o.apply(s,n),o=a&&s[a],o&&o.apply&&it.acceptData(s)&&(t.result=o.apply(s,n),t.result===!1&&t.preventDefault());if(t.type=p,!i&&!t.isDefaultPrevented()&&(!u._default||u._default.apply(f.pop(),n)===!1)&&it.acceptData(r)&&a&&r[p]&&!it.isWindow(r)){c=r[a],c&&(r[a]=null),it.event.triggered=p;try{r[p]()}catch(m){}it.event.triggered=void 0,c&&(r[a]=c)}return t.result}},dispatch:function(e){e=it.event.fix(e);var t,n,r,i,o,a=[],s=Y.call(arguments),l=(it._data(this,"events")||{})[e.type]||[],u=it.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!u.preDispatch||u.preDispatch.call(this,e)!==!1){for(a=it.event.handlers.call(this,e,l),t=0;(i=a[t++])&&!e.isPropagationStopped();)for(e.currentTarget=i.elem,o=0;(r=i.handlers[o++])&&!e.isImmediatePropagationStopped();)(!e.namespace_re||e.namespace_re.test(r.namespace))&&(e.handleObj=r,e.data=r.data,n=((it.event.special[r.origType]||{}).handle||r.handler).apply(i.elem,s),void 0!==n&&(e.result=n)===!1&&(e.preventDefault(),e.stopPropagation()));return u.postDispatch&&u.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,l=e.target;if(s&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(i=[],o=0;s>o;o++)r=t[o],n=r.selector+" ",void 0===i[n]&&(i[n]=r.needsContext?it(n,this).index(l)>=0:it.find(n,this,null,[l]).length),i[n]&&i.push(r);i.length&&a.push({elem:l,handlers:i})}return s<t.length&&a.push({elem:this,handlers:t.slice(s)}),a},fix:function(e){if(e[it.expando])return e;var t,n,r,i=e.type,o=e,a=this.fixHooks[i];for(a||(this.fixHooks[i]=a=_t.test(i)?this.mouseHooks:Ht.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new it.Event(o),t=r.length;t--;)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||ht),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,a.filter?a.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,o=t.button,a=t.fromElement;return null==e.pageX&&null!=t.clientX&&(r=e.target.ownerDocument||ht,i=r.documentElement,n=r.body,e.pageX=t.clientX+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0),e.pageY=t.clientY+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?t.toElement:a),e.which||void 0===o||(e.which=1&o?1:2&o?3:4&o?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==h()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===h()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return it.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(e){return it.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=it.extend(new it.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?it.event.trigger(i,null,t):it.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},it.removeEvent=ht.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===Et&&(e[r]=null),e.detachEvent(r,n))},it.Event=function(e,t){return this instanceof it.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&e.returnValue===!1?f:p):this.type=e,t&&it.extend(this,t),this.timeStamp=e&&e.timeStamp||it.now(),void(this[it.expando]=!0)):new it.Event(e,t)},it.Event.prototype={isDefaultPrevented:p,isPropagationStopped:p,isImmediatePropagationStopped:p,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=f,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=f,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=f,e&&e.stopImmediatePropagation&&e.stopImmediatePropagation(),this.stopPropagation()}},it.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){it.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!it.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),nt.submitBubbles||(it.event.special.submit={setup:function(){return it.nodeName(this,"form")?!1:void it.event.add(this,"click._submit keypress._submit",function(e){var t=e.target,n=it.nodeName(t,"input")||it.nodeName(t,"button")?t.form:void 0;n&&!it._data(n,"submitBubbles")&&(it.event.add(n,"submit._submit",function(e){e._submit_bubble=!0}),it._data(n,"submitBubbles",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&it.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return it.nodeName(this,"form")?!1:void it.event.remove(this,"._submit")}}),nt.changeBubbles||(it.event.special.change={setup:function(){return Lt.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(it.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),it.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),it.event.simulate("change",this,e,!0)})),!1):void it.event.add(this,"beforeactivate._change",function(e){var t=e.target;Lt.test(t.nodeName)&&!it._data(t,"changeBubbles")&&(it.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||it.event.simulate("change",this.parentNode,e,!0)}),it._data(t,"changeBubbles",!0))})},handle:function(e){var t=e.target;return this!==t||e.isSimulated||e.isTrigger||"radio"!==t.type&&"checkbox"!==t.type?e.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return it.event.remove(this,"._change"),!Lt.test(this.nodeName)}}),nt.focusinBubbles||it.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){it.event.simulate(t,e.target,it.event.fix(e),!0)};it.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=it._data(r,t);i||r.addEventListener(e,n,!0),it._data(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=it._data(r,t)-1;i?it._data(r,t,i):(r.removeEventListener(e,n,!0),it._removeData(r,t))}}}),it.fn.extend({on:function(e,t,n,r,i){var o,a;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=void 0);for(o in e)this.on(o,t,n,e[o],i);return this}if(null==n&&null==r?(r=t,n=t=void 0):null==r&&("string"==typeof t?(r=n,n=void 0):(r=n,n=t,t=void 0)),r===!1)r=p;else if(!r)return this;return 1===i&&(a=r,r=function(e){return it().off(e),a.apply(this,arguments)},r.guid=a.guid||(a.guid=it.guid++)),this.each(function(){it.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,it(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=void 0),n===!1&&(n=p),this.each(function(){it.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){it.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?it.event.trigger(e,t,n,!0):void 0}});var Mt="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",Ot=/ jQuery\d+="(?:null|\d+)"/g,Rt=new RegExp("<(?:"+Mt+")[\\s/>]","i"),Bt=/^\s+/,Pt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,It=/<([\w:]+)/,Wt=/<tbody/i,$t=/<|&#?\w+;/,zt=/<(?:script|style|link)/i,Xt=/checked\s*(?:[^=]|=\s*.checked.)/i,Ut=/^$|\/(?:java|ecma)script/i,Vt=/^true\/(.*)/,Gt=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Yt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:nt.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},Jt=m(ht),Kt=Jt.appendChild(ht.createElement("div"));Yt.optgroup=Yt.option,Yt.tbody=Yt.tfoot=Yt.colgroup=Yt.caption=Yt.thead,Yt.th=Yt.td,it.extend({clone:function(e,t,n){var r,i,o,a,s,l=it.contains(e.ownerDocument,e);if(nt.html5Clone||it.isXMLDoc(e)||!Rt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Kt.innerHTML=e.outerHTML,Kt.removeChild(o=Kt.firstChild)),!(nt.noCloneEvent&&nt.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||it.isXMLDoc(e)))for(r=g(o),s=g(e),a=0;null!=(i=s[a]);++a)r[a]&&E(i,r[a]);if(t)if(n)for(s=s||g(e),r=r||g(o),a=0;null!=(i=s[a]);a++)T(i,r[a]);else T(e,o);return r=g(o,"script"),r.length>0&&w(r,!l&&g(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){for(var i,o,a,s,l,u,c,d=e.length,f=m(t),p=[],h=0;d>h;h++)if(o=e[h],o||0===o)if("object"===it.type(o))it.merge(p,o.nodeType?[o]:o);else if($t.test(o)){for(s=s||f.appendChild(t.createElement("div")),l=(It.exec(o)||["",""])[1].toLowerCase(),c=Yt[l]||Yt._default,s.innerHTML=c[1]+o.replace(Pt,"<$1></$2>")+c[2],i=c[0];i--;)s=s.lastChild;if(!nt.leadingWhitespace&&Bt.test(o)&&p.push(t.createTextNode(Bt.exec(o)[0])),!nt.tbody)for(o="table"!==l||Wt.test(o)?"<table>"!==c[1]||Wt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;i--;)it.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u);for(it.merge(p,s.childNodes),s.textContent="";s.firstChild;)s.removeChild(s.firstChild);s=f.lastChild}else p.push(t.createTextNode(o));for(s&&f.removeChild(s),nt.appendChecked||it.grep(g(p,"input"),v),h=0;o=p[h++];)if((!r||-1===it.inArray(o,r))&&(a=it.contains(o.ownerDocument,o),s=g(f.appendChild(o),"script"),a&&w(s),n))for(i=0;o=s[i++];)Ut.test(o.type||"")&&n.push(o);return s=null,f},cleanData:function(e,t){for(var n,r,i,o,a=0,s=it.expando,l=it.cache,u=nt.deleteExpando,c=it.event.special;null!=(n=e[a]);a++)if((t||it.acceptData(n))&&(i=n[s],o=i&&l[i])){if(o.events)for(r in o.events)c[r]?it.event.remove(n,r):it.removeEvent(n,r,o.handle);l[i]&&(delete l[i],u?delete n[s]:typeof n.removeAttribute!==Et?n.removeAttribute(s):n[s]=null,G.push(i))}}}),it.fn.extend({text:function(e){return At(this,function(e){return void 0===e?it.text(this):this.empty().append((this[0]&&this[0].ownerDocument||ht).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){for(var n,r=e?it.filter(e,this):this,i=0;null!=(n=r[i]);i++)t||1!==n.nodeType||it.cleanData(g(n)),n.parentNode&&(t&&it.contains(n.ownerDocument,n)&&w(g(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&it.cleanData(g(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&it.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return it.clone(this,e,t)})},html:function(e){return At(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(Ot,""):void 0;if(!("string"!=typeof e||zt.test(e)||!nt.htmlSerialize&&Rt.test(e)||!nt.leadingWhitespace&&Bt.test(e)||Yt[(It.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(Pt,"<$1></$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(it.cleanData(g(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=arguments[0];return this.domManip(arguments,function(t){e=this.parentNode,it.cleanData(g(this)),e&&e.replaceChild(t,this)}),e&&(e.length||e.nodeType)?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t){e=J.apply([],e);var n,r,i,o,a,s,l=0,u=this.length,c=this,d=u-1,f=e[0],p=it.isFunction(f);if(p||u>1&&"string"==typeof f&&!nt.checkClone&&Xt.test(f))return this.each(function(n){var r=c.eq(n);p&&(e[0]=f.call(this,n,r.html())),r.domManip(e,t)});if(u&&(s=it.buildFragment(e,this[0].ownerDocument,!1,this),n=s.firstChild,1===s.childNodes.length&&(s=n),n)){for(o=it.map(g(s,"script"),b),i=o.length;u>l;l++)r=s,l!==d&&(r=it.clone(r,!0,!0),i&&it.merge(o,g(r,"script"))),t.call(this[l],r,l);if(i)for(a=o[o.length-1].ownerDocument,it.map(o,x),l=0;i>l;l++)r=o[l],Ut.test(r.type||"")&&!it._data(r,"globalEval")&&it.contains(a,r)&&(r.src?it._evalUrl&&it._evalUrl(r.src):it.globalEval((r.text||r.textContent||r.innerHTML||"").replace(Gt,"")));s=n=null}return this}}),it.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){it.fn[e]=function(e){for(var n,r=0,i=[],o=it(e),a=o.length-1;a>=r;r++)n=r===a?this:this.clone(!0),it(o[r])[t](n),K.apply(i,n.get());return this.pushStack(i)}});var Qt,Zt={};!function(){var e;nt.shrinkWrapBlocks=function(){if(null!=e)return e;e=!1;var t,n,r;return n=ht.getElementsByTagName("body")[0],n&&n.style?(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",t.appendChild(ht.createElement("div")).style.width="5px",e=3!==t.offsetWidth),n.removeChild(r),e):void 0}}();var en,tn,nn=/^margin/,rn=new RegExp("^("+Nt+")(?!px)[a-z%]+$","i"),on=/^(top|right|bottom|left)$/;e.getComputedStyle?(en=function(e){return e.ownerDocument.defaultView.getComputedStyle(e,null)},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n.getPropertyValue(t)||n[t]:void 0,n&&(""!==a||it.contains(e.ownerDocument,e)||(a=it.style(e,t)),rn.test(a)&&nn.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0===a?a:a+""}):ht.documentElement.currentStyle&&(en=function(e){return e.currentStyle},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n[t]:void 0,null==a&&s&&s[t]&&(a=s[t]),rn.test(a)&&!on.test(t)&&(r=s.left,i=e.runtimeStyle,o=i&&i.left,o&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"}),function(){function t(){var t,n,r,i;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),t.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",o=a=!1,l=!0,e.getComputedStyle&&(o="1%"!==(e.getComputedStyle(t,null)||{}).top,a="4px"===(e.getComputedStyle(t,null)||{width:"4px"}).width,i=t.appendChild(ht.createElement("div")),i.style.cssText=t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",t.style.width="1px",l=!parseFloat((e.getComputedStyle(i,null)||{}).marginRight)),t.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=t.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",s=0===i[0].offsetHeight,s&&(i[0].style.display="",i[1].style.display="none",s=0===i[0].offsetHeight),n.removeChild(r))}var n,r,i,o,a,s,l;n=ht.createElement("div"),n.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",i=n.getElementsByTagName("a")[0],r=i&&i.style,r&&(r.cssText="float:left;opacity:.5",nt.opacity="0.5"===r.opacity,nt.cssFloat=!!r.cssFloat,n.style.backgroundClip="content-box",n.cloneNode(!0).style.backgroundClip="",nt.clearCloneStyle="content-box"===n.style.backgroundClip,nt.boxSizing=""===r.boxSizing||""===r.MozBoxSizing||""===r.WebkitBoxSizing,it.extend(nt,{reliableHiddenOffsets:function(){return null==s&&t(),s +},boxSizingReliable:function(){return null==a&&t(),a},pixelPosition:function(){return null==o&&t(),o},reliableMarginRight:function(){return null==l&&t(),l}}))}(),it.swap=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};var an=/alpha\([^)]*\)/i,sn=/opacity\s*=\s*([^)]*)/,ln=/^(none|table(?!-c[ea]).+)/,un=new RegExp("^("+Nt+")(.*)$","i"),cn=new RegExp("^([+-])=("+Nt+")","i"),dn={position:"absolute",visibility:"hidden",display:"block"},fn={letterSpacing:"0",fontWeight:"400"},pn=["Webkit","O","Moz","ms"];it.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=tn(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":nt.cssFloat?"cssFloat":"styleFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=it.camelCase(t),l=e.style;if(t=it.cssProps[s]||(it.cssProps[s]=S(l,s)),a=it.cssHooks[t]||it.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];if(o=typeof n,"string"===o&&(i=cn.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(it.css(e,t)),o="number"),null!=n&&n===n&&("number"!==o||it.cssNumber[s]||(n+="px"),nt.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),!(a&&"set"in a&&void 0===(n=a.set(e,n,r)))))try{l[t]=n}catch(u){}}},css:function(e,t,n,r){var i,o,a,s=it.camelCase(t);return t=it.cssProps[s]||(it.cssProps[s]=S(e.style,s)),a=it.cssHooks[t]||it.cssHooks[s],a&&"get"in a&&(o=a.get(e,!0,n)),void 0===o&&(o=tn(e,t,r)),"normal"===o&&t in fn&&(o=fn[t]),""===n||n?(i=parseFloat(o),n===!0||it.isNumeric(i)?i||0:o):o}}),it.each(["height","width"],function(e,t){it.cssHooks[t]={get:function(e,n,r){return n?ln.test(it.css(e,"display"))&&0===e.offsetWidth?it.swap(e,dn,function(){return L(e,t,r)}):L(e,t,r):void 0},set:function(e,n,r){var i=r&&en(e);return A(e,n,r?D(e,t,r,nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,i),i):0)}}}),nt.opacity||(it.cssHooks.opacity={get:function(e,t){return sn.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=it.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===it.trim(o.replace(an,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=an.test(o)?o.replace(an,i):o+" "+i)}}),it.cssHooks.marginRight=N(nt.reliableMarginRight,function(e,t){return t?it.swap(e,{display:"inline-block"},tn,[e,"marginRight"]):void 0}),it.each({margin:"",padding:"",border:"Width"},function(e,t){it.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];4>r;r++)i[e+St[r]+t]=o[r]||o[r-2]||o[0];return i}},nn.test(e)||(it.cssHooks[e+t].set=A)}),it.fn.extend({css:function(e,t){return At(this,function(e,t,n){var r,i,o={},a=0;if(it.isArray(t)){for(r=en(e),i=t.length;i>a;a++)o[t[a]]=it.css(e,t[a],!1,r);return o}return void 0!==n?it.style(e,t,n):it.css(e,t)},e,t,arguments.length>1)},show:function(){return j(this,!0)},hide:function(){return j(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){jt(this)?it(this).show():it(this).hide()})}}),it.Tween=H,H.prototype={constructor:H,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(it.cssNumber[n]?"":"px")},cur:function(){var e=H.propHooks[this.prop];return e&&e.get?e.get(this):H.propHooks._default.get(this)},run:function(e){var t,n=H.propHooks[this.prop];return this.pos=t=this.options.duration?it.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):H.propHooks._default.set(this),this}},H.prototype.init.prototype=H.prototype,H.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=it.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){it.fx.step[e.prop]?it.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[it.cssProps[e.prop]]||it.cssHooks[e.prop])?it.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},H.propHooks.scrollTop=H.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},it.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},it.fx=H.prototype.init,it.fx.step={};var hn,mn,gn=/^(?:toggle|show|hide)$/,vn=new RegExp("^(?:([+-])=|)("+Nt+")([a-z%]*)$","i"),yn=/queueHooks$/,bn=[M],xn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=vn.exec(t),o=i&&i[3]||(it.cssNumber[e]?"":"px"),a=(it.cssNumber[e]||"px"!==o&&+r)&&vn.exec(it.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,it.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};it.Animation=it.extend(R,{tweener:function(e,t){it.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");for(var n,r=0,i=e.length;i>r;r++)n=e[r],xn[n]=xn[n]||[],xn[n].unshift(t)},prefilter:function(e,t){t?bn.unshift(e):bn.push(e)}}),it.speed=function(e,t,n){var r=e&&"object"==typeof e?it.extend({},e):{complete:n||!n&&t||it.isFunction(e)&&e,duration:e,easing:n&&t||t&&!it.isFunction(t)&&t};return r.duration=it.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in it.fx.speeds?it.fx.speeds[r.duration]:it.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){it.isFunction(r.old)&&r.old.call(this),r.queue&&it.dequeue(this,r.queue)},r},it.fn.extend({fadeTo:function(e,t,n,r){return this.filter(jt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=it.isEmptyObject(e),o=it.speed(t,n,r),a=function(){var t=R(this,it.extend({},e),o);(i||it._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=it.timers,a=it._data(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&yn.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));(t||!n)&&it.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=it._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=it.timers,a=r?r.length:0;for(n.finish=!0,it.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),it.each(["toggle","show","hide"],function(e,t){var n=it.fn[t];it.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(q(t,!0),e,r,i)}}),it.each({slideDown:q("show"),slideUp:q("hide"),slideToggle:q("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){it.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),it.timers=[],it.fx.tick=function(){var e,t=it.timers,n=0;for(hn=it.now();n<t.length;n++)e=t[n],e()||t[n]!==e||t.splice(n--,1);t.length||it.fx.stop(),hn=void 0},it.fx.timer=function(e){it.timers.push(e),e()?it.fx.start():it.timers.pop()},it.fx.interval=13,it.fx.start=function(){mn||(mn=setInterval(it.fx.tick,it.fx.interval))},it.fx.stop=function(){clearInterval(mn),mn=null},it.fx.speeds={slow:600,fast:200,_default:400},it.fn.delay=function(e,t){return e=it.fx?it.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},function(){var e,t,n,r,i;t=ht.createElement("div"),t.setAttribute("className","t"),t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",r=t.getElementsByTagName("a")[0],n=ht.createElement("select"),i=n.appendChild(ht.createElement("option")),e=t.getElementsByTagName("input")[0],r.style.cssText="top:1px",nt.getSetAttribute="t"!==t.className,nt.style=/top/.test(r.getAttribute("style")),nt.hrefNormalized="/a"===r.getAttribute("href"),nt.checkOn=!!e.value,nt.optSelected=i.selected,nt.enctype=!!ht.createElement("form").enctype,n.disabled=!0,nt.optDisabled=!i.disabled,e=ht.createElement("input"),e.setAttribute("value",""),nt.input=""===e.getAttribute("value"),e.value="t",e.setAttribute("type","radio"),nt.radioValue="t"===e.value}();var wn=/\r/g;it.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=it.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,it(this).val()):e,null==i?i="":"number"==typeof i?i+="":it.isArray(i)&&(i=it.map(i,function(e){return null==e?"":e+""})),t=it.valHooks[this.type]||it.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return t=it.valHooks[i.type]||it.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(wn,""):null==n?"":n)}}}),it.extend({valHooks:{option:{get:function(e){var t=it.find.attr(e,"value");return null!=t?t:it.trim(it.text(e))}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(nt.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&it.nodeName(n.parentNode,"optgroup"))){if(t=it(n).val(),o)return t;a.push(t)}return a},set:function(e,t){for(var n,r,i=e.options,o=it.makeArray(t),a=i.length;a--;)if(r=i[a],it.inArray(it.valHooks.option.get(r),o)>=0)try{r.selected=n=!0}catch(s){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),it.each(["radio","checkbox"],function(){it.valHooks[this]={set:function(e,t){return it.isArray(t)?e.checked=it.inArray(it(e).val(),t)>=0:void 0}},nt.checkOn||(it.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Tn,En,kn=it.expr.attrHandle,Cn=/^(?:checked|selected)$/i,Nn=nt.getSetAttribute,Sn=nt.input;it.fn.extend({attr:function(e,t){return At(this,it.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){it.removeAttr(this,e)})}}),it.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(e&&3!==o&&8!==o&&2!==o)return typeof e.getAttribute===Et?it.prop(e,t,n):(1===o&&it.isXMLDoc(e)||(t=t.toLowerCase(),r=it.attrHooks[t]||(it.expr.match.bool.test(t)?En:Tn)),void 0===n?r&&"get"in r&&null!==(i=r.get(e,t))?i:(i=it.find.attr(e,t),null==i?void 0:i):null!==n?r&&"set"in r&&void 0!==(i=r.set(e,n,t))?i:(e.setAttribute(t,n+""),n):void it.removeAttr(e,t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(bt);if(o&&1===e.nodeType)for(;n=o[i++];)r=it.propFix[n]||n,it.expr.match.bool.test(n)?Sn&&Nn||!Cn.test(n)?e[r]=!1:e[it.camelCase("default-"+n)]=e[r]=!1:it.attr(e,n,""),e.removeAttribute(Nn?n:r)},attrHooks:{type:{set:function(e,t){if(!nt.radioValue&&"radio"===t&&it.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}}}),En={set:function(e,t,n){return t===!1?it.removeAttr(e,n):Sn&&Nn||!Cn.test(n)?e.setAttribute(!Nn&&it.propFix[n]||n,n):e[it.camelCase("default-"+n)]=e[n]=!0,n}},it.each(it.expr.match.bool.source.match(/\w+/g),function(e,t){var n=kn[t]||it.find.attr;kn[t]=Sn&&Nn||!Cn.test(t)?function(e,t,r){var i,o;return r||(o=kn[t],kn[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,kn[t]=o),i}:function(e,t,n){return n?void 0:e[it.camelCase("default-"+t)]?t.toLowerCase():null}}),Sn&&Nn||(it.attrHooks.value={set:function(e,t,n){return it.nodeName(e,"input")?void(e.defaultValue=t):Tn&&Tn.set(e,t,n)}}),Nn||(Tn={set:function(e,t,n){var r=e.getAttributeNode(n);return r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n)?t:void 0}},kn.id=kn.name=kn.coords=function(e,t,n){var r;return n?void 0:(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},it.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:void 0},set:Tn.set},it.attrHooks.contenteditable={set:function(e,t,n){Tn.set(e,""===t?!1:t,n)}},it.each(["width","height"],function(e,t){it.attrHooks[t]={set:function(e,n){return""===n?(e.setAttribute(t,"auto"),n):void 0}}})),nt.style||(it.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var jn=/^(?:input|select|textarea|button|object)$/i,An=/^(?:a|area)$/i;it.fn.extend({prop:function(e,t){return At(this,it.prop,e,t,arguments.length>1)},removeProp:function(e){return e=it.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(t){}})}}),it.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,a=e.nodeType;if(e&&3!==a&&8!==a&&2!==a)return o=1!==a||!it.isXMLDoc(e),o&&(t=it.propFix[t]||t,i=it.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=it.find.attr(e,"tabindex");return t?parseInt(t,10):jn.test(e.nodeName)||An.test(e.nodeName)&&e.href?0:-1}}}}),nt.hrefNormalized||it.each(["href","src"],function(e,t){it.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),nt.optSelected||(it.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),it.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){it.propFix[this.toLowerCase()]=this}),nt.enctype||(it.propFix.enctype="encoding");var Dn=/[\t\r\n\f]/g;it.fn.extend({addClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u="string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):" ")){for(o=0;i=t[o++];)r.indexOf(" "+i+" ")<0&&(r+=i+" ");a=it.trim(r),n.className!==a&&(n.className=a)}return this},removeClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u=0===arguments.length||"string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):"")){for(o=0;i=t[o++];)for(;r.indexOf(" "+i+" ")>=0;)r=r.replace(" "+i+" "," ");a=e?it.trim(r):"",n.className!==a&&(n.className=a)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):this.each(it.isFunction(e)?function(n){it(this).toggleClass(e.call(this,n,this.className,t),t)}:function(){if("string"===n)for(var t,r=0,i=it(this),o=e.match(bt)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else(n===Et||"boolean"===n)&&(this.className&&it._data(this,"__className__",this.className),this.className=this.className||e===!1?"":it._data(this,"__className__")||"")})},hasClass:function(e){for(var t=" "+e+" ",n=0,r=this.length;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(Dn," ").indexOf(t)>=0)return!0;return!1}}),it.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){it.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),it.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var Ln=it.now(),Hn=/\?/,_n=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;it.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=it.trim(t+"");return i&&!it.trim(i.replace(_n,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():it.error("Invalid JSON: "+t)},it.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{e.DOMParser?(r=new DOMParser,n=r.parseFromString(t,"text/xml")):(n=new ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(t))}catch(i){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||it.error("Invalid XML: "+t),n};var qn,Fn,Mn=/#.*$/,On=/([?&])_=[^&]*/,Rn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Bn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Pn=/^(?:GET|HEAD)$/,In=/^\/\//,Wn=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,$n={},zn={},Xn="*/".concat("*");try{Fn=location.href}catch(Un){Fn=ht.createElement("a"),Fn.href="",Fn=Fn.href}qn=Wn.exec(Fn.toLowerCase())||[],it.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Fn,type:"GET",isLocal:Bn.test(qn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Xn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":it.parseJSON,"text xml":it.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?I(I(e,it.ajaxSettings),t):I(it.ajaxSettings,e)},ajaxPrefilter:B($n),ajaxTransport:B(zn),ajax:function(e,t){function n(e,t,n,r){var i,c,v,y,x,T=t;2!==b&&(b=2,s&&clearTimeout(s),u=void 0,a=r||"",w.readyState=e>0?4:0,i=e>=200&&300>e||304===e,n&&(y=W(d,w,n)),y=$(d,y,w,i),i?(d.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(it.lastModified[o]=x),x=w.getResponseHeader("etag"),x&&(it.etag[o]=x)),204===e||"HEAD"===d.type?T="nocontent":304===e?T="notmodified":(T=y.state,c=y.data,v=y.error,i=!v)):(v=T,(e||!T)&&(T="error",0>e&&(e=0))),w.status=e,w.statusText=(t||T)+"",i?h.resolveWith(f,[c,T,w]):h.rejectWith(f,[w,T,v]),w.statusCode(g),g=void 0,l&&p.trigger(i?"ajaxSuccess":"ajaxError",[w,d,i?c:v]),m.fireWith(f,[w,T]),l&&(p.trigger("ajaxComplete",[w,d]),--it.active||it.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,i,o,a,s,l,u,c,d=it.ajaxSetup({},t),f=d.context||d,p=d.context&&(f.nodeType||f.jquery)?it(f):it.event,h=it.Deferred(),m=it.Callbacks("once memory"),g=d.statusCode||{},v={},y={},b=0,x="canceled",w={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=Rn.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=y[n]=y[n]||e,v[e]=t),this},overrideMimeType:function(e){return b||(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)g[t]=[g[t],e[t]];else w.always(e[w.status]);return this},abort:function(e){var t=e||x;return u&&u.abort(t),n(0,t),this}};if(h.promise(w).complete=m.add,w.success=w.done,w.error=w.fail,d.url=((e||d.url||Fn)+"").replace(Mn,"").replace(In,qn[1]+"//"),d.type=t.method||t.type||d.method||d.type,d.dataTypes=it.trim(d.dataType||"*").toLowerCase().match(bt)||[""],null==d.crossDomain&&(r=Wn.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]===qn[1]&&r[2]===qn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(qn[3]||("http:"===qn[1]?"80":"443")))),d.data&&d.processData&&"string"!=typeof d.data&&(d.data=it.param(d.data,d.traditional)),P($n,d,t,w),2===b)return w;l=d.global,l&&0===it.active++&&it.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!Pn.test(d.type),o=d.url,d.hasContent||(d.data&&(o=d.url+=(Hn.test(o)?"&":"?")+d.data,delete d.data),d.cache===!1&&(d.url=On.test(o)?o.replace(On,"$1_="+Ln++):o+(Hn.test(o)?"&":"?")+"_="+Ln++)),d.ifModified&&(it.lastModified[o]&&w.setRequestHeader("If-Modified-Since",it.lastModified[o]),it.etag[o]&&w.setRequestHeader("If-None-Match",it.etag[o])),(d.data&&d.hasContent&&d.contentType!==!1||t.contentType)&&w.setRequestHeader("Content-Type",d.contentType),w.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+Xn+"; q=0.01":""):d.accepts["*"]);for(i in d.headers)w.setRequestHeader(i,d.headers[i]);if(d.beforeSend&&(d.beforeSend.call(f,w,d)===!1||2===b))return w.abort();x="abort";for(i in{success:1,error:1,complete:1})w[i](d[i]);if(u=P(zn,d,t,w)){w.readyState=1,l&&p.trigger("ajaxSend",[w,d]),d.async&&d.timeout>0&&(s=setTimeout(function(){w.abort("timeout")},d.timeout));try{b=1,u.send(v,n)}catch(T){if(!(2>b))throw T;n(-1,T)}}else n(-1,"No Transport");return w},getJSON:function(e,t,n){return it.get(e,t,n,"json")},getScript:function(e,t){return it.get(e,void 0,t,"script")}}),it.each(["get","post"],function(e,t){it[t]=function(e,n,r,i){return it.isFunction(n)&&(i=i||r,r=n,n=void 0),it.ajax({url:e,type:t,dataType:i,data:n,success:r})}}),it.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){it.fn[t]=function(e){return this.on(t,e)}}),it._evalUrl=function(e){return it.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},it.fn.extend({wrapAll:function(e){if(it.isFunction(e))return this.each(function(t){it(this).wrapAll(e.call(this,t))});if(this[0]){var t=it(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return this.each(it.isFunction(e)?function(t){it(this).wrapInner(e.call(this,t))}:function(){var t=it(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=it.isFunction(e);return this.each(function(n){it(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){it.nodeName(this,"body")||it(this).replaceWith(this.childNodes)}).end()}}),it.expr.filters.hidden=function(e){return e.offsetWidth<=0&&e.offsetHeight<=0||!nt.reliableHiddenOffsets()&&"none"===(e.style&&e.style.display||it.css(e,"display"))},it.expr.filters.visible=function(e){return!it.expr.filters.hidden(e)};var Vn=/%20/g,Gn=/\[\]$/,Yn=/\r?\n/g,Jn=/^(?:submit|button|image|reset|file)$/i,Kn=/^(?:input|select|textarea|keygen)/i;it.param=function(e,t){var n,r=[],i=function(e,t){t=it.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=it.ajaxSettings&&it.ajaxSettings.traditional),it.isArray(e)||e.jquery&&!it.isPlainObject(e))it.each(e,function(){i(this.name,this.value)});else for(n in e)z(n,e[n],t,i);return r.join("&").replace(Vn,"+")},it.fn.extend({serialize:function(){return it.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=it.prop(this,"elements");return e?it.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!it(this).is(":disabled")&&Kn.test(this.nodeName)&&!Jn.test(e)&&(this.checked||!Dt.test(e))}).map(function(e,t){var n=it(this).val();return null==n?null:it.isArray(n)?it.map(n,function(e){return{name:t.name,value:e.replace(Yn,"\r\n")}}):{name:t.name,value:n.replace(Yn,"\r\n")}}).get()}}),it.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&X()||U()}:X;var Qn=0,Zn={},er=it.ajaxSettings.xhr();e.ActiveXObject&&it(e).on("unload",function(){for(var e in Zn)Zn[e](void 0,!0)}),nt.cors=!!er&&"withCredentials"in er,er=nt.ajax=!!er,er&&it.ajaxTransport(function(e){if(!e.crossDomain||nt.cors){var t;return{send:function(n,r){var i,o=e.xhr(),a=++Qn;if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)o[i]=e.xhrFields[i];e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest");for(i in n)void 0!==n[i]&&o.setRequestHeader(i,n[i]+"");o.send(e.hasContent&&e.data||null),t=function(n,i){var s,l,u;if(t&&(i||4===o.readyState))if(delete Zn[a],t=void 0,o.onreadystatechange=it.noop,i)4!==o.readyState&&o.abort();else{u={},s=o.status,"string"==typeof o.responseText&&(u.text=o.responseText);try{l=o.statusText}catch(c){l=""}s||!e.isLocal||e.crossDomain?1223===s&&(s=204):s=u.text?200:404}u&&r(s,l,u,o.getAllResponseHeaders())},e.async?4===o.readyState?setTimeout(t):o.onreadystatechange=Zn[a]=t:t()},abort:function(){t&&t(void 0,!0)}}}}),it.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return it.globalEval(e),e}}}),it.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),it.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=ht.head||it("head")[0]||ht.documentElement;return{send:function(r,i){t=ht.createElement("script"),t.async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||i(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var tr=[],nr=/(=)\?(?=&|$)|\?\?/;it.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=tr.pop()||it.expando+"_"+Ln++;return this[e]=!0,e}}),it.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=t.jsonp!==!1&&(nr.test(t.url)?"url":"string"==typeof t.data&&!(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&nr.test(t.data)&&"data");return s||"jsonp"===t.dataTypes[0]?(i=t.jsonpCallback=it.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(nr,"$1"+i):t.jsonp!==!1&&(t.url+=(Hn.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||it.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,tr.push(i)),a&&it.isFunction(o)&&o(a[0]),a=o=void 0}),"script"):void 0}),it.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||ht;var r=dt.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=it.buildFragment([e],t,i),i&&i.length&&it(i).remove(),it.merge([],r.childNodes))};var rr=it.fn.load;it.fn.load=function(e,t,n){if("string"!=typeof e&&rr)return rr.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>=0&&(r=it.trim(e.slice(s,e.length)),e=e.slice(0,s)),it.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(o="POST"),a.length>0&&it.ajax({url:e,type:o,dataType:"html",data:t}).done(function(e){i=arguments,a.html(r?it("<div>").append(it.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){a.each(n,i||[e.responseText,t,e])}),this},it.expr.filters.animated=function(e){return it.grep(it.timers,function(t){return e===t.elem}).length};var ir=e.document.documentElement;it.offset={setOffset:function(e,t,n){var r,i,o,a,s,l,u,c=it.css(e,"position"),d=it(e),f={};"static"===c&&(e.style.position="relative"),s=d.offset(),o=it.css(e,"top"),l=it.css(e,"left"),u=("absolute"===c||"fixed"===c)&&it.inArray("auto",[o,l])>-1,u?(r=d.position(),a=r.top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(l)||0),it.isFunction(t)&&(t=t.call(e,n,s)),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):d.css(f)}},it.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){it.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;if(o)return t=o.documentElement,it.contains(t,i)?(typeof i.getBoundingClientRect!==Et&&(r=i.getBoundingClientRect()),n=V(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===it.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),it.nodeName(e[0],"html")||(n=e.offset()),n.top+=it.css(e[0],"borderTopWidth",!0),n.left+=it.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-it.css(r,"marginTop",!0),left:t.left-n.left-it.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent||ir;e&&!it.nodeName(e,"html")&&"static"===it.css(e,"position");)e=e.offsetParent;return e||ir})}}),it.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);it.fn[e]=function(r){return At(this,function(e,r,i){var o=V(e);return void 0===i?o?t in o?o[t]:o.document.documentElement[r]:e[r]:void(o?o.scrollTo(n?it(o).scrollLeft():i,n?i:it(o).scrollTop()):e[r]=i)},e,r,arguments.length,null)}}),it.each(["top","left"],function(e,t){it.cssHooks[t]=N(nt.pixelPosition,function(e,n){return n?(n=tn(e,t),rn.test(n)?it(e).position()[t]+"px":n):void 0})}),it.each({Height:"height",Width:"width"},function(e,t){it.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){it.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(r===!0||i===!0?"margin":"border");return At(this,function(t,n,r){var i;return it.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?it.css(t,n,a):it.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),it.fn.size=function(){return this.length},it.fn.andSelf=it.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return it});var or=e.jQuery,ar=e.$;return it.noConflict=function(t){return e.$===it&&(e.$=ar),t&&e.jQuery===it&&(e.jQuery=or),it},typeof t===Et&&(e.jQuery=e.$=it),it}),function(e,t){e.rails!==t&&e.error("jquery-ujs has already been loaded!");var n,r=e(document);e.rails=n={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]",buttonClickSelector:"button[data-remote], button[data-confirm]",inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form",formInputClickSelector:"form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])",disableSelector:"input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled",enableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled",requiredInputSelector:"input[name][required]:not([disabled]),textarea[name][required]:not([disabled])",fileInputSelector:"input[type=file]",linkDisableSelector:"a[data-disable-with], a[data-disable]",buttonDisableSelector:"button[data-remote][data-disable-with], button[data-remote][data-disable]",CSRFProtection:function(t){var n=e('meta[name="csrf-token"]').attr("content");n&&t.setRequestHeader("X-CSRF-Token",n)},refreshCSRFTokens:function(){var t=e("meta[name=csrf-token]").attr("content"),n=e("meta[name=csrf-param]").attr("content");e('form input[name="'+n+'"]').val(t)},fire:function(t,n,r){var i=e.Event(n);return t.trigger(i,r),i.result!==!1},confirm:function(e){return confirm(e)},ajax:function(t){return e.ajax(t)},href:function(e){return e.attr("href")},handleRemote:function(r){var i,o,a,s,l,u,c,d;if(n.fire(r,"ajax:before")){if(s=r.data("cross-domain"),l=s===t?null:s,u=r.data("with-credentials")||null,c=r.data("type")||e.ajaxSettings&&e.ajaxSettings.dataType,r.is("form")){i=r.attr("method"),o=r.attr("action"),a=r.serializeArray();var f=r.data("ujs:submit-button");f&&(a.push(f),r.data("ujs:submit-button",null))}else r.is(n.inputChangeSelector)?(i=r.data("method"),o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):r.is(n.buttonClickSelector)?(i=r.data("method")||"get",o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):(i=r.data("method"),o=n.href(r),a=r.data("params")||null);return d={type:i||"GET",data:a,dataType:c,beforeSend:function(e,i){return i.dataType===t&&e.setRequestHeader("accept","*/*;q=0.5, "+i.accepts.script),n.fire(r,"ajax:beforeSend",[e,i])?void r.trigger("ajax:send",e):!1 +},success:function(e,t,n){r.trigger("ajax:success",[e,t,n])},complete:function(e,t){r.trigger("ajax:complete",[e,t])},error:function(e,t,n){r.trigger("ajax:error",[e,t,n])},crossDomain:l},u&&(d.xhrFields={withCredentials:u}),o&&(d.url=o),n.ajax(d)}return!1},handleMethod:function(r){var i=n.href(r),o=r.data("method"),a=r.attr("target"),s=e("meta[name=csrf-token]").attr("content"),l=e("meta[name=csrf-param]").attr("content"),u=e('<form method="post" action="'+i+'"></form>'),c='<input name="_method" value="'+o+'" type="hidden" />';l!==t&&s!==t&&(c+='<input name="'+l+'" value="'+s+'" type="hidden" />'),a&&u.attr("target",a),u.hide().append(c).appendTo("body"),u.submit()},formElements:function(t,n){return t.is("form")?e(t[0].elements).filter(n):t.find(n)},disableFormElements:function(t){n.formElements(t,n.disableSelector).each(function(){n.disableFormElement(e(this))})},disableFormElement:function(e){var n,r;n=e.is("button")?"html":"val",r=e.data("disable-with"),e.data("ujs:enable-with",e[n]()),r!==t&&e[n](r),e.prop("disabled",!0)},enableFormElements:function(t){n.formElements(t,n.enableSelector).each(function(){n.enableFormElement(e(this))})},enableFormElement:function(e){var t=e.is("button")?"html":"val";e.data("ujs:enable-with")&&e[t](e.data("ujs:enable-with")),e.prop("disabled",!1)},allowAction:function(e){var t,r=e.data("confirm"),i=!1;return r?(n.fire(e,"confirm")&&(i=n.confirm(r),t=n.fire(e,"confirm:complete",[i])),i&&t):!0},blankInputs:function(t,n,r){var i,o,a=e(),s=n||"input,textarea",l=t.find(s);return l.each(function(){if(i=e(this),o=i.is("input[type=checkbox],input[type=radio]")?i.is(":checked"):i.val(),!o==!r){if(i.is("input[type=radio]")&&l.filter('input[type=radio]:checked[name="'+i.attr("name")+'"]').length)return!0;a=a.add(i)}}),a.length?a:!1},nonBlankInputs:function(e,t){return n.blankInputs(e,t,!0)},stopEverything:function(t){return e(t.target).trigger("ujs:everythingStopped"),t.stopImmediatePropagation(),!1},disableElement:function(e){var r=e.data("disable-with");e.data("ujs:enable-with",e.html()),r!==t&&e.html(r),e.bind("click.railsDisable",function(e){return n.stopEverything(e)})},enableElement:function(e){e.data("ujs:enable-with")!==t&&(e.html(e.data("ujs:enable-with")),e.removeData("ujs:enable-with")),e.unbind("click.railsDisable")}},n.fire(r,"rails:attachBindings")&&(e.ajaxPrefilter(function(e,t,r){e.crossDomain||n.CSRFProtection(r)}),r.delegate(n.linkDisableSelector,"ajax:complete",function(){n.enableElement(e(this))}),r.delegate(n.buttonDisableSelector,"ajax:complete",function(){n.enableFormElement(e(this))}),r.delegate(n.linkClickSelector,"click.rails",function(r){var i=e(this),o=i.data("method"),a=i.data("params"),s=r.metaKey||r.ctrlKey;if(!n.allowAction(i))return n.stopEverything(r);if(!s&&i.is(n.linkDisableSelector)&&n.disableElement(i),i.data("remote")!==t){if(s&&(!o||"GET"===o)&&!a)return!0;var l=n.handleRemote(i);return l===!1?n.enableElement(i):l.error(function(){n.enableElement(i)}),!1}return i.data("method")?(n.handleMethod(i),!1):void 0}),r.delegate(n.buttonClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);r.is(n.buttonDisableSelector)&&n.disableFormElement(r);var i=n.handleRemote(r);return i===!1?n.enableFormElement(r):i.error(function(){n.enableFormElement(r)}),!1}),r.delegate(n.inputChangeSelector,"change.rails",function(t){var r=e(this);return n.allowAction(r)?(n.handleRemote(r),!1):n.stopEverything(t)}),r.delegate(n.formSubmitSelector,"submit.rails",function(r){var i,o,a=e(this),s=a.data("remote")!==t;if(!n.allowAction(a))return n.stopEverything(r);if(a.attr("novalidate")==t&&(i=n.blankInputs(a,n.requiredInputSelector),i&&n.fire(a,"ajax:aborted:required",[i])))return n.stopEverything(r);if(s){if(o=n.nonBlankInputs(a,n.fileInputSelector)){setTimeout(function(){n.disableFormElements(a)},13);var l=n.fire(a,"ajax:aborted:file",[o]);return l||setTimeout(function(){n.enableFormElements(a)},13),l}return n.handleRemote(a),!1}setTimeout(function(){n.disableFormElements(a)},13)}),r.delegate(n.formInputClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);var i=r.attr("name"),o=i?{name:i,value:r.val()}:null;r.closest("form").data("ujs:submit-button",o)}),r.delegate(n.formSubmitSelector,"ajax:send.rails",function(t){this==t.target&&n.disableFormElements(e(this))}),r.delegate(n.formSubmitSelector,"ajax:complete.rails",function(t){this==t.target&&n.enableFormElements(e(this))}),e(function(){n.refreshCSRFTokens()}))}(jQuery),function(){var e,t,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v,y,b,x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B,P,I,W,$,z,X,U,V,G,Y=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1},J={}.hasOwnProperty,K=function(e,t){function n(){this.constructor=e}for(var r in t)J.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},Q=[].slice;j={},d=10,$=!1,m=null,S=null,q=null,h=null,V=null,b=function(e){var t;return e=new n(e),B(),c(),F(e),$&&(t=z(e.absolute))?(x(t),w(e)):w(e,W)},z=function(e){var t;return t=j[e],t&&!t.transitionCacheDisabled?t:void 0},g=function(e){return null==e&&(e=!0),$=e},w=function(e,t){return null==t&&(t=function(){return function(){}}(this)),X("page:fetch",{url:e.absolute}),null!=V&&V.abort(),V=new XMLHttpRequest,V.open("GET",e.withoutHashForIE10compatibility(),!0),V.setRequestHeader("Accept","text/html, application/xhtml+xml, application/xml"),V.setRequestHeader("X-XHR-Referer",q),V.onload=function(){var n;return X("page:receive"),(n=H())?(f.apply(null,y(n)),M(),t(),X("page:load")):document.location.href=e.absolute},V.onloadend=function(){return V=null},V.onerror=function(){return document.location.href=e.absolute},V.send()},x=function(e){return null!=V&&V.abort(),f(e.title,e.body),_(e),X("page:restore")},c=function(){var e;return e=new n(m.url),j[e.absolute]={url:e.relative,body:document.body,title:document.title,positionY:window.pageYOffset,positionX:window.pageXOffset,cachedAt:(new Date).getTime(),transitionCacheDisabled:null!=document.querySelector("[data-no-transition-cache]")},p(d)},D=function(e){return null==e&&(e=d),/^[\d]+$/.test(e)?d=parseInt(e):void 0},p=function(e){var t,n,r,i,o,a;for(r=Object.keys(j),t=r.map(function(e){return j[e].cachedAt}).sort(function(e,t){return t-e}),a=[],i=0,o=r.length;o>i;i++)n=r[i],j[n].cachedAt<=t[e]&&(X("page:expire",j[n]),a.push(delete j[n]));return a},f=function(t,n,r,i){return document.title=t,document.documentElement.replaceChild(n,document.body),null!=r&&e.update(r),i&&v(),m=window.history.state,X("page:change"),X("page:update")},v=function(){var e,t,n,r,i,o,a,s,l,u,c,d;for(o=Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])')),a=0,l=o.length;l>a;a++)if(i=o[a],""===(c=i.type)||"text/javascript"===c){for(t=document.createElement("script"),d=i.attributes,s=0,u=d.length;u>s;s++)e=d[s],t.setAttribute(e.name,e.value);t.appendChild(document.createTextNode(i.innerHTML)),r=i.parentNode,n=i.nextSibling,r.removeChild(i),r.insertBefore(t,n)}},P=function(e){return e.innerHTML=e.innerHTML.replace(/<noscript[\S\s]*?<\/noscript>/gi,""),e},F=function(e){return(e=new n(e)).absolute!==q?window.history.pushState({turbolinks:!0,url:e.absolute},"",e.absolute):void 0},M=function(){var e,t;return(e=V.getResponseHeader("X-XHR-Redirected-To"))?(e=new n(e),t=e.hasNoHash()?document.location.hash:"",window.history.replaceState(m,"",e.href+t)):void 0},B=function(){return q=document.location.href},R=function(){return window.history.replaceState({turbolinks:!0,url:document.location.href},"",document.location.href)},O=function(){return m=window.history.state},_=function(e){return window.scrollTo(e.positionX,e.positionY)},W=function(){return document.location.hash?document.location.href=document.location.href:window.scrollTo(0,0)},L=function(e){var t,n;return t=(null!=(n=document.cookie.match(new RegExp(e+"=(\\w+)")))?n[1].toUpperCase():void 0)||"",document.cookie=e+"=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/",t},X=function(e,t){var n;return n=document.createEvent("Events"),t&&(n.data=t),n.initEvent(e,!0,!0),document.dispatchEvent(n)},A=function(){return!X("page:before-change")},H=function(){var e,t,n,r,i,o;return t=function(){var e;return 400<=(e=V.status)&&600>e},o=function(){return V.getResponseHeader("Content-Type").match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/)},r=function(e){var t,n,r,i,o;for(i=e.head.childNodes,o=[],n=0,r=i.length;r>n;n++)t=i[n],null!=("function"==typeof t.getAttribute?t.getAttribute("data-turbolinks-track"):void 0)&&o.push(t.getAttribute("src")||t.getAttribute("href"));return o},e=function(e){var t;return S||(S=r(document)),t=r(e),t.length!==S.length||i(t,S).length!==S.length},i=function(e,t){var n,r,i,o,a;for(e.length>t.length&&(o=[t,e],e=o[0],t=o[1]),a=[],r=0,i=e.length;i>r;r++)n=e[r],Y.call(t,n)>=0&&a.push(n);return a},!t()&&o()&&(n=h(V.responseText),n&&!e(n))?n:void 0},y=function(t){var n;return n=t.querySelector("title"),[null!=n?n.textContent:void 0,P(t.body),e.get(t).token,"runScripts"]},e={get:function(e){var t;return null==e&&(e=document),{node:t=e.querySelector('meta[name="csrf-token"]'),token:null!=t&&"function"==typeof t.getAttribute?t.getAttribute("content"):void 0}},update:function(e){var t;return t=this.get(),null!=t.token&&null!=e&&t.token!==e?t.node.setAttribute("content",e):void 0}},i=function(){var e,t,n,r,i,o;t=function(e){return(new DOMParser).parseFromString(e,"text/html")},e=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.documentElement.innerHTML=e,t},n=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.open("replace"),t.write(e),t.close(),t};try{if(window.DOMParser)return i=t("<html><body><p>test"),t}catch(a){return r=a,i=e("<html><body><p>test"),e}finally{if(1!==(null!=i&&null!=(o=i.body)?o.childNodes.length:void 0))return n}},n=function(){function e(t){return this.original=null!=t?t:document.location.href,this.original.constructor===e?this.original:void this._parse()}return e.prototype.withoutHash=function(){return this.href.replace(this.hash,"")},e.prototype.withoutHashForIE10compatibility=function(){return this.withoutHash()},e.prototype.hasNoHash=function(){return 0===this.hash.length},e.prototype._parse=function(){var e;return(null!=this.link?this.link:this.link=document.createElement("a")).href=this.original,e=this.link,this.href=e.href,this.protocol=e.protocol,this.host=e.host,this.hostname=e.hostname,this.port=e.port,this.pathname=e.pathname,this.search=e.search,this.hash=e.hash,this.origin=[this.protocol,"//",this.hostname].join(""),0!==this.port.length&&(this.origin+=":"+this.port),this.relative=[this.pathname,this.search,this.hash].join(""),this.absolute=this.href},e}(),r=function(e){function t(e){return this.link=e,this.link.constructor===t?this.link:(this.original=this.link.href,void t.__super__.constructor.apply(this,arguments))}return K(t,e),t.HTML_EXTENSIONS=["html"],t.allowExtensions=function(){var e,n,r,i;for(n=1<=arguments.length?Q.call(arguments,0):[],r=0,i=n.length;i>r;r++)e=n[r],t.HTML_EXTENSIONS.push(e);return t.HTML_EXTENSIONS},t.prototype.shouldIgnore=function(){return this._crossOrigin()||this._anchored()||this._nonHtml()||this._optOut()||this._target()},t.prototype._crossOrigin=function(){return this.origin!==(new n).origin},t.prototype._anchored=function(){var e;return(this.hash&&this.withoutHash())===(e=new n).withoutHash()||this.href===e.href+"#"},t.prototype._nonHtml=function(){return this.pathname.match(/\.[a-z]+$/g)&&!this.pathname.match(new RegExp("\\.(?:"+t.HTML_EXTENSIONS.join("|")+")?$","g"))},t.prototype._optOut=function(){var e,t;for(t=this.link;!e&&t!==document;)e=null!=t.getAttribute("data-no-turbolink"),t=t.parentNode;return e},t.prototype._target=function(){return 0!==this.link.target.length},t}(n),t=function(){function e(e){this.event=e,this.event.defaultPrevented||(this._extractLink(),this._validForTurbolinks()&&(A()||U(this.link.href),this.event.preventDefault()))}return e.installHandlerLast=function(t){return t.defaultPrevented?void 0:(document.removeEventListener("click",e.handle,!1),document.addEventListener("click",e.handle,!1))},e.handle=function(t){return new e(t)},e.prototype._extractLink=function(){var e;for(e=this.event.target;e.parentNode&&"A"!==e.nodeName;)e=e.parentNode;return"A"===e.nodeName&&0!==e.href.length?this.link=new r(e):void 0},e.prototype._validForTurbolinks=function(){return null!=this.link&&!(this.link.shouldIgnore()||this._nonStandardClick())},e.prototype._nonStandardClick=function(){return this.event.which>1||this.event.metaKey||this.event.ctrlKey||this.event.shiftKey||this.event.altKey},e}(),u=function(e){return setTimeout(e,500)},k=function(){return document.addEventListener("DOMContentLoaded",function(){return X("page:change"),X("page:update")},!0)},N=function(){return"undefined"!=typeof jQuery?jQuery(document).on("ajaxSuccess",function(e,t){return jQuery.trim(t.responseText)?X("page:update"):void 0}):void 0},C=function(e){var t,r;return(null!=(r=e.state)?r.turbolinks:void 0)?(t=j[new n(e.state.url).absolute])?(c(),x(t)):U(e.target.location.href):void 0},E=function(){return R(),O(),h=i(),document.addEventListener("click",t.installHandlerLast,!0),u(function(){return window.addEventListener("popstate",C,!1)})},T=void 0!==window.history.state||navigator.userAgent.match(/Firefox\/2[6|7]/),s=window.history&&window.history.pushState&&window.history.replaceState&&T,o=!navigator.userAgent.match(/CriOS\//),I="GET"===(G=L("request_method"))||""===G,l=s&&o&&I,a=document.addEventListener&&document.createEvent,a&&(k(),N()),l?(U=b,E()):U=function(e){return document.location.href=e},this.Turbolinks={visit:U,pagesCached:D,enableTransitionCache:g,allowLinkExtensions:r.allowExtensions,supported:l}}.call(this),function(){}.call(this);
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/gzip/foo.zoo.gz b/actionpack/test/fixtures/公共/gzip/foo.zoo.gz Binary files differnew file mode 100644 index 0000000000..f62c656dc8 --- /dev/null +++ b/actionpack/test/fixtures/公共/gzip/foo.zoo.gz diff --git a/actionpack/test/fixtures/公共/index.html b/actionpack/test/fixtures/公共/index.html new file mode 100644 index 0000000000..525950ba6b --- /dev/null +++ b/actionpack/test/fixtures/公共/index.html @@ -0,0 +1 @@ +/index.html
\ No newline at end of file diff --git a/actionpack/test/fixtures/公共/other-index.html b/actionpack/test/fixtures/公共/other-index.html new file mode 100644 index 0000000000..0820dfcb6e --- /dev/null +++ b/actionpack/test/fixtures/公共/other-index.html @@ -0,0 +1 @@ +/other-index.html
\ No newline at end of file diff --git a/actionpack/test/journey/gtg/builder_test.rb b/actionpack/test/journey/gtg/builder_test.rb new file mode 100644 index 0000000000..b92460884d --- /dev/null +++ b/actionpack/test/journey/gtg/builder_test.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + module GTG + class TestBuilder < ActiveSupport::TestCase + def test_following_states_multi + table = tt ["a|a"] + assert_equal 1, table.move([0], "a").length + end + + def test_following_states_multi_regexp + table = tt [":a|b"] + assert_equal 1, table.move([0], "fooo").length + assert_equal 2, table.move([0], "b").length + end + + def test_multi_path + table = tt ["/:a/d", "/b/c"] + + [ + [1, "/"], + [2, "b"], + [2, "/"], + [1, "c"], + ].inject([0]) { |state, (exp, sym)| + new = table.move(state, sym) + assert_equal exp, new.length + new + } + end + + def test_match_data_ambiguous + table = tt %w{ + /articles(.:format) + /articles/new(.:format) + /articles/:id/edit(.:format) + /articles/:id(.:format) + } + + sim = NFA::Simulator.new table + + match = sim.match "/articles/new" + assert_equal 2, match.memos.length + end + + ## + # Identical Routes may have different restrictions. + def test_match_same_paths + table = tt %w{ + /articles/new(.:format) + /articles/new(.:format) + } + + sim = NFA::Simulator.new table + + match = sim.match "/articles/new" + assert_equal 2, match.memos.length + end + + private + def ast(strings) + parser = Journey::Parser.new + asts = strings.map { |string| + memo = Object.new + ast = parser.parse string + ast.each { |n| n.memo = memo } + ast + } + Nodes::Or.new asts + end + + def tt(strings) + Builder.new(ast(strings)).transition_table + end + end + end + end +end diff --git a/actionpack/test/journey/gtg/transition_table_test.rb b/actionpack/test/journey/gtg/transition_table_test.rb new file mode 100644 index 0000000000..9044934f05 --- /dev/null +++ b/actionpack/test/journey/gtg/transition_table_test.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/json/decoding" + +module ActionDispatch + module Journey + module GTG + class TestGeneralizedTable < ActiveSupport::TestCase + def test_to_json + table = tt %w{ + /articles(.:format) + /articles/new(.:format) + /articles/:id/edit(.:format) + /articles/:id(.:format) + } + + json = ActiveSupport::JSON.decode table.to_json + assert json["regexp_states"] + assert json["string_states"] + assert json["accepting"] + end + + if system("dot -V", 2 => File::NULL) + def test_to_svg + table = tt %w{ + /articles(.:format) + /articles/new(.:format) + /articles/:id/edit(.:format) + /articles/:id(.:format) + } + svg = table.to_svg + assert svg + assert_no_match(/DOCTYPE/, svg) + end + end + + def test_simulate_gt + sim = simulator_for ["/foo", "/bar"] + assert_match_route sim, "/foo" + end + + def test_simulate_gt_regexp + sim = simulator_for [":foo"] + assert_match_route sim, "foo" + end + + def test_simulate_gt_regexp_mix + sim = simulator_for ["/get", "/:method/foo"] + assert_match_route sim, "/get" + assert_match_route sim, "/get/foo" + end + + def test_simulate_optional + sim = simulator_for ["/foo(/bar)"] + assert_match_route sim, "/foo" + assert_match_route sim, "/foo/bar" + assert_no_match_route sim, "/foo/" + end + + def test_match_data + path_asts = asts %w{ /get /:method/foo } + paths = path_asts.dup + + builder = GTG::Builder.new Nodes::Or.new path_asts + tt = builder.transition_table + + sim = GTG::Simulator.new tt + + memos = sim.memos "/get" + assert_equal [paths.first], memos + + memos = sim.memos "/get/foo" + assert_equal [paths.last], memos + end + + def test_match_data_ambiguous + path_asts = asts %w{ + /articles(.:format) + /articles/new(.:format) + /articles/:id/edit(.:format) + /articles/:id(.:format) + } + + paths = path_asts.dup + ast = Nodes::Or.new path_asts + + builder = GTG::Builder.new ast + sim = GTG::Simulator.new builder.transition_table + + memos = sim.memos "/articles/new" + assert_equal [paths[1], paths[3]], memos + end + + private + def asts(paths) + parser = Journey::Parser.new + paths.map { |x| + ast = parser.parse x + ast.each { |n| n.memo = ast } + ast + } + end + + def tt(paths) + x = asts paths + builder = GTG::Builder.new Nodes::Or.new x + builder.transition_table + end + + def simulator_for(paths) + GTG::Simulator.new tt(paths) + end + + def assert_match_route(simulator, path) + assert simulator.memos(path), "Simulator should match #{path}." + end + + def assert_no_match_route(simulator, path) + assert_not simulator.memos(path) { nil }, "Simulator should not match #{path}." + end + end + end + end +end diff --git a/actionpack/test/journey/nfa/simulator_test.rb b/actionpack/test/journey/nfa/simulator_test.rb new file mode 100644 index 0000000000..6b9f87b452 --- /dev/null +++ b/actionpack/test/journey/nfa/simulator_test.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + module NFA + class TestSimulator < ActiveSupport::TestCase + def test_simulate_simple + sim = simulator_for ["/foo"] + assert_match sim, "/foo" + end + + def test_simulate_simple_no_match + sim = simulator_for ["/foo"] + assert_no_match sim, "foo" + end + + def test_simulate_simple_no_match_too_long + sim = simulator_for ["/foo"] + assert_no_match sim, "/foo/bar" + end + + def test_simulate_simple_no_match_wrong_string + sim = simulator_for ["/foo"] + assert_no_match sim, "/bar" + end + + def test_simulate_regex + sim = simulator_for ["/:foo/bar"] + assert_match sim, "/bar/bar" + assert_match sim, "/foo/bar" + end + + def test_simulate_or + sim = simulator_for ["/foo", "/bar"] + assert_match sim, "/bar" + assert_match sim, "/foo" + assert_no_match sim, "/baz" + end + + def test_simulate_optional + sim = simulator_for ["/foo(/bar)"] + assert_match sim, "/foo" + assert_match sim, "/foo/bar" + assert_no_match sim, "/foo/" + end + + def test_matchdata_has_memos + paths = %w{ /foo /bar } + parser = Journey::Parser.new + asts = paths.map { |x| + ast = parser.parse x + ast.each { |n| n.memo = ast } + ast + } + + expected = asts.first + + builder = Builder.new Nodes::Or.new asts + + sim = Simulator.new builder.transition_table + + md = sim.match "/foo" + assert_equal [expected], md.memos + end + + def test_matchdata_memos_on_merge + parser = Journey::Parser.new + routes = [ + "/articles(.:format)", + "/articles/new(.:format)", + "/articles/:id/edit(.:format)", + "/articles/:id(.:format)", + ].map { |path| + ast = parser.parse path + ast.each { |n| n.memo = ast } + ast + } + + asts = routes.dup + + ast = Nodes::Or.new routes + + nfa = Journey::NFA::Builder.new ast + sim = Simulator.new nfa.transition_table + md = sim.match "/articles" + assert_equal [asts.first], md.memos + end + + def simulator_for(paths) + parser = Journey::Parser.new + asts = paths.map { |x| parser.parse x } + builder = Builder.new Nodes::Or.new asts + Simulator.new builder.transition_table + end + end + end + end +end diff --git a/actionpack/test/journey/nfa/transition_table_test.rb b/actionpack/test/journey/nfa/transition_table_test.rb new file mode 100644 index 0000000000..c23611e980 --- /dev/null +++ b/actionpack/test/journey/nfa/transition_table_test.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + module NFA + class TestTransitionTable < ActiveSupport::TestCase + def setup + @parser = Journey::Parser.new + end + + def test_eclosure + table = tt "/" + assert_equal [0], table.eclosure(0) + + table = tt ":a|:b" + assert_equal 3, table.eclosure(0).length + + table = tt "(:a|:b)" + assert_equal 5, table.eclosure(0).length + assert_equal 5, table.eclosure([0]).length + end + + def test_following_states_one + table = tt "/" + + assert_equal [1], table.following_states(0, "/") + assert_equal [1], table.following_states([0], "/") + end + + def test_following_states_group + table = tt "a|b" + states = table.eclosure 0 + + assert_equal 1, table.following_states(states, "a").length + assert_equal 1, table.following_states(states, "b").length + end + + def test_following_states_multi + table = tt "a|a" + states = table.eclosure 0 + + assert_equal 2, table.following_states(states, "a").length + assert_equal 0, table.following_states(states, "b").length + end + + def test_following_states_regexp + table = tt "a|:a" + states = table.eclosure 0 + + assert_equal 1, table.following_states(states, "a").length + assert_equal 1, table.following_states(states, /[^\.\/\?]+/).length + assert_equal 0, table.following_states(states, "b").length + end + + def test_alphabet + table = tt "a|:a" + assert_equal [/[^\.\/\?]+/, "a"], table.alphabet + + table = tt "a|a" + assert_equal ["a"], table.alphabet + end + + private + def tt(string) + ast = @parser.parse string + builder = Builder.new ast + builder.transition_table + end + end + end + end +end diff --git a/actionpack/test/journey/nodes/symbol_test.rb b/actionpack/test/journey/nodes/symbol_test.rb new file mode 100644 index 0000000000..b0622ac71a --- /dev/null +++ b/actionpack/test/journey/nodes/symbol_test.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + module Nodes + class TestSymbol < ActiveSupport::TestCase + def test_default_regexp? + sym = Symbol.new "foo" + assert_predicate sym, :default_regexp? + + sym.regexp = nil + assert_not_predicate sym, :default_regexp? + end + end + end + end +end diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb new file mode 100644 index 0000000000..3e7aea57f1 --- /dev/null +++ b/actionpack/test/journey/path/pattern_test.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + module Path + class TestPattern < ActiveSupport::TestCase + SEPARATORS = ["/", ".", "?"].join + + x = /.+/ + { + "/:controller(/:action)" => %r{\A/(#{x})(?:/([^/.?]+))?\Z}, + "/:controller/foo" => %r{\A/(#{x})/foo\Z}, + "/:controller/:action" => %r{\A/(#{x})/([^/.?]+)\Z}, + "/:controller" => %r{\A/(#{x})\Z}, + "/:controller(/:action(/:id))" => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z}, + "/:controller/:action.xml" => %r{\A/(#{x})/([^/.?]+)\.xml\Z}, + "/:controller.:format" => %r{\A/(#{x})\.([^/.?]+)\Z}, + "/:controller(.:format)" => %r{\A/(#{x})(?:\.([^/.?]+))?\Z}, + "/:controller/*foo" => %r{\A/(#{x})/(.+)\Z}, + "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar\Z}, + "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))\Z}, + }.each do |path, expected| + define_method(:"test_to_regexp_#{Regexp.escape(path)}") do + path = Pattern.build( + path, + { controller: /.+/ }, + SEPARATORS, + true + ) + assert_equal(expected, path.to_regexp) + end + end + + { + "/:controller(/:action)" => %r{\A/(#{x})(?:/([^/.?]+))?}, + "/:controller/foo" => %r{\A/(#{x})/foo}, + "/:controller/:action" => %r{\A/(#{x})/([^/.?]+)}, + "/:controller" => %r{\A/(#{x})}, + "/:controller(/:action(/:id))" => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?}, + "/:controller/:action.xml" => %r{\A/(#{x})/([^/.?]+)\.xml}, + "/:controller.:format" => %r{\A/(#{x})\.([^/.?]+)}, + "/:controller(.:format)" => %r{\A/(#{x})(?:\.([^/.?]+))?}, + "/:controller/*foo" => %r{\A/(#{x})/(.+)}, + "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar}, + "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))}, + }.each do |path, expected| + define_method(:"test_to_non_anchored_regexp_#{Regexp.escape(path)}") do + path = Pattern.build( + path, + { controller: /.+/ }, + SEPARATORS, + false + ) + assert_equal(expected, path.to_regexp) + end + end + + { + "/:controller(/:action)" => %w{ controller action }, + "/:controller/foo" => %w{ controller }, + "/:controller/:action" => %w{ controller action }, + "/:controller" => %w{ controller }, + "/:controller(/:action(/:id))" => %w{ controller action id }, + "/:controller/:action.xml" => %w{ controller action }, + "/:controller.:format" => %w{ controller format }, + "/:controller(.:format)" => %w{ controller format }, + "/:controller/*foo" => %w{ controller foo }, + "/:controller/*foo/bar" => %w{ controller foo }, + }.each do |path, expected| + define_method(:"test_names_#{Regexp.escape(path)}") do + path = Pattern.build( + path, + { controller: /.+/ }, + SEPARATORS, + true + ) + assert_equal(expected, path.names) + end + end + + def test_to_regexp_with_extended_group + path = Pattern.build( + "/page/:name", + { name: / + #ROFL + (tender|love + #MAO + )/x }, + SEPARATORS, + true + ) + assert_match(path, "/page/tender") + assert_match(path, "/page/love") + assert_no_match(path, "/page/loving") + end + + def test_optional_names + [ + ["/:foo(/:bar(/:baz))", %w{ bar baz }], + ["/:foo(/:bar)", %w{ bar }], + ["/:foo(/:bar)/:lol(/:baz)", %w{ bar baz }], + ].each do |pattern, list| + path = Pattern.from_string pattern + assert_equal list.sort, path.optional_names.sort + end + end + + def test_to_regexp_match_non_optional + path = Pattern.build( + "/:name", + { name: /\d+/ }, + SEPARATORS, + true + ) + assert_match(path, "/123") + assert_no_match(path, "/") + end + + def test_to_regexp_with_group + path = Pattern.build( + "/page/:name", + { name: /(tender|love)/ }, + SEPARATORS, + true + ) + assert_match(path, "/page/tender") + assert_match(path, "/page/love") + assert_no_match(path, "/page/loving") + end + + def test_ast_sets_regular_expressions + requirements = { name: /(tender|love)/, value: /./ } + path = Pattern.build( + "/page/:name/:value", + requirements, + SEPARATORS, + true + ) + + nodes = path.ast.grep(Nodes::Symbol) + assert_equal 2, nodes.length + nodes.each do |node| + assert_equal requirements[node.to_sym], node.regexp + end + end + + def test_match_data_with_group + path = Pattern.build( + "/page/:name", + { name: /(tender|love)/ }, + SEPARATORS, + true + ) + match = path.match "/page/tender" + assert_equal "tender", match[1] + assert_equal 2, match.length + end + + def test_match_data_with_multi_group + path = Pattern.build( + "/page/:name/:id", + { name: /t(((ender|love)))()/ }, + SEPARATORS, + true + ) + match = path.match "/page/tender/10" + assert_equal "tender", match[1] + assert_equal "10", match[2] + assert_equal 3, match.length + assert_equal %w{ tender 10 }, match.captures + end + + def test_star_with_custom_re + z = /\d+/ + path = Pattern.build( + "/page/*foo", + { foo: z }, + SEPARATORS, + true + ) + assert_equal(%r{\A/page/(#{z})\Z}, path.to_regexp) + end + + def test_insensitive_regexp_with_group + path = Pattern.build( + "/page/:name/aaron", + { name: /(tender|love)/i }, + SEPARATORS, + true + ) + assert_match(path, "/page/TENDER/aaron") + assert_match(path, "/page/loVE/aaron") + assert_no_match(path, "/page/loVE/AAron") + end + + def test_to_regexp_with_strexp + path = Pattern.build("/:controller", {}, SEPARATORS, true) + x = %r{\A/([^/.?]+)\Z} + + assert_equal(x.source, path.source) + end + + def test_to_regexp_defaults + path = Pattern.from_string "/:controller(/:action(/:id))" + expected = %r{\A/([^/.?]+)(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z} + assert_equal expected, path.to_regexp + end + + def test_failed_match + path = Pattern.from_string "/:controller(/:action(/:id(.:format)))" + uri = "content" + + assert_not path =~ uri + end + + def test_match_controller + path = Pattern.from_string "/:controller(/:action(/:id(.:format)))" + uri = "/content" + + match = path =~ uri + assert_equal %w{ controller action id format }, match.names + assert_equal "content", match[1] + assert_nil match[2] + assert_nil match[3] + assert_nil match[4] + end + + def test_match_controller_action + path = Pattern.from_string "/:controller(/:action(/:id(.:format)))" + uri = "/content/list" + + match = path =~ uri + assert_equal %w{ controller action id format }, match.names + assert_equal "content", match[1] + assert_equal "list", match[2] + assert_nil match[3] + assert_nil match[4] + end + + def test_match_controller_action_id + path = Pattern.from_string "/:controller(/:action(/:id(.:format)))" + uri = "/content/list/10" + + match = path =~ uri + assert_equal %w{ controller action id format }, match.names + assert_equal "content", match[1] + assert_equal "list", match[2] + assert_equal "10", match[3] + assert_nil match[4] + end + + def test_match_literal + path = Path::Pattern.from_string "/books(/:action(.:format))" + + uri = "/books" + match = path =~ uri + assert_equal %w{ action format }, match.names + assert_nil match[1] + assert_nil match[2] + end + + def test_match_literal_with_action + path = Path::Pattern.from_string "/books(/:action(.:format))" + + uri = "/books/list" + match = path =~ uri + assert_equal %w{ action format }, match.names + assert_equal "list", match[1] + assert_nil match[2] + end + + def test_match_literal_with_action_and_format + path = Path::Pattern.from_string "/books(/:action(.:format))" + + uri = "/books/list.rss" + match = path =~ uri + assert_equal %w{ action format }, match.names + assert_equal "list", match[1] + assert_equal "rss", match[2] + end + end + end + end +end diff --git a/actionpack/test/journey/route/definition/parser_test.rb b/actionpack/test/journey/route/definition/parser_test.rb new file mode 100644 index 0000000000..39693198b8 --- /dev/null +++ b/actionpack/test/journey/route/definition/parser_test.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + module Definition + class TestParser < ActiveSupport::TestCase + def setup + @parser = Parser.new + end + + def test_slash + assert_equal :SLASH, @parser.parse("/").type + assert_round_trip "/" + end + + def test_segment + assert_round_trip "/foo" + end + + def test_segments + assert_round_trip "/foo/bar" + end + + def test_segment_symbol + assert_round_trip "/foo/:id" + end + + def test_symbol + assert_round_trip "/:foo" + end + + def test_group + assert_round_trip "(/:foo)" + end + + def test_groups + assert_round_trip "(/:foo)(/:bar)" + end + + def test_nested_groups + assert_round_trip "(/:foo(/:bar))" + end + + def test_dot_symbol + assert_round_trip(".:format") + end + + def test_dot_literal + assert_round_trip(".xml") + end + + def test_segment_dot + assert_round_trip("/foo.:bar") + end + + def test_segment_group_dot + assert_round_trip("/foo(.:bar)") + end + + def test_segment_group + assert_round_trip("/foo(/:action)") + end + + def test_segment_groups + assert_round_trip("/foo(/:action)(/:bar)") + end + + def test_segment_nested_groups + assert_round_trip("/foo(/:action(/:bar))") + end + + def test_group_followed_by_path + assert_round_trip("/foo(/:action)/:bar") + end + + def test_star + assert_round_trip("*foo") + assert_round_trip("/*foo") + assert_round_trip("/bar/*foo") + assert_round_trip("/bar/(*foo)") + end + + def test_or + assert_round_trip("a|b") + assert_round_trip("a|b|c") + assert_round_trip("(a|b)|c") + assert_round_trip("a|(b|c)") + assert_round_trip("*a|(b|c)") + assert_round_trip("*a|:b|c") + end + + def test_arbitrary + assert_round_trip("/bar/*foo#") + end + + def test_literal_dot_paren + assert_round_trip "/sprockets.js(.:format)" + end + + def test_groups_with_dot + assert_round_trip "/(:locale)(.:format)" + end + + def assert_round_trip(str) + assert_equal str, @parser.parse(str).to_s + end + end + end + end +end diff --git a/actionpack/test/journey/route/definition/scanner_test.rb b/actionpack/test/journey/route/definition/scanner_test.rb new file mode 100644 index 0000000000..092177d315 --- /dev/null +++ b/actionpack/test/journey/route/definition/scanner_test.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + module Definition + class TestScanner < ActiveSupport::TestCase + def setup + @scanner = Scanner.new + end + + CASES = [ + ["/", [[:SLASH, "/"]]], + ["*omg", [[:STAR, "*omg"]]], + ["/page", [[:SLASH, "/"], [:LITERAL, "page"]]], + ["/page!", [[:SLASH, "/"], [:LITERAL, "page!"]]], + ["/page$", [[:SLASH, "/"], [:LITERAL, "page$"]]], + ["/page&", [[:SLASH, "/"], [:LITERAL, "page&"]]], + ["/page'", [[:SLASH, "/"], [:LITERAL, "page'"]]], + ["/page*", [[:SLASH, "/"], [:LITERAL, "page*"]]], + ["/page+", [[:SLASH, "/"], [:LITERAL, "page+"]]], + ["/page,", [[:SLASH, "/"], [:LITERAL, "page,"]]], + ["/page;", [[:SLASH, "/"], [:LITERAL, "page;"]]], + ["/page=", [[:SLASH, "/"], [:LITERAL, "page="]]], + ["/page@", [[:SLASH, "/"], [:LITERAL, "page@"]]], + ['/page\:', [[:SLASH, "/"], [:LITERAL, "page:"]]], + ['/page\(', [[:SLASH, "/"], [:LITERAL, "page("]]], + ['/page\)', [[:SLASH, "/"], [:LITERAL, "page)"]]], + ["/~page", [[:SLASH, "/"], [:LITERAL, "~page"]]], + ["/pa-ge", [[:SLASH, "/"], [:LITERAL, "pa-ge"]]], + ["/:page", [[:SLASH, "/"], [:SYMBOL, ":page"]]], + ["/:page|*foo", [ + [:SLASH, "/"], + [:SYMBOL, ":page"], + [:OR, "|"], + [:STAR, "*foo"] + ]], + ["/(:page)", [ + [:SLASH, "/"], + [:LPAREN, "("], + [:SYMBOL, ":page"], + [:RPAREN, ")"], + ]], + ["(/:action)", [ + [:LPAREN, "("], + [:SLASH, "/"], + [:SYMBOL, ":action"], + [:RPAREN, ")"], + ]], + ["(())", [[:LPAREN, "("], + [:LPAREN, "("], [:RPAREN, ")"], [:RPAREN, ")"]]], + ["(.:format)", [ + [:LPAREN, "("], + [:DOT, "."], + [:SYMBOL, ":format"], + [:RPAREN, ")"], + ]], + ] + + CASES.each do |pattern, expected_tokens| + test "Scanning `#{pattern}`" do + @scanner.scan_setup pattern + assert_tokens expected_tokens, @scanner, pattern + end + end + + private + + def assert_tokens(expected_tokens, scanner, pattern) + actual_tokens = [] + while token = scanner.next_token + actual_tokens << token + end + assert_equal expected_tokens, actual_tokens, "Wrong tokens for `#{pattern}`" + end + end + end + end +end diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb new file mode 100644 index 0000000000..a8bf4a11e2 --- /dev/null +++ b/actionpack/test/journey/route_test.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + class TestRoute < ActiveSupport::TestCase + def test_initialize + app = Object.new + path = Path::Pattern.from_string "/:controller(/:action(/:id(.:format)))" + defaults = {} + route = Route.build("name", app, path, {}, [], defaults) + + assert_equal app, route.app + assert_equal path, route.path + assert_same defaults, route.defaults + end + + def test_route_adds_itself_as_memo + app = Object.new + path = Path::Pattern.from_string "/:controller(/:action(/:id(.:format)))" + defaults = {} + route = Route.build("name", app, path, {}, [], defaults) + + route.ast.grep(Nodes::Terminal).each do |node| + assert_equal route, node.memo + end + end + + def test_path_requirements_override_defaults + path = Path::Pattern.build(":name", { name: /love/ }, "/", true) + defaults = { name: "tender" } + route = Route.build("name", nil, path, {}, [], defaults) + assert_equal(/love/, route.requirements[:name]) + end + + def test_ip_address + path = Path::Pattern.from_string "/messages/:id(.:format)" + route = Route.build("name", nil, path, { ip: "192.168.1.1" }, [], + controller: "foo", action: "bar") + assert_equal "192.168.1.1", route.ip + end + + def test_default_ip + path = Path::Pattern.from_string "/messages/:id(.:format)" + route = Route.build("name", nil, path, {}, [], + controller: "foo", action: "bar") + assert_equal(//, route.ip) + end + + def test_format_with_star + path = Path::Pattern.from_string "/:controller/*extra" + route = Route.build("name", nil, path, {}, [], + controller: "foo", action: "bar") + assert_equal "/foo/himom", route.format( + controller: "foo", + extra: "himom") + end + + def test_connects_all_match + path = Path::Pattern.from_string "/:controller(/:action(/:id(.:format)))" + route = Route.build("name", nil, path, { action: "bar" }, [], controller: "foo") + + assert_equal "/foo/bar/10", route.format( + controller: "foo", + action: "bar", + id: 10) + end + + def test_extras_are_not_included_if_optional + path = Path::Pattern.from_string "/page/:id(/:action)" + route = Route.build("name", nil, path, {}, [], action: "show") + + assert_equal "/page/10", route.format(id: 10) + end + + def test_extras_are_not_included_if_optional_with_parameter + path = Path::Pattern.from_string "(/sections/:section)/pages/:id" + route = Route.build("name", nil, path, {}, [], action: "show") + + assert_equal "/pages/10", route.format(id: 10) + end + + def test_extras_are_not_included_if_optional_parameter_is_nil + path = Path::Pattern.from_string "(/sections/:section)/pages/:id" + route = Route.build("name", nil, path, {}, [], action: "show") + + assert_equal "/pages/10", route.format(id: 10, section: nil) + end + + def test_score + constraints = {} + defaults = { controller: "pages", action: "show" } + + path = Path::Pattern.from_string "/page/:id(/:action)(.:format)" + specific = Route.build "name", nil, path, constraints, [:controller, :action], defaults + + path = Path::Pattern.from_string "/:controller(/:action(/:id))(.:format)" + generic = Route.build "name", nil, path, constraints, [], {} + + knowledge = { "id" => true, "controller" => true, "action" => true } + + routes = [specific, generic] + + assert_not_equal specific.score(knowledge), generic.score(knowledge) + + found = routes.sort_by { |r| r.score(knowledge) }.last + + assert_equal specific, found + end + end + end +end diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb new file mode 100644 index 0000000000..472f1bf35e --- /dev/null +++ b/actionpack/test/journey/router/utils_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + class Router + class TestUtils < ActiveSupport::TestCase + def test_path_escape + assert_equal "a/b%20c+d%25", Utils.escape_path("a/b c+d%") + end + + def test_segment_escape + assert_equal "a%2Fb%20c+d%25", Utils.escape_segment("a/b c+d%") + end + + def test_fragment_escape + assert_equal "a/b%20c+d%25?e", Utils.escape_fragment("a/b c+d%?e") + end + + def test_uri_unescape + assert_equal "a/b c+d", Utils.unescape_uri("a%2Fb%20c+d") + end + + def test_uri_unescape_with_utf8_string + assert_equal "Šašinková", Utils.unescape_uri((+"%C5%A0a%C5%A1inkov%C3%A1").force_encoding(Encoding::US_ASCII)) + end + + def test_normalize_path_not_greedy + assert_equal "/foo%20bar%20baz", Utils.normalize_path("/foo%20bar%20baz") + end + + def test_normalize_path_uppercase + assert_equal "/foo%AAbar%AAbaz", Utils.normalize_path("/foo%aabar%aabaz") + end + + def test_normalize_path_maintains_string_encoding + path = "/foo%AAbar%AAbaz".b + assert_equal Encoding::ASCII_8BIT, Utils.normalize_path(path).encoding + end + + def test_normalize_path_with_nil + assert_equal "/", Utils.normalize_path(nil) + end + end + end + end +end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb new file mode 100644 index 0000000000..1f4e14aef6 --- /dev/null +++ b/actionpack/test/journey/router_test.rb @@ -0,0 +1,544 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + class TestRouter < ActiveSupport::TestCase + attr_reader :mapper, :routes, :route_set, :router + + def setup + @app = Routing::RouteSet::Dispatcher.new({}) + @route_set = ActionDispatch::Routing::RouteSet.new + @routes = @route_set.router.routes + @router = @route_set.router + @formatter = @route_set.formatter + @mapper = ActionDispatch::Routing::Mapper.new @route_set + end + + def test_dashes + get "/foo-bar-baz", to: "foo#bar" + + env = rails_env "PATH_INFO" => "/foo-bar-baz" + called = false + router.recognize(env) do |r, params| + called = true + end + assert called + end + + def test_unicode + get "/ほげ", to: "foo#bar" + + # match the escaped version of /ほげ + env = rails_env "PATH_INFO" => "/%E3%81%BB%E3%81%92" + called = false + router.recognize(env) do |r, params| + called = true + end + assert called + end + + def test_regexp_first_precedence + get "/whois/:domain", domain: /\w+\.[\w\.]+/, to: "foo#bar" + get "/whois/:id(.:format)", to: "foo#baz" + + env = rails_env "PATH_INFO" => "/whois/example.com" + + list = [] + router.recognize(env) do |r, params| + list << r + end + assert_equal 2, list.length + + r = list.first + + assert_equal "/whois/:domain(.:format)", r.path.spec.to_s + end + + def test_required_parts_verified_are_anchored + get "/foo/:id", id: /\d/, anchor: false, to: "foo#bar" + + assert_raises(ActionController::UrlGenerationError) do + @formatter.generate(nil, { controller: "foo", action: "bar", id: "10" }, {}) + end + end + + def test_required_parts_are_verified_when_building + get "/foo/:id", id: /\d+/, anchor: false, to: "foo#bar" + + path, _ = @formatter.generate(nil, { controller: "foo", action: "bar", id: "10" }, {}) + assert_equal "/foo/10", path + + assert_raises(ActionController::UrlGenerationError) do + @formatter.generate(nil, { id: "aa" }, {}) + end + end + + def test_only_required_parts_are_verified + get "/foo(/:id)", id: /\d/, to: "foo#bar" + + path, _ = @formatter.generate(nil, { controller: "foo", action: "bar", id: "10" }, {}) + assert_equal "/foo/10", path + + path, _ = @formatter.generate(nil, { controller: "foo", action: "bar" }, {}) + assert_equal "/foo", path + + path, _ = @formatter.generate(nil, { controller: "foo", action: "bar", id: "aa" }, {}) + assert_equal "/foo/aa", path + end + + def test_knows_what_parts_are_missing_from_named_route + route_name = "gorby_thunderhorse" + get "/foo/:id", as: route_name, id: /\d+/, to: "foo#bar" + + error = assert_raises(ActionController::UrlGenerationError) do + @formatter.generate(route_name, {}, {}) + end + + assert_match(/missing required keys: \[:id\]/, error.message) + end + + def test_does_not_include_missing_keys_message + route_name = "gorby_thunderhorse" + + error = assert_raises(ActionController::UrlGenerationError) do + @formatter.generate(route_name, {}, {}) + end + + assert_no_match(/missing required keys: \[\]/, error.message) + end + + def test_X_Cascade + get "/messages(.:format)", to: "foo#bar" + resp = router.serve(rails_env("REQUEST_METHOD" => "GET", "PATH_INFO" => "/lol")) + assert_equal ["Not Found"], resp.last + assert_equal "pass", resp[1]["X-Cascade"] + assert_equal 404, resp.first + end + + def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes + app = lambda { |env| [200, {}, ["success!"]] } + get "/weblog", to: app + + env = rack_env("SCRIPT_NAME" => "", "PATH_INFO" => "/weblog") + resp = route_set.call env + assert_equal ["success!"], resp.last + assert_equal "", env["SCRIPT_NAME"] + end + + def test_defaults_merge_correctly + get "/foo(/:id)", to: "foo#bar", id: nil + + env = rails_env "PATH_INFO" => "/foo/10" + router.recognize(env) do |r, params| + assert_equal({ id: "10", controller: "foo", action: "bar" }, params) + end + + env = rails_env "PATH_INFO" => "/foo" + router.recognize(env) do |r, params| + assert_equal({ id: nil, controller: "foo", action: "bar" }, params) + end + end + + def test_recognize_with_unbound_regexp + get "/foo", anchor: false, to: "foo#bar" + + env = rails_env "PATH_INFO" => "/foo/bar" + + router.recognize(env) { |*_| } + + assert_equal "/foo", env.env["SCRIPT_NAME"] + assert_equal "/bar", env.env["PATH_INFO"] + end + + def test_bound_regexp_keeps_path_info + get "/foo", to: "foo#bar" + + env = rails_env "PATH_INFO" => "/foo" + + before = env.env["SCRIPT_NAME"] + + router.recognize(env) { |*_| } + + assert_equal before, env.env["SCRIPT_NAME"] + assert_equal "/foo", env.env["PATH_INFO"] + end + + def test_path_not_found + [ + "/messages(.:format)", + "/messages/new(.:format)", + "/messages/:id/edit(.:format)", + "/messages/:id(.:format)" + ].each do |path| + get path, to: "foo#bar" + end + env = rails_env "PATH_INFO" => "/messages/unknown/path" + yielded = false + + router.recognize(env) do |*whatever| + yielded = true + end + assert_not yielded + end + + def test_required_part_in_recall + get "/messages/:a/:b", to: "foo#bar" + + path, _ = @formatter.generate(nil, { controller: "foo", action: "bar", a: "a" }, { b: "b" }) + assert_equal "/messages/a/b", path + end + + def test_splat_in_recall + get "/*path", to: "foo#bar" + + path, _ = @formatter.generate(nil, { controller: "foo", action: "bar" }, { path: "b" }) + assert_equal "/b", path + end + + def test_recall_should_be_used_when_scoring + get "/messages/:action(/:id(.:format))", to: "foo#bar" + get "/messages/:id(.:format)", to: "bar#baz" + + path, _ = @formatter.generate(nil, { controller: "foo", id: 10 }, { action: "index" }) + assert_equal "/messages/index/10", path + end + + def test_nil_path_parts_are_ignored + get "/:controller(/:action(.:format))", to: "tasks#lol" + + params = { controller: "tasks", format: nil } + extras = { action: "lol" } + + path, _ = @formatter.generate(nil, params, extras) + assert_equal "/tasks", path + end + + def test_generate_slash + params = [ [:controller, "tasks"], + [:action, "show"] ] + get "/", Hash[params] + + path, _ = @formatter.generate(nil, Hash[params], {}) + assert_equal "/", path + end + + def test_generate_calls_param_proc + get "/:controller(/:action)", to: "foo#bar" + + parameterized = [] + params = [ [:controller, "tasks"], + [:action, "show"] ] + + @formatter.generate( + nil, + Hash[params], + {}, + lambda { |k, v| parameterized << [k, v]; v }) + + assert_equal params.map(&:to_s).sort, parameterized.map(&:to_s).sort + end + + def test_generate_id + get "/:controller(/:action)", to: "foo#bar" + + path, params = @formatter.generate( + nil, { id: 1, controller: "tasks", action: "show" }, {}) + assert_equal "/tasks/show", path + assert_equal({ id: 1 }, params) + end + + def test_generate_escapes + get "/:controller(/:action)", to: "foo#bar" + + path, _ = @formatter.generate(nil, + { controller: "tasks", + action: "a/b c+d", + }, {}) + assert_equal "/tasks/a%2Fb%20c+d", path + end + + def test_generate_escapes_with_namespaced_controller + get "/:controller(/:action)", to: "foo#bar" + + path, _ = @formatter.generate( + nil, { controller: "admin/tasks", + action: "a/b c+d", + }, {}) + assert_equal "/admin/tasks/a%2Fb%20c+d", path + end + + def test_generate_extra_params + get "/:controller(/:action)", to: "foo#bar" + + path, params = @formatter.generate( + nil, { id: 1, + controller: "tasks", + action: "show", + relative_url_root: nil + }, {}) + assert_equal "/tasks/show", path + assert_equal({ id: 1, relative_url_root: nil }, params) + end + + def test_generate_missing_keys_no_matches_different_format_keys + get "/:controller/:action/:name", to: "foo#bar" + primarty_parameters = { + id: 1, + controller: "tasks", + action: "show", + relative_url_root: nil + } + redirection_parameters = { + "action" => "show", + } + missing_key = "name" + missing_parameters = { + missing_key => "task_1" + } + request_parameters = primarty_parameters.merge(redirection_parameters).merge(missing_parameters) + + message = "No route matches #{Hash[request_parameters.sort_by { |k, v|k.to_s }].inspect}, missing required keys: #{[missing_key.to_sym].inspect}" + + error = assert_raises(ActionController::UrlGenerationError) do + @formatter.generate( + nil, request_parameters, request_parameters) + end + assert_equal message, error.message + end + + def test_generate_uses_recall_if_needed + get "/:controller(/:action(/:id))", to: "foo#bar" + + path, params = @formatter.generate( + nil, + { controller: "tasks", id: 10 }, + { action: "index" }) + assert_equal "/tasks/index/10", path + assert_equal({}, params) + end + + def test_generate_with_name + get "/:controller(/:action)", to: "foo#bar", as: "tasks" + + path, params = @formatter.generate( + "tasks", + { controller: "tasks" }, + { controller: "tasks", action: "index" }) + assert_equal "/tasks", path + assert_equal({}, params) + end + + { + "/content" => { controller: "content" }, + "/content/list" => { controller: "content", action: "list" }, + "/content/show/10" => { controller: "content", action: "show", id: "10" }, + }.each do |request_path, expected| + define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do + get "/:controller(/:action(/:id))", to: "foo#bar" + route = @routes.first + + env = rails_env "PATH_INFO" => request_path + called = false + + router.recognize(env) do |r, params| + assert_equal route, r + assert_equal({ action: "bar" }.merge(expected), params) + called = true + end + + assert called + end + end + + { + segment: ["/a%2Fb%20c+d/splat", { segment: "a/b c+d", splat: "splat" }], + splat: ["/segment/a/b%20c+d", { segment: "segment", splat: "a/b c+d" }] + }.each do |name, (request_path, expected)| + define_method("test_recognize_#{name}") do + get "/:segment/*splat", to: "foo#bar" + + env = rails_env "PATH_INFO" => request_path + called = false + route = @routes.first + + router.recognize(env) do |r, params| + assert_equal route, r + assert_equal(expected.merge(controller: "foo", action: "bar"), params) + called = true + end + + assert called + end + end + + def test_namespaced_controller + get "/:controller(/:action(/:id))", controller: /.+?/ + route = @routes.first + + env = rails_env "PATH_INFO" => "/admin/users/show/10" + called = false + expected = { + controller: "admin/users", + action: "show", + id: "10" + } + + router.recognize(env) do |r, params| + assert_equal route, r + assert_equal(expected, params) + called = true + end + assert called + end + + def test_recognize_literal + get "/books(/:action(.:format))", controller: "books" + route = @routes.first + + env = rails_env "PATH_INFO" => "/books/list.rss" + expected = { controller: "books", action: "list", format: "rss" } + called = false + router.recognize(env) do |r, params| + assert_equal route, r + assert_equal(expected, params) + called = true + end + + assert called + end + + def test_recognize_head_route + match "/books(/:action(.:format))", via: "head", to: "foo#bar" + + env = rails_env( + "PATH_INFO" => "/books/list.rss", + "REQUEST_METHOD" => "HEAD" + ) + + called = false + router.recognize(env) do |r, params| + called = true + end + + assert called + end + + def test_recognize_head_request_as_get_route + get "/books(/:action(.:format))", to: "foo#bar" + + env = rails_env "PATH_INFO" => "/books/list.rss", + "REQUEST_METHOD" => "HEAD" + + called = false + router.recognize(env) do |r, params| + called = true + end + + assert called + end + + def test_recognize_cares_about_get_verbs + match "/books(/:action(.:format))", to: "foo#bar", via: :get + + env = rails_env "PATH_INFO" => "/books/list.rss", + "REQUEST_METHOD" => "POST" + + called = false + router.recognize(env) do |r, params| + called = true + end + + assert_not called + end + + def test_recognize_cares_about_post_verbs + match "/books(/:action(.:format))", to: "foo#bar", via: :post + + env = rails_env "PATH_INFO" => "/books/list.rss", + "REQUEST_METHOD" => "POST" + + called = false + router.recognize(env) do |r, params| + called = true + end + + assert called + end + + def test_multi_verb_recognition + match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get] + + %w( POST GET ).each do |verb| + env = rails_env "PATH_INFO" => "/books/list.rss", + "REQUEST_METHOD" => verb + + called = false + router.recognize(env) do |r, params| + called = true + end + + assert called + end + + env = rails_env "PATH_INFO" => "/books/list.rss", + "REQUEST_METHOD" => "PUT" + + called = false + router.recognize(env) do |r, params| + called = true + end + + assert_not called + end + + def test_eager_load_with_routes + get "/foo-bar", to: "foo#bar" + assert_nil router.eager_load! + end + + def test_eager_load_without_routes + assert_nil router.eager_load! + end + + private + + def get(*args) + ActiveSupport::Deprecation.silence do + mapper.get(*args) + end + end + + def match(*args) + ActiveSupport::Deprecation.silence do + mapper.match(*args) + end + end + + def rails_env(env, klass = ActionDispatch::Request) + klass.new(rack_env(env)) + end + + def rack_env(env) + { + "rack.version" => [1, 1], + "rack.input" => StringIO.new, + "rack.errors" => StringIO.new, + "rack.multithread" => true, + "rack.multiprocess" => true, + "rack.run_once" => false, + "REQUEST_METHOD" => "GET", + "SERVER_NAME" => "example.org", + "SERVER_PORT" => "80", + "QUERY_STRING" => "", + "PATH_INFO" => "/content", + "rack.url_scheme" => "http", + "HTTPS" => "off", + "SCRIPT_NAME" => "", + "CONTENT_LENGTH" => "0" + }.merge env + end + end + end +end diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb new file mode 100644 index 0000000000..d5c81a8421 --- /dev/null +++ b/actionpack/test/journey/routes_test.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Journey + class TestRoutes < ActiveSupport::TestCase + attr_reader :routes, :mapper + + def setup + @route_set = ActionDispatch::Routing::RouteSet.new + @routes = @route_set.router.routes + @router = @route_set.router + @mapper = ActionDispatch::Routing::Mapper.new @route_set + super + end + + def test_clear + mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron" + assert_not_empty routes + assert_equal 1, routes.length + + routes.clear + assert_empty routes + assert_equal 0, routes.length + end + + def test_ast + mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron" + ast = routes.ast + mapper.get "/foo(/:id)", to: "foo#bar", as: "gorby" + assert_not_equal ast, routes.ast + end + + def test_simulator_changes + mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron" + sim = routes.simulator + mapper.get "/foo(/:id)", to: "foo#bar", as: "gorby" + assert_not_equal sim, routes.simulator + end + + def test_partition_route + mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron" + + assert_equal 1, @routes.anchored_routes.length + assert_empty @routes.custom_routes + + mapper.get "/hello/:who", to: "foo#bar", as: "bar", who: /\d/ + + assert_equal 1, @routes.custom_routes.length + assert_equal 1, @routes.anchored_routes.length + end + + def test_first_name_wins + mapper.get "/hello", to: "foo#bar", as: "aaron" + assert_raise(ArgumentError) do + mapper.get "/aaron", to: "foo#bar", as: "aaron" + end + end + end + end +end diff --git a/actionpack/test/lib/controller/fake_controllers.rb b/actionpack/test/lib/controller/fake_controllers.rb new file mode 100644 index 0000000000..e985716f43 --- /dev/null +++ b/actionpack/test/lib/controller/fake_controllers.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class ContentController < ActionController::Base; end + +module Admin + class AccountsController < ActionController::Base; end + class PostsController < ActionController::Base; end + class StuffController < ActionController::Base; end + class UserController < ActionController::Base; end + class UsersController < ActionController::Base; end +end + +module Api + class UsersController < ActionController::Base; end + class ProductsController < ActionController::Base; end +end + +class AccountController < ActionController::Base; end +class ArchiveController < ActionController::Base; end +class ArticlesController < ActionController::Base; end +class BarController < ActionController::Base; end +class BlogController < ActionController::Base; end +class BooksController < ActionController::Base; end +class CarsController < ActionController::Base; end +class CcController < ActionController::Base; end +class CController < ActionController::Base; end +class FooController < ActionController::Base; end +class GeocodeController < ActionController::Base; end +class NewsController < ActionController::Base; end +class NotesController < ActionController::Base; end +class PagesController < ActionController::Base; end +class PeopleController < ActionController::Base; end +class PostsController < ActionController::Base; end +class SubpathBooksController < ActionController::Base; end +class SymbolsController < ActionController::Base; end +class UserController < ActionController::Base; end +class UsersController < ActionController::Base; end diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb new file mode 100644 index 0000000000..01c7ec26ae --- /dev/null +++ b/actionpack/test/lib/controller/fake_models.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "active_model" + +Customer = Struct.new(:name, :id) do + extend ActiveModel::Naming + include ActiveModel::Conversion + + undef_method :to_json + + def to_xml(options = {}) + if options[:builder] + options[:builder].name name + else + "<name>#{name}</name>" + end + end + + def to_js(options = {}) + "name: #{name.inspect}" + end + alias :to_text :to_js + + def errors + [] + end + + def persisted? + id.present? + end + + def cache_key + "#{name}/#{id}" + end +end + +Post = Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) do + extend ActiveModel::Naming + include ActiveModel::Conversion + extend ActiveModel::Translation + + alias_method :secret?, :secret + alias_method :persisted?, :persisted + + def initialize(*args) + super + @persisted = false + end + + attr_accessor :author + def author_attributes=(attributes); end + + attr_accessor :comments, :comment_ids + def comments_attributes=(attributes); end + + attr_accessor :tags + def tags_attributes=(attributes); end +end + +class Comment + extend ActiveModel::Naming + include ActiveModel::Conversion + + attr_reader :id + attr_reader :post_id + def initialize(id = nil, post_id = nil); @id, @post_id = id, post_id end + def to_key; id ? [id] : nil end + def save; @id = 1; @post_id = 1 end + def persisted?; @id.present? end + def to_param; @id.to_s; end + def name + @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}" + end + + attr_accessor :relevances + def relevances_attributes=(attributes); end + + attr_accessor :body +end diff --git a/actionpack/test/routing/helper_test.rb b/actionpack/test/routing/helper_test.rb new file mode 100644 index 0000000000..d13b043b0b --- /dev/null +++ b/actionpack/test/routing/helper_test.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + module Routing + class HelperTest < ActiveSupport::TestCase + class Duck + def to_param + nil + end + end + + def test_exception + rs = ::ActionDispatch::Routing::RouteSet.new + rs.draw do + resources :ducks do + member do + get :pond + end + end + end + + x = Class.new { + include rs.url_helpers + } + assert_raises ActionController::UrlGenerationError do + x.new.pond_duck_path Duck.new + end + end + end + end +end |