diff options
Diffstat (limited to 'actionpack')
24 files changed, 519 insertions, 216 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 5e0c64900f..055d4b4f5b 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,68 @@ +* Non-string authenticity tokens do not raise NoMethodError when decoding + the masked token. + + *Ville Lautanala* + +* Add http_cache_forever to Action Controller, so we can cache a response that never gets expired. + + *arthurnn* + +* ActionController#translate supports symbols as shortcuts. + When shortcut is given it also lookups without action name. + + *Max Melentiev* + +* Expand `ActionController::ConditionalGet#fresh_when` and `stale?` to also + accept a collection of records as the first argument, so that the + following code can be written in a shorter form. + + # Before + def index + @articles = Article.all + fresh_when(etag: @articles, last_modified: @articles.maximum(:updated_at)) + end + + # After + def index + @articles = Article.all + fresh_when(@articles) + end + + *claudiob* + +* Explicitly ignored wildcard verbs when searching for HEAD routes before fallback + + Fixes an issue where a mounted rack app at root would intercept the HEAD + request causing an incorrect behavior during the fall back to GET requests. + + Example: + ```ruby + draw do + get '/home' => 'test#index' + mount rack_app, at: '/' + end + head '/home' + assert_response :success + ``` + In this case, a HEAD request runs through the routes the first time and fails + to match anything. Then, it runs through the list with the fallback and matches + `get '/home'`. The original behavior would match the rack app in the first pass. + + *Terence Sun* + +* Migrating xhr methods to keyword arguments syntax + in `ActionController::TestCase` and `ActionDispatch::Integration` + + Old syntax: + + xhr :get, :create, params: { id: 1 } + + New syntax example: + + get :create, params: { id: 1 }, xhr: true + + *Kir Shatrov* + * Migrating to keyword arguments syntax in `ActionController::TestCase` and `ActionDispatch::Integration` HTTP request methods. diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb index 02028d8e05..56b8ce895e 100644 --- a/actionpack/lib/abstract_controller/translation.rb +++ b/actionpack/lib/abstract_controller/translation.rb @@ -8,14 +8,15 @@ module AbstractController # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive # to translate many keys within the same controller / action and gives you a # simple framework for scoping them consistently. - def translate(*args) - key = args.first - if key.is_a?(String) && (key[0] == '.') - key = "#{ controller_path.tr('/', '.') }.#{ action_name }#{ key }" - args[0] = key + def translate(key, options = {}) + if key.to_s.first == '.' + path = controller_path.tr('/', '.') + defaults = [:"#{path}#{key}"] + defaults << options[:default] if options[:default] + options[:default] = defaults + key = "#{path}.#{action_name}#{key}" end - - I18n.translate(*args) + I18n.translate(key, options) end alias :t :translate diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index febbc72861..858870d8b8 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -57,15 +57,25 @@ module ActionController # This will render the show template if the request isn't sending a matching ETag or # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match. # - # You can also just pass a record where +last_modified+ will be set by calling - # +updated_at+ and the +etag+ by passing the object itself. + # You can also just pass a record. In this case +last_modified+ will be set + # by calling +updated_at+ and +etag+ by passing the object itself. # # def show # @article = Article.find(params[:id]) # fresh_when(@article) # end # - # When passing a record, you can still set whether the public header: + # You can also pass an object that responds to +maximum+, such as a + # collection of active records. In this case +last_modified+ will be set by + # calling +maximum(:updated_at)+ on the collection (the timestamp of the + # most recently updated record) and the +etag+ by passing the object itself. + # + # def index + # @articles = Article.all + # fresh_when(@articles) + # end + # + # When passing a record or a collection, you can still set the public header: # # def show # @article = Article.find(params[:id]) @@ -77,18 +87,16 @@ module ActionController # # before_action { fresh_when @article, template: 'widgets/show' } # - def fresh_when(record_or_options, additional_options = {}) - if record_or_options.is_a? Hash - options = record_or_options - options.assert_valid_keys(:etag, :last_modified, :public, :template) - else - record = record_or_options - options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options) + def fresh_when(object = nil, etag: object, last_modified: nil, public: false, template: nil) + last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at) + + if etag || template + response.etag = combine_etags(etag: etag, last_modified: last_modified, + public: public, template: template) end - response.etag = combine_etags(options) if options[:etag] || options[:template] - response.last_modified = options[:last_modified] if options[:last_modified] - response.cache_control[:public] = true if options[:public] + response.last_modified = last_modified if last_modified + response.cache_control[:public] = true if public head :not_modified if request.fresh?(response) end @@ -123,8 +131,8 @@ module ActionController # end # end # - # You can also just pass a record where +last_modified+ will be set by calling - # +updated_at+ and the +etag+ by passing the object itself. + # You can also just pass a record. In this case +last_modified+ will be set + # by calling +updated_at+ and +etag+ by passing the object itself. # # def show # @article = Article.find(params[:id]) @@ -137,7 +145,23 @@ module ActionController # end # end # - # When passing a record, you can still set whether the public header: + # You can also pass an object that responds to +maximum+, such as a + # collection of active records. In this case +last_modified+ will be set by + # calling +maximum(:updated_at)+ on the collection (the timestamp of the + # most recently updated record) and the +etag+ by passing the object itself. + # + # def index + # @articles = Article.all + # + # if stale?(@articles) + # @statistics = @articles.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end + # end + # end + # + # When passing a record or a collection, you can still set the public header: # # def show # @article = Article.find(params[:id]) @@ -157,8 +181,8 @@ module ActionController # super if stale? @article, template: 'widgets/show' # end # - def stale?(record_or_options, additional_options = {}) - fresh_when(record_or_options, additional_options) + def stale?(object = nil, etag: object, last_modified: nil, public: nil, template: nil) + fresh_when(object, etag: etag, last_modified: last_modified, public: public, template: template) !request.fresh?(response) end @@ -191,6 +215,24 @@ module ActionController response.cache_control.replace(:no_cache => true) end + # Cache or yield the block. The cache is supposed to never expire. + # + # You can use this method when you have a HTTP response that never changes, + # and the browser and proxies should cache it indefinitely. + # + # * +public+: By default, HTTP responses are private, cached only on the + # user's web browser. To allow proxies to cache the response, set +true+ to + # indicate that they can serve the cached response to all users. + # + # * +version+: the version passed as a key for the cache. + def http_cache_forever(public: false, version: 'v1') + expires_in 100.years, public: public + + yield if stale?(etag: "#{version}-#{request.fullpath}", + last_modified: Time.parse('2011-01-01').utc, + public: public) + end + private def combine_etags(options) etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 7facbe79aa..367b736035 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -275,7 +275,9 @@ module ActionController #:nodoc: # session token. Essentially the inverse of # +masked_authenticity_token+. def valid_authenticity_token?(session, encoded_masked_token) - return false if encoded_masked_token.nil? || encoded_masked_token.empty? + if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String) + return false + end begin masked_token = Base64.strict_decode64(encoded_masked_token) diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index b05700aace..4782991463 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -546,8 +546,13 @@ module ActionController end def xml_http_request(*args) + ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) + xhr and xml_http_request methods are deprecated in favor of + `get :index, xhr: true` and `post :create, xhr: true` + MSG + @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') __send__(*args).tap do @request.env.delete 'HTTP_X_REQUESTED_WITH' @request.env.delete 'HTTP_ACCEPT' @@ -600,7 +605,7 @@ module ActionController check_required_ivars if kwarg_request?(*args) - parameters, session, body, flash, http_method, format = args[0].values_at(:params, :session, :body, :flash, :method, :format) + parameters, session, body, flash, http_method, format, xhr = args[0].values_at(:params, :session, :body, :flash, :method, :format, :xhr) else http_method, parameters, session, flash = args format = nil @@ -656,6 +661,11 @@ module ActionController @request.session.update(session) if session @request.flash.update(flash || {}) + if xhr + @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + end + @controller.request = @request @controller.response = @response @@ -681,6 +691,11 @@ module ActionController @request.session.delete('flash') end + if xhr + @request.env.delete 'HTTP_X_REQUESTED_WITH' + @request.env.delete 'HTTP_ACCEPT' + end + @response end @@ -741,7 +756,7 @@ module ActionController end end - REQUEST_KWARGS = %i(params session flash method body) + REQUEST_KWARGS = %i(params session flash method body xhr) def kwarg_request?(*args) args[0].respond_to?(:keys) && ( (args[0].key?(:format) && args[0].keys.size == 1) || diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 2b036796ab..e9df984c86 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -121,6 +121,7 @@ module ActionDispatch end def match_head_routes(routes, req) + routes.delete_if { |route| route.verb == // } head_routes = match_routes(routes, req) if head_routes.empty? diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 5595a73887..d176a73633 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,5 +1,6 @@ require 'action_controller/metal/exceptions' require 'active_support/core_ext/module/attribute_accessors' +require 'rack/utils' module ActionDispatch class ExceptionWrapper diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 25658bac3d..cf31815c23 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -12,19 +12,23 @@ module ActionDispatch # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files # from multiple pieces of the stack. class RequestId + X_REQUEST_ID = "X-Request-Id".freeze # :nodoc: + ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc: + HTTP_X_REQUEST_ID = "HTTP_X_REQUEST_ID".freeze # :nodoc: + def initialize(app) @app = app end def call(env) - env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id - @app.call(env).tap { |_status, headers, _body| headers["X-Request-Id"] = env["action_dispatch.request_id"] } + env[ACTION_DISPATCH_REQUEST_ID] = external_request_id(env) || internal_request_id + @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = env[ACTION_DISPATCH_REQUEST_ID] } end private def external_request_id(env) - if request_id = env["HTTP_X_REQUEST_ID"].presence - request_id.gsub(/[^\w\-]/, "").first(255) + if request_id = env[HTTP_X_REQUEST_ID].presence + request_id.gsub(/[^\w\-]/, "".freeze).first(255) end end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 002bf8b11a..2e1bd45c3d 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -23,10 +23,9 @@ module ActionDispatch def match?(path) path = URI.parser.unescape(path) return false unless path.valid_encoding? + path = Rack::Utils.clean_path_info path - paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"].map { |v| - Rack::Utils.clean_path_info v - } + paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"] if match = paths.detect { |p| path = File.join(@root, p) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 948cbdc014..ce04f0b08a 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -395,9 +395,11 @@ module ActionDispatch return if MountedHelpers.method_defined?(name) routes = self + helpers = routes.url_helpers + MountedHelpers.class_eval do define_method "_#{name}" do - RoutesProxy.new(routes, _routes_context) + RoutesProxy.new(routes, _routes_context, helpers) end end @@ -409,14 +411,6 @@ module ActionDispatch end def url_helpers(supports_path = true) - if supports_path - @url_helpers_with_paths ||= generate_url_helpers(supports_path) - else - @url_helpers_without_paths ||= generate_url_helpers(supports_path) - end - end - - def generate_url_helpers(supports_path) routes = self Module.new do diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb index e2393d3799..040ea04046 100644 --- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -8,8 +8,9 @@ module ActionDispatch attr_accessor :scope, :routes alias :_routes :routes - def initialize(routes, scope) + def initialize(routes, scope, helpers) @routes, @scope = routes, scope + @helpers = helpers end def url_options @@ -19,16 +20,16 @@ module ActionDispatch end def respond_to?(method, include_private = false) - super || routes.url_helpers.respond_to?(method) + super || @helpers.respond_to?(method) end def method_missing(method, *args) - if routes.url_helpers.respond_to?(method) + if @helpers.respond_to?(method) self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args) options = args.extract_options! args << url_options.merge((options || {}).symbolize_keys) - routes.url_helpers.#{method}(*args) + @helpers.#{method}(*args) end RUBY send(method, *args) diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index a927738b42..c94eea9134 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -38,18 +38,24 @@ module ActionDispatch # # Test a custom route # assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1') def assert_recognizes(expected_options, path, extras={}, msg=nil) - request = recognized_request_for(path, extras, msg) + if path.is_a?(Hash) && path[:method].to_s == "all" + [:get, :post, :put, :delete].each do |method| + assert_recognizes(expected_options, path.merge(method: method), extras, msg) + end + else + request = recognized_request_for(path, extras, msg) - expected_options = expected_options.clone + expected_options = expected_options.clone - expected_options.stringify_keys! + expected_options.stringify_keys! - msg = message(msg, "") { - sprintf("The recognized options <%s> did not match <%s>, difference:", - request.path_parameters, expected_options) - } + msg = message(msg, "") { + sprintf("The recognized options <%s> did not match <%s>, difference:", + request.path_parameters, expected_options) + } - assert_equal(expected_options, request.path_parameters, msg) + assert_equal(expected_options, request.path_parameters, msg) + end end # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+. diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index db6233840a..9b00616968 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -92,11 +92,12 @@ module ActionDispatch end end - headers ||= {} - headers.merge!(env) if env - headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') - process(request_method, path, params: params, headers: headers) + ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) + xhr and xml_http_request methods are deprecated in favor of + `get "/posts", xhr: true` and `post "/posts/1", xhr: true` + MSG + + process(request_method, path, params: params, headers: headers, xhr: true) end alias xhr :xml_http_request @@ -221,15 +222,6 @@ module ActionDispatch super() @app = app - # If the app is a Rails app, make url_helpers available on the session - # This makes app.url_for and app.foo_path available in the console - if app.respond_to?(:routes) - singleton_class.class_eval do - include app.routes.url_helpers - include app.routes.mounted_helpers - end - end - reset! end @@ -306,30 +298,29 @@ module ActionDispatch end end - REQUEST_KWARGS = %i(params headers env) + REQUEST_KWARGS = %i(params headers env xhr) def kwarg_request?(*args) args[0].respond_to?(:keys) && args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } end def non_kwarg_request_warning ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc) - ActionDispatch::Integration::TestCase HTTP request methods will accept only - keyword arguments in future Rails versions. + ActionDispatch::IntegrationTest HTTP request methods will accept only + the following keyword arguments in future Rails versions: + #{REQUEST_KWARGS.join(', ')} Examples: get '/profile', params: { id: 1 }, headers: { 'X-Extra-Header' => '123' }, - env: { 'action_dispatch.custom' => 'custom' } - - xhr :post, '/profile', - params: { id: 1 } + env: { 'action_dispatch.custom' => 'custom' }, + xhr: true MSG end # Performs the actual request. - def process(method, path, params: nil, headers: nil, env: nil) + def process(method, path, params: nil, headers: nil, env: nil, xhr: false) if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme @@ -354,6 +345,13 @@ module ActionDispatch "CONTENT_TYPE" => "application/x-www-form-urlencoded", "HTTP_ACCEPT" => accept } + + if xhr + headers ||= {} + headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + end + # this modifies the passed request_env directly if headers.present? Http::Headers.new(request_env).merge!(headers) @@ -377,7 +375,7 @@ module ActionDispatch @controller = session.last_request.env['action_controller.instance'] - return response.status + response.status end def build_full_uri(path, env) @@ -388,14 +386,36 @@ module ActionDispatch module Runner include ActionDispatch::Assertions - def app - @app ||= nil + APP_SESSIONS = {} + + attr_reader :app + + def before_setup + super + @app = nil + @integration_session = nil + end + + def integration_session + @integration_session ||= create_session(app) end # Reset the current session. This is useful for testing multiple sessions # in a single test case. def reset! - @integration_session = Integration::Session.new(app) + @integration_session = create_session(app) + end + + def create_session(app) + klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) { + # If the app is a Rails app, make url_helpers available on the session + # This makes app.url_for and app.foo_path available in the console + if app.respond_to?(:routes) + include app.routes.url_helpers + include app.routes.mounted_helpers + end + } + klass.new(app) end def remove! # :nodoc: @@ -405,8 +425,6 @@ module ActionDispatch %w(get post patch put head delete cookies assigns xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| - reset! unless integration_session - # reset the html_document variable, except for cookies/assigns calls unless method == 'cookies' || method == 'assigns' @html_document = nil @@ -438,19 +456,16 @@ module ActionDispatch # Copy the instance variables from the current session instance into the # test instance. def copy_session_variables! #:nodoc: - return unless integration_session - %w(controller response request).each do |var| - instance_variable_set("@#{var}", @integration_session.__send__(var)) - end + @controller = @integration_session.controller + @response = @integration_session.response + @request = @integration_session.request end def default_url_options - reset! unless integration_session integration_session.default_url_options end def default_url_options=(options) - reset! unless integration_session integration_session.default_url_options = options end @@ -460,7 +475,6 @@ module ActionDispatch # Delegate unhandled messages to the current session instance. def method_missing(sym, *args, &block) - reset! unless integration_session if integration_session.respond_to?(sym) integration_session.__send__(sym, *args, &block).tap do copy_session_variables! @@ -469,11 +483,6 @@ module ActionDispatch super end end - - private - def integration_session - @integration_session ||= nil - end end end @@ -650,7 +659,6 @@ module ActionDispatch end def url_options - reset! unless integration_session integration_session.url_options end diff --git a/actionpack/test/abstract/translation_test.rb b/actionpack/test/abstract/translation_test.rb index 4fdc480b43..8289252dfc 100644 --- a/actionpack/test/abstract/translation_test.rb +++ b/actionpack/test/abstract/translation_test.rb @@ -9,6 +9,22 @@ module AbstractController 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', + }, + }, + }, + }) + @controller.stubs(action_name: :index) end def test_action_controller_base_responds_to_translate @@ -28,16 +44,19 @@ module AbstractController end def test_lazy_lookup - expected = 'bar' - @controller.stubs(action_name: :index) - I18n.stubs(:translate).with('abstract_controller.testing.translation.index.foo').returns(expected) - assert_equal expected, @controller.t('.foo') + assert_equal 'bar', @controller.t('.foo') + end + + def test_lazy_lookup_with_symbol + assert_equal 'bar', @controller.t(:'.foo') + end + + def test_lazy_lookup_fallback + assert_equal 'no_action_tr', @controller.t(:'.no_action') end def test_default_translation - key, expected = 'one.two', 'bar' - I18n.stubs(:translate).with(key).returns(expected) - assert_equal expected, @controller.t(key) + assert_equal 'bar', @controller.t('one.two') end def test_localize diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 17d5bae00b..9ab1549e8e 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -195,133 +195,112 @@ class SessionTest < ActiveSupport::TestCase def test_xml_http_request_get path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:get, path, params: params, headers: headers_after_xhr) - @session.xml_http_request(:get, path, params: params, headers: headers) + @session.expects(:process).with(:get, path, params: params, headers: headers, xhr: true) + @session.get(path, params: params, headers: headers, xhr: true) end def test_deprecated_xml_http_request_get path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:get, path, params: params, headers: headers_after_xhr) - assert_deprecated { - @session.xml_http_request(:get,path,params,headers) + @session.expects(:process).with(:get, path, params: params, headers: headers, xhr: true) + @session.get(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_args_xml_http_request_get + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:get, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { + @session.xml_http_request(:get, path, params, headers) } end def test_xml_http_request_post path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:post, path, params: params, headers: headers_after_xhr) - @session.xml_http_request(:post, path, params: params, headers: headers) + @session.expects(:process).with(:post, path, params: params, headers: headers, xhr: true) + @session.post(path, params: params, headers: headers, xhr: true) end def test_deprecated_xml_http_request_post path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:post, path, params: params, headers: headers_after_xhr) - assert_deprecated { @session.xml_http_request(:post,path,params,headers) } + @session.expects(:process).with(:post, path, params: params, headers: headers, xhr: true) + @session.post(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_args_xml_http_request_post + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:post, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:post,path,params,headers) } end def test_xml_http_request_patch path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:patch, path, params: params, headers: headers_after_xhr) - @session.xml_http_request(:patch, path, params: params, headers: headers) + @session.expects(:process).with(:patch, path, params: params, headers: headers, xhr: true) + @session.patch(path, params: params, headers: headers, xhr: true) end def test_deprecated_xml_http_request_patch path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:patch, path, params: params, headers: headers_after_xhr) - assert_deprecated { @session.xml_http_request(:patch,path,params,headers) } + @session.expects(:process).with(:patch, path, params: params, headers: headers, xhr: true) + @session.patch(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_args_xml_http_request_patch + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:patch, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:patch,path,params,headers) } end def test_xml_http_request_put path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:put, path, params: params, headers: headers_after_xhr) - @session.xml_http_request(:put, path, params: params, headers: headers) + @session.expects(:process).with(:put, path, params: params, headers: headers, xhr: true) + @session.put(path, params: params, headers: headers, xhr: true) end def test_deprecated_xml_http_request_put path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:put, path, params: params, headers: headers_after_xhr) - assert_deprecated { @session.xml_http_request(:put, path, params, headers) } + @session.expects(:process).with(:put, path, params: params, headers: headers, xhr: true) + @session.put(path, params: params, headers: headers, xhr: true) + end + + def test_deprecated_args_xml_http_request_put + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:put, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:put, path, params, headers) } end def test_xml_http_request_delete path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:delete, path, params: params, headers: headers_after_xhr) - @session.xml_http_request(:delete, path, params: params, headers: headers) + @session.expects(:process).with(:delete, path, params: params, headers: headers, xhr: true) + @session.delete(path, params: params, headers: headers, xhr: true) end def test_deprecated_xml_http_request_delete path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:delete, path, params: params, headers: headers_after_xhr) - assert_deprecated { @session.xml_http_request(:delete, path, params, headers) } + @session.expects(:process).with(:delete, path, params: params, headers: headers, xhr: true) + assert_deprecated { @session.xml_http_request(:delete, path, params: params, headers: headers) } + end + + def test_deprecated_args_xml_http_request_delete + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:delete, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:delete, path, params, headers) } end def test_xml_http_request_head path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:head, path, params: params, headers: headers_after_xhr) - @session.xml_http_request(:head, path, params: params, headers: headers) + @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true) + @session.head(path, params: params, headers: headers, xhr: true) end def test_deprecated_xml_http_request_head path = "/index"; params = "blah"; headers = { location: 'blah' } - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" - ) - @session.expects(:process).with(:head, path, params: params, headers: headers_after_xhr) - assert_deprecated { @session.xml_http_request(:head, path, params, headers) } + @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true) + assert_deprecated(/xml_http_request/) { @session.xml_http_request(:head, path, params: params, headers: headers) } end - def test_xml_http_request_override_accept - path = "/index"; params = "blah"; headers = {:location => 'blah', "HTTP_ACCEPT" => "application/xml"} - headers_after_xhr = headers.merge( - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" - ) - @session.expects(:process).with(:post, path, params: params, headers: headers_after_xhr) - @session.xml_http_request(:post, path, params: params, headers: headers) + def test_deprecated_args_xml_http_request_head + path = "/index"; params = "blah"; headers = { location: 'blah' } + @session.expects(:process).with(:head, path, params: params, headers: headers, xhr: true) + assert_deprecated { @session.xml_http_request(:head, path, params, headers) } end end @@ -526,7 +505,19 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest def test_xml_http_request_get with_test_route_set do - xhr :get, '/get' + 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_deprecated_xml_http_request_get + with_test_route_set do + assert_deprecated { xhr :get, '/get' } assert_equal 200, status assert_equal "OK", status_message assert_response 200 @@ -538,7 +529,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest def test_request_with_bad_format with_test_route_set do - xhr :get, '/get.php' + get '/get.php', xhr: true assert_equal 406, status assert_response 406 assert_response :not_acceptable diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 8bd32f9584..1f5f66dc80 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -310,17 +310,17 @@ class RespondToControllerTest < ActionController::TestCase def test_js_or_html @request.accept = "text/javascript, text/html" - xhr :get, :js_or_html + get :js_or_html, xhr: true assert_equal 'JS', @response.body @request.accept = "text/javascript, text/html" - xhr :get, :html_or_xml + get :html_or_xml, xhr: true assert_equal 'HTML', @response.body @request.accept = "text/javascript, text/html" assert_raises(ActionController::UnknownFormat) do - xhr :get, :just_xml + get :just_xml, xhr: true end end @@ -335,7 +335,7 @@ class RespondToControllerTest < ActionController::TestCase end def test_json_or_yaml - xhr :get, :json_or_yaml + get :json_or_yaml, xhr: true assert_equal 'JSON', @response.body get :json_or_yaml, format: 'json' @@ -357,13 +357,13 @@ class RespondToControllerTest < ActionController::TestCase def test_js_or_anything @request.accept = "text/javascript, */*" - xhr :get, :js_or_html + get :js_or_html, xhr: true assert_equal 'JS', @response.body - xhr :get, :html_or_xml + get :html_or_xml, xhr: true assert_equal 'HTML', @response.body - xhr :get, :just_xml + get :just_xml, xhr: true assert_equal 'XML', @response.body end @@ -408,14 +408,14 @@ class RespondToControllerTest < ActionController::TestCase def test_with_atom_content_type @request.accept = "" @request.env["CONTENT_TYPE"] = "application/atom+xml" - xhr :get, :made_for_content_type + 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" - xhr :get, :made_for_content_type + get :made_for_content_type, xhr: true assert_equal "RSS", @response.body end @@ -525,7 +525,7 @@ class RespondToControllerTest < ActionController::TestCase end def test_xhr - xhr :get, :js_or_html + get :js_or_html, xhr: true assert_equal 'JS', @response.body end diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb index d482df195f..6b661de064 100644 --- a/actionpack/test/controller/render_js_test.rb +++ b/actionpack/test/controller/render_js_test.rb @@ -22,13 +22,13 @@ class RenderJSTest < ActionController::TestCase tests TestController def test_render_vanilla_js - xhr :get, :render_vanilla_js_hello + 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 - xhr :get, :show_partial, format: 'js' + 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 index ada978aa11..b1ad16bc55 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -100,13 +100,13 @@ class RenderJsonTest < ActionController::TestCase end def test_render_json_with_callback - xhr :get, :render_json_hello_world_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 - xhr :get, :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 diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 1c3f436cfa..488585c7a4 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -58,6 +58,27 @@ class TestController < ActionController::Base end 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' @@ -288,7 +309,6 @@ class LastModifiedRenderTest < ActionController::TestCase 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'] @@ -299,6 +319,7 @@ class LastModifiedRenderTest < ActionController::TestCase get :conditional_hello_with_record assert_equal 304, @response.status.to_i assert @response.body.blank? + assert_not_nil @response.etag assert_equal @last_modified, @response.headers['Last-Modified'] end @@ -317,6 +338,34 @@ class LastModifiedRenderTest < ActionController::TestCase assert_equal @last_modified, @response.headers['Last-Modified'] end + def test_responds_with_last_modified_with_collection_of_records + get :conditional_hello_with_collection_of_records + assert_equal @last_modified, @response.headers['Last-Modified'] + end + + def test_request_not_modified_with_collection_of_records + @request.if_modified_since = @last_modified + get :conditional_hello_with_collection_of_records + assert_equal 304, @response.status.to_i + assert @response.body.blank? + assert_equal @last_modified, @response.headers['Last-Modified'] + end + + def test_request_not_modified_but_etag_differs_with_collection_of_records + @request.if_modified_since = @last_modified + @request.if_none_match = "234" + get :conditional_hello_with_collection_of_records + assert_response :success + end + + def test_request_modified_with_collection_of_records + @request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT' + get :conditional_hello_with_collection_of_records + assert_equal 200, @response.status.to_i + assert @response.body.present? + assert_equal @last_modified, @response.headers['Last-Modified'] + end + def test_request_with_bang_gets_last_modified get :conditional_hello_with_bangs assert_equal @last_modified, @response.headers['Last-Modified'] @@ -512,3 +561,56 @@ class HeadRenderTest < ActionController::TestCase assert_response :forbidden end end + +class HttpCacheForeverTest < ActionController::TestCase + class HttpCacheForeverController < ActionController::Base + def cache_me_forever + http_cache_forever(public: params[:public], version: params[:version] || 'v1') do + render text: 'hello' + end + end + end + + tests HttpCacheForeverController + + def test_cache_with_public + get :cache_me_forever, params: {public: true} + assert_equal "max-age=#{100.years.to_i}, public", @response.headers["Cache-Control"] + assert_not_nil @response.etag + end + + def test_cache_with_private + get :cache_me_forever + assert_equal "max-age=#{100.years.to_i}, private", @response.headers["Cache-Control"] + assert_not_nil @response.etag + assert_response :success + end + + def test_cache_response_code_with_if_modified_since + get :cache_me_forever + assert_response :success + @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 :success + @request.if_modified_since = @response.headers['Last-Modified'] + @request.if_none_match = @response.etag + + get :cache_me_forever + assert_response :not_modified + @request.if_modified_since = @response.headers['Last-Modified'] + @request.if_none_match = @response.etag + + get :cache_me_forever, params: {version: 'v2'} + assert_response :success + @request.if_modified_since = @response.headers['Last-Modified'] + @request.if_none_match = @response.etag + + get :cache_me_forever, params: {version: 'v2'} + assert_response :not_modified + end +end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 8a0eed5a87..8887f291cf 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -262,7 +262,7 @@ module RequestForgeryProtectionTests end def test_should_not_allow_xhr_post_without_token - assert_blocked { xhr :post, :index } + assert_blocked { post :index, xhr: true } end def test_should_allow_post_with_token @@ -340,11 +340,11 @@ module RequestForgeryProtectionTests get :negotiate_same_origin end - assert_cross_origin_not_blocked { xhr :get, :same_origin_js } - assert_cross_origin_not_blocked { xhr :get, :same_origin_js, format: 'js'} + 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' - xhr :get, :negotiate_same_origin + get :negotiate_same_origin, xhr: true end end @@ -366,11 +366,18 @@ module RequestForgeryProtectionTests get :negotiate_cross_origin end - assert_cross_origin_not_blocked { xhr :get, :cross_origin_js } - assert_cross_origin_not_blocked { xhr :get, :cross_origin_js, format: 'js' } + 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' - xhr :get, :negotiate_cross_origin + get :negotiate_cross_origin, xhr: true + end + end + + def test_should_not_raise_error_if_token_is_not_a_string + @controller.unstub(:valid_authenticity_token?) + assert_blocked do + patch :index, params: { custom_authenticity_token: { foo: 'bar' } } end end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 02e7614ba2..f3da2df3ef 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -1047,6 +1047,28 @@ class ResourcesTest < ActionController::TestCase 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(:preferences) do assert_singleton_restful_for :preferences diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 25337a7dd4..ca854040b7 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -706,19 +706,34 @@ XML end def test_header_properly_reset_after_remote_http_request - xhr :get, :test_params + get :test_params, xhr: true assert_nil @request.env['HTTP_X_REQUESTED_WITH'] assert_nil @request.env['HTTP_ACCEPT'] end + def test_deprecated_xhr_with_params + assert_deprecated { xhr :get, :test_params, params: { id: 1 } } + + assert_equal(%({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}), @response.body) + end + def test_xhr_with_params - xhr :get, :test_params, params: { id: 1 } + get :test_params, params: { id: 1 }, xhr: true assert_equal(%({"id"=>"1", "controller"=>"test_case_test/test", "action"=>"test_params"}), @response.body) end def test_xhr_with_session - xhr :get, :set_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_deprecated_xhr_with_session + assert_deprecated { xhr :get, :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" diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index c61423dce4..5fbd19acdf 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -254,10 +254,6 @@ class ResponseTest < ActiveSupport::TestCase end class ResponseIntegrationTest < ActionDispatch::IntegrationTest - def app - @app - end - test "response cache control from railsish app" do @app = lambda { |env| ActionDispatch::Response.new.tap { |resp| diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 3a95a9025f..ca5de05814 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3477,6 +3477,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest 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| [404, {"Content-Type" => "text/html"}, ["testing"]] }, at: '/' + end + head '/home' + assert_response :success + + head '/' + assert_response :not_found + end + private def draw(&block) |