aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack')
-rw-r--r--actionpack/CHANGELOG.md67
-rw-r--r--actionpack/lib/abstract_controller.rb1
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb4
-rw-r--r--actionpack/lib/action_controller.rb2
-rw-r--r--actionpack/lib/action_controller/base.rb1
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb2
-rw-r--r--actionpack/lib/action_controller/metal/feature_policy.rb46
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb4
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb17
-rw-r--r--actionpack/lib/action_dispatch.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb33
-rw-r--r--actionpack/lib/action_dispatch/http/feature_policy.rb168
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb20
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb44
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb15
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb6
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb4
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb36
-rw-r--r--actionpack/lib/action_dispatch/system_testing/browser.rb23
-rw-r--r--actionpack/lib/action_dispatch/system_testing/driver.rb2
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb11
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb12
-rw-r--r--actionpack/test/controller/integration_test.rb39
-rw-r--r--actionpack/test/controller/parameters/accessors_test.rb8
-rw-r--r--actionpack/test/controller/parameters/mutators_test.rb18
-rw-r--r--actionpack/test/controller/render_test.rb24
-rw-r--r--actionpack/test/controller/resources_test.rb3
-rw-r--r--actionpack/test/dispatch/content_security_policy_test.rb78
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb31
-rw-r--r--actionpack/test/dispatch/exception_wrapper_test.rb2
-rw-r--r--actionpack/test/dispatch/feature_policy_test.rb142
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb12
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb2
-rw-r--r--actionpack/test/dispatch/request_test.rb18
-rw-r--r--actionpack/test/dispatch/system_testing/driver_test.rb13
-rw-r--r--actionpack/test/dispatch/system_testing/system_test_case_test.rb8
40 files changed, 828 insertions, 98 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 9aa592f1a0..a502e85044 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -7,6 +7,73 @@
*Tom Fakes*
+* Add `Vary: Accept` header when using `Accept` header for response
+
+ For some requests like `/users/1`, Rails uses requests' `Accept`
+ header to determine what to return. And if we don't add `Vary`
+ in the response header, browsers might accidentally cache different
+ types of content, which would cause issues: e.g. javascript got displayed
+ instead of html content. This PR fixes these issues by adding `Vary: Accept`
+ in these types of requests. For more detailed problem description, please read:
+
+ https://github.com/rails/rails/pull/36213
+
+ Fixes #25842
+
+ *Stan Lo*
+
+* Fix IntegrationTest `follow_redirect!` to follow redirection using the same HTTP verb when following
+ a 307 redirection.
+
+ *Edouard Chin*
+
+* System tests require Capybara 3.26 or newer.
+
+ *George Claghorn*
+
+* Reduced log noise handling ActionController::RoutingErrors.
+
+ *Alberto Fernández-Capel*
+
+* Add DSL for configuring HTTP Feature Policy
+
+ This new DSL provides a way to configure a HTTP Feature Policy at a
+ global or per-controller level. Full details of HTTP Feature Policy
+ specification and guidelines can be found at MDN:
+
+ https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
+
+ Example global policy
+
+ ```
+ Rails.application.config.feature_policy do |f|
+ f.camera :none
+ f.gyroscope :none
+ f.microphone :none
+ f.usb :none
+ f.fullscreen :self
+ f.payment :self, "https://secure.example.com"
+ end
+ ```
+
+ Example controller level policy
+
+ ```
+ class PagesController < ApplicationController
+ feature_policy do |p|
+ p.geolocation "https://example.com"
+ end
+ end
+ ```
+
+ *Jacob Bednarz*
+
+* Add the ability to set the CSP nonce only to the specified directives.
+
+ Fixes #35137.
+
+ *Yuji Yaginuma*
+
* Keep part when scope option has value.
When a route was defined within an optional scope, if that route didn't
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb
index 3a98931167..d1ff62a032 100644
--- a/actionpack/lib/abstract_controller.rb
+++ b/actionpack/lib/abstract_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "action_pack"
+require "active_support"
require "active_support/rails"
require "active_support/i18n"
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 8ba2b25552..280af50a44 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -28,6 +28,7 @@ module AbstractController
else
_set_rendered_content_type rendered_format
end
+ _set_vary_header
self.response_body = rendered_body
end
@@ -109,6 +110,9 @@ module AbstractController
def _set_html_content_type # :nodoc:
end
+ def _set_vary_header # :nodoc:
+ end
+
def _set_rendered_content_type(format) # :nodoc:
end
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 29d61c3ceb..22dc229599 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-require "active_support/rails"
require "abstract_controller"
require "action_dispatch"
require "action_controller/metal/live"
@@ -28,6 +27,7 @@ module ActionController
autoload :DefaultHeaders
autoload :EtagWithTemplateDigest
autoload :EtagWithFlash
+ autoload :FeaturePolicy
autoload :Flash
autoload :ForceSSL
autoload :Head
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 2e565d5d44..63c138af55 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -226,6 +226,7 @@ module ActionController
FormBuilder,
RequestForgeryProtection,
ContentSecurityPolicy,
+ FeaturePolicy,
ForceSSL,
Streaming,
DataStreaming,
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 9ef4f50df1..879745a895 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -53,7 +53,7 @@ module ActionController #:nodoc:
#
# Show a 404 page in the browser:
#
- # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', status: 404
+ # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', disposition: 'inline', status: 404
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description) in
diff --git a/actionpack/lib/action_controller/metal/feature_policy.rb b/actionpack/lib/action_controller/metal/feature_policy.rb
new file mode 100644
index 0000000000..a627eabea6
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/feature_policy.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module ActionController #:nodoc:
+ # HTTP Feature Policy is a web standard for defining a mechanism to
+ # allow and deny the use of browser features in its own context, and
+ # in content within any <iframe> elements in the document.
+ #
+ # Full details of HTTP Feature Policy specification and guidelines can
+ # be found at MDN:
+ #
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
+ #
+ # Examples of usage:
+ #
+ # # Global policy
+ # Rails.application.config.feature_policy do |f|
+ # f.camera :none
+ # f.gyroscope :none
+ # f.microphone :none
+ # f.usb :none
+ # f.fullscreen :self
+ # f.payment :self, "https://secure.example.com"
+ # end
+ #
+ # # Controller level policy
+ # class PagesController < ApplicationController
+ # feature_policy do |p|
+ # p.geolocation "https://example.com"
+ # end
+ # end
+ module FeaturePolicy
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def feature_policy(**options, &block)
+ before_action(options) do
+ if block_given?
+ policy = request.feature_policy.clone
+ yield policy
+ request.feature_policy = policy
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index efa5de313c..fd22c4fa64 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -77,6 +77,10 @@ module ActionController
end
end
+ def _set_vary_header
+ self.headers["Vary"] = "Accept" if request.should_apply_vary_header?
+ end
+
# Normalize arguments by catching blocks and setting them on :update.
def _normalize_args(action = nil, options = {}, &blk)
options = super
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index 6a07a73d94..920ae52f2b 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -259,6 +259,11 @@ module ActionController
@parameters == other
end
end
+ alias eql? ==
+
+ def hash
+ [@parameters.hash, @permitted].hash
+ end
# Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt>
# representation of the parameters with all unpermitted keys removed.
@@ -744,6 +749,18 @@ module ActionController
end
alias_method :delete_if, :reject!
+ # Returns a new instance of <tt>ActionController::Parameters</tt> without the blank values.
+ # Uses Object#blank? for determining if a value is blank.
+ def compact_blank
+ reject { |_k, v| v.blank? }
+ end
+
+ # Removes all blank values in place and returns self.
+ # Uses Object#blank? for determining if a value is blank.
+ def compact_blank!
+ reject! { |_k, v| v.blank? }
+ end
+
# Returns values that were assigned to the given +keys+. Note that all the
# +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>.
def values_at(*keys)
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 6a4ba9af4a..67d303a368 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -43,6 +43,7 @@ module ActionDispatch
eager_autoload do
autoload_under "http" do
autoload :ContentSecurityPolicy
+ autoload :FeaturePolicy
autoload :Request
autoload :Response
end
diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb
index 5c6fa2dfa7..9c430b57e3 100644
--- a/actionpack/lib/action_dispatch/http/content_security_policy.rb
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -22,8 +22,9 @@ module ActionDispatch #:nodoc:
if policy = request.content_security_policy
nonce = request.content_security_policy_nonce
+ nonce_directives = request.content_security_policy_nonce_directives
context = request.controller_instance || request
- headers[header_name(request)] = policy.build(context, nonce)
+ headers[header_name(request)] = policy.build(context, nonce, nonce_directives)
end
response
@@ -54,6 +55,7 @@ module ActionDispatch #:nodoc:
POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
NONCE = "action_dispatch.content_security_policy_nonce"
+ NONCE_DIRECTIVES = "action_dispatch.content_security_policy_nonce_directives"
def content_security_policy
get_header(POLICY)
@@ -79,6 +81,14 @@ module ActionDispatch #:nodoc:
set_header(NONCE_GENERATOR, generator)
end
+ def content_security_policy_nonce_directives
+ get_header(NONCE_DIRECTIVES)
+ end
+
+ def content_security_policy_nonce_directives=(generator)
+ set_header(NONCE_DIRECTIVES, generator)
+ end
+
def content_security_policy_nonce
if content_security_policy_nonce_generator
if nonce = get_header(NONCE)
@@ -127,13 +137,17 @@ module ActionDispatch #:nodoc:
object_src: "object-src",
prefetch_src: "prefetch-src",
script_src: "script-src",
+ script_src_attr: "script-src-attr",
+ script_src_elem: "script-src-elem",
style_src: "style-src",
+ style_src_attr: "style-src-attr",
+ style_src_elem: "style-src-elem",
worker_src: "worker-src"
}.freeze
- NONCE_DIRECTIVES = %w[script-src style-src].freeze
+ DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
- private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES
+ private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
attr_reader :directives
@@ -202,8 +216,9 @@ module ActionDispatch #:nodoc:
end
end
- def build(context = nil, nonce = nil)
- build_directives(context, nonce).compact.join("; ")
+ def build(context = nil, nonce = nil, nonce_directives = nil)
+ nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil?
+ build_directives(context, nonce, nonce_directives).compact.join("; ")
end
private
@@ -226,10 +241,10 @@ module ActionDispatch #:nodoc:
end
end
- def build_directives(context, nonce)
+ def build_directives(context, nonce, nonce_directives)
@directives.map do |directive, sources|
if sources.is_a?(Array)
- if nonce && nonce_directive?(directive)
+ if nonce && nonce_directive?(directive, nonce_directives)
"#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
else
"#{directive} #{build_directive(sources, context).join(' ')}"
@@ -264,8 +279,8 @@ module ActionDispatch #:nodoc:
end
end
- def nonce_directive?(directive)
- NONCE_DIRECTIVES.include?(directive)
+ def nonce_directive?(directive, nonce_directives)
+ nonce_directives.include?(directive)
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/feature_policy.rb b/actionpack/lib/action_dispatch/http/feature_policy.rb
new file mode 100644
index 0000000000..592b6e4393
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/feature_policy.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+
+module ActionDispatch #:nodoc:
+ class FeaturePolicy
+ class Middleware
+ CONTENT_TYPE = "Content-Type"
+ POLICY = "Feature-Policy"
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new(env)
+ _, headers, _ = response = @app.call(env)
+
+ return response unless html_response?(headers)
+ return response if policy_present?(headers)
+
+ if policy = request.feature_policy
+ headers[POLICY] = policy.build(request.controller_instance)
+ end
+
+ if policy_empty?(policy)
+ headers.delete(POLICY)
+ end
+
+ response
+ end
+
+ private
+ def html_response?(headers)
+ if content_type = headers[CONTENT_TYPE]
+ content_type =~ /html/
+ end
+ end
+
+ def policy_present?(headers)
+ headers[POLICY]
+ end
+
+ def policy_empty?(policy)
+ policy.try(:directives) && policy.directives.empty?
+ end
+ end
+
+ module Request
+ POLICY = "action_dispatch.feature_policy"
+
+ def feature_policy
+ get_header(POLICY)
+ end
+
+ def feature_policy=(policy)
+ set_header(POLICY, policy)
+ end
+ end
+
+ MAPPINGS = {
+ self: "'self'",
+ none: "'none'",
+ }.freeze
+
+ # List of available features can be found at
+ # https://github.com/WICG/feature-policy/blob/master/features.md#policy-controlled-features
+ DIRECTIVES = {
+ accelerometer: "accelerometer",
+ ambient_light_sensor: "ambient-light-sensor",
+ autoplay: "autoplay",
+ camera: "camera",
+ encrypted_media: "encrypted-media",
+ fullscreen: "fullscreen",
+ geolocation: "geolocation",
+ gyroscope: "gyroscope",
+ magnetometer: "magnetometer",
+ microphone: "microphone",
+ midi: "midi",
+ payment: "payment",
+ picture_in_picture: "picture-in-picture",
+ speaker: "speaker",
+ usb: "usb",
+ vibrate: "vibrate",
+ vr: "vr",
+ }.freeze
+
+ private_constant :MAPPINGS, :DIRECTIVES
+
+ attr_reader :directives
+
+ def initialize
+ @directives = {}
+ yield self if block_given?
+ end
+
+ def initialize_copy(other)
+ @directives = other.directives.deep_dup
+ end
+
+ DIRECTIVES.each do |name, directive|
+ define_method(name) do |*sources|
+ if sources.first
+ @directives[directive] = apply_mappings(sources)
+ else
+ @directives.delete(directive)
+ end
+ end
+ end
+
+ def build(context = nil)
+ build_directives(context).compact.join("; ")
+ end
+
+ private
+ def apply_mappings(sources)
+ sources.map do |source|
+ case source
+ when Symbol
+ apply_mapping(source)
+ when String, Proc
+ source
+ else
+ raise ArgumentError, "Invalid HTTP feature policy source: #{source.inspect}"
+ end
+ end
+ end
+
+ def apply_mapping(source)
+ MAPPINGS.fetch(source) do
+ raise ArgumentError, "Unknown HTTP feature policy source mapping: #{source.inspect}"
+ end
+ end
+
+ def build_directives(context)
+ @directives.map do |directive, sources|
+ if sources.is_a?(Array)
+ "#{directive} #{build_directive(sources, context).join(' ')}"
+ elsif sources
+ directive
+ else
+ nil
+ end
+ end
+ end
+
+ def build_directive(sources, context)
+ sources.map { |source| resolve_source(source, context) }
+ end
+
+ def resolve_source(source, context)
+ case source
+ when String
+ source
+ when Symbol
+ source.to_s
+ when Proc
+ if context.nil?
+ raise RuntimeError, "Missing context for the dynamic feature policy source: #{source.inspect}"
+ else
+ context.instance_exec(&source)
+ end
+ else
+ raise RuntimeError, "Unexpected feature policy source: #{source.inspect}"
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index a2cac49082..6bf4e652d3 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -62,13 +62,7 @@ module ActionDispatch
def formats
fetch_header("action_dispatch.request.formats") do |k|
- params_readable = begin
- parameters[:format]
- rescue *RESCUABLE_MIME_FORMAT_ERRORS
- false
- end
-
- v = if params_readable
+ v = if params_readable?
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
@@ -153,12 +147,22 @@ module ActionDispatch
order.include?(Mime::ALL) ? format : nil
end
+ def should_apply_vary_header?
+ !params_readable? && use_accept_header && valid_accept_header
+ end
+
private
BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
+ def params_readable? # :doc:
+ parameters[:format]
+ rescue *RESCUABLE_MIME_FORMAT_ERRORS
+ false
+ end
+
def valid_accept_header # :doc:
(xhr? && (accept.present? || content_mime_type)) ||
- (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
+ (accept.present? && !accept.match?(BROWSER_LIKE_ACCEPTS))
end
def use_accept_header # :doc:
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index 44f23940d3..4ac7c5c2bd 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -23,6 +23,7 @@ module ActionDispatch
include ActionDispatch::Http::FilterParameters
include ActionDispatch::Http::URL
include ActionDispatch::ContentSecurityPolicy::Request
+ include ActionDispatch::FeaturePolicy::Request
include Rack::Request::Env
autoload :Session, "action_dispatch/request/session"
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 3b0f6378ea..225ae0a497 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -133,7 +133,7 @@ module ActionDispatch
end
def named_host?(host)
- IP_HOST_REGEXP !~ host
+ !IP_HOST_REGEXP.match?(host)
end
def normalize_protocol(protocol)
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index 4aee7a6f83..9184676801 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -148,7 +148,7 @@ module ActionDispatch
end
def glob?
- !path.spec.grep(Nodes::Star).empty?
+ path.spec.any?(Nodes::Star)
end
def dispatcher?
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 642f155085..96bdf570af 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -346,28 +346,6 @@ module ActionDispatch
@cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
end
- def handle_options(options) # :nodoc:
- if options[:expires].respond_to?(:from_now)
- options[:expires] = options[:expires].from_now
- end
-
- options[:path] ||= "/"
-
- if options[:domain] == :all || options[:domain] == "all"
- # If there is a provided tld length then we use it otherwise default domain regexp.
- domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
-
- # If host is not ip and matches domain regexp.
- # (ip confirms to domain regexp so we explicitly check for ip)
- options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
- ".#{$&}"
- end
- elsif options[:domain].is_a? Array
- # If host matches one of the supplied domains without a dot in front of it.
- options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
- end
- end
-
# Sets the cookie named +name+. The second argument may be the cookie's
# value or a hash of options as documented above.
def []=(name, options)
@@ -447,6 +425,28 @@ module ActionDispatch
def write_cookie?(cookie)
request.ssl? || !cookie[:secure] || always_write_cookie
end
+
+ def handle_options(options)
+ if options[:expires].respond_to?(:from_now)
+ options[:expires] = options[:expires].from_now
+ end
+
+ options[:path] ||= "/"
+
+ if options[:domain] == :all || options[:domain] == "all"
+ # If there is a provided tld length then we use it otherwise default domain regexp.
+ domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
+
+ # If host is not ip and matches domain regexp.
+ # (ip confirms to domain regexp so we explicitly check for ip)
+ options[:domain] = if !request.host.match?(/^[\d.]+$/) && (request.host =~ domain_regexp)
+ ".#{$&}"
+ end
+ elsif options[:domain].is_a? Array
+ # If host matches one of the supplied domains without a dot in front of it.
+ options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
+ end
+ end
end
class AbstractCookieJar # :nodoc:
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index f8937a2faf..e546d1c11f 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -137,9 +137,7 @@ module ActionDispatch
return unless logger
exception = wrapper.exception
-
- trace = wrapper.application_trace
- trace = wrapper.framework_trace if trace.empty?
+ trace = wrapper.exception_trace
ActiveSupport::Deprecation.silence do
message = []
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 2da0ef9600..e4a2a51c57 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -36,18 +36,23 @@ module ActionDispatch
"ActionView::Template::Error"
]
+ cattr_accessor :silent_exceptions, default: [
+ "ActionController::RoutingError"
+ ]
+
attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
def initialize(backtrace_cleaner, exception)
@backtrace_cleaner = backtrace_cleaner
@exception = exception
+ @exception_class_name = @exception.class.name
@wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
end
def unwrapped_exception
- if wrapper_exceptions.include?(exception.class.to_s)
+ if wrapper_exceptions.include?(@exception_class_name)
exception.cause
else
exception
@@ -55,13 +60,19 @@ module ActionDispatch
end
def rescue_template
- @@rescue_templates[@exception.class.name]
+ @@rescue_templates[@exception_class_name]
end
def status_code
self.class.status_code_for_exception(unwrapped_exception.class.name)
end
+ def exception_trace
+ trace = application_trace
+ trace = framework_trace if trace.empty? && !silent_exceptions.include?(@exception_class_name)
+ trace
+ end
+
def application_trace
clean_backtrace(:silent)
end
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 66f90980b9..2e09aed41d 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -54,11 +54,5 @@ module ActionDispatch
ActionDispatch.test_app = app
end
-
- initializer "action_dispatch.system_tests" do |app|
- ActiveSupport.on_load(:action_dispatch_system_test_case) do
- include app.routes.url_helpers
- end
- end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index d1100089b1..e3090e7ba2 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -112,7 +112,7 @@ module ActionDispatch
end
def self.optional_format?(path, format)
- format != false && path !~ OPTIONAL_FORMAT_REGEX
+ format != false && !path.match?(OPTIONAL_FORMAT_REGEX)
end
def initialize(set:, ast:, controller:, default_action:, to:, formatted:, via:, options_constraints:, anchor:, scope_params:, options:)
@@ -1833,7 +1833,7 @@ module ActionDispatch
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
if as.nil?
- candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
+ candidate unless !candidate.match?(/\A[_a-z]/i) || has_named_route?(candidate)
else
candidate
end
diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb
index a7fb5fa330..aae96975c7 100644
--- a/actionpack/lib/action_dispatch/system_test_case.rb
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
-gem "capybara", ">= 2.15"
+gem "capybara", ">= 3.26"
require "capybara/dsl"
require "capybara/minitest"
+require "selenium/webdriver"
require "action_controller"
require "action_dispatch/system_testing/driver"
require "action_dispatch/system_testing/browser"
@@ -158,12 +159,33 @@ module ActionDispatch
driven_by :selenium
- def url_options # :nodoc:
- default_url_options.merge(host: Capybara.app_host)
- end
+ private
+ def url_helpers
+ @url_helpers ||=
+ if ActionDispatch.test_app
+ Class.new do
+ include ActionDispatch.test_app.routes.url_helpers
- ActiveSupport.run_load_hooks(:action_dispatch_system_test_case, self)
- end
+ def url_options
+ default_url_options.reverse_merge(host: Capybara.app_host || Capybara.current_session.server_url)
+ end
+ end.new
+ end
+ end
- SystemTestCase.start_application
+ def method_missing(name, *args, &block)
+ if url_helpers.respond_to?(name)
+ url_helpers.public_send(name, *args, &block)
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(name, include_private = false)
+ url_helpers.respond_to?(name)
+ end
+ end
end
+
+ActiveSupport.run_load_hooks :action_dispatch_system_test_case, ActionDispatch::SystemTestCase
+ActionDispatch::SystemTestCase.start_application
diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb
index c34907b6cb..e861e52f09 100644
--- a/actionpack/lib/action_dispatch/system_testing/browser.rb
+++ b/actionpack/lib/action_dispatch/system_testing/browser.rb
@@ -39,6 +39,29 @@ module ActionDispatch
end
end
+ # driver_path can be configured as a proc. The webdrivers gem uses this
+ # proc to update web drivers. Running this proc early allows us to only
+ # update the webdriver once and avoid race conditions when using
+ # parallel tests.
+ def preload
+ case type
+ when :chrome
+ if ::Selenium::WebDriver::Service.respond_to? :driver_path=
+ ::Selenium::WebDriver::Chrome::Service.driver_path.try(:call)
+ else
+ # Selenium <= v3.141.0
+ ::Selenium::WebDriver::Chrome.driver_path
+ end
+ when :firefox
+ if ::Selenium::WebDriver::Service.respond_to? :driver_path=
+ ::Selenium::WebDriver::Firefox::Service.driver_path.try(:call)
+ else
+ # Selenium <= v3.141.0
+ ::Selenium::WebDriver::Firefox.driver_path
+ end
+ end
+ end
+
private
def headless_chrome_browser_options
capabilities.args << "--headless"
diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb
index 25a09dd918..15943a55ea 100644
--- a/actionpack/lib/action_dispatch/system_testing/driver.rb
+++ b/actionpack/lib/action_dispatch/system_testing/driver.rb
@@ -9,6 +9,8 @@ module ActionDispatch
@screen_size = options[:screen_size]
@options = options[:options]
@capabilities = capabilities
+
+ @browser.preload
end
def use
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
index 20f6a7634f..30dc21ebb9 100644
--- a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
@@ -4,15 +4,12 @@ module ActionDispatch
module SystemTesting
module TestHelpers
module SetupAndTeardown # :nodoc:
- DEFAULT_HOST = "http://127.0.0.1"
-
def host!(host)
- Capybara.app_host = host
- end
+ ActiveSupport::Deprecation.warn \
+ "ActionDispatch::SystemTestCase#host! is deprecated with no replacement. " \
+ "Set Capybara.app_host directly or rely on Capybara's default host."
- def before_setup
- host! DEFAULT_HOST
- super
+ Capybara.app_host = host
end
def before_teardown
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index bb8b43ad4d..9e7b4301a8 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -3,7 +3,6 @@
require "stringio"
require "uri"
require "active_support/core_ext/kernel/singleton_class"
-require "active_support/core_ext/object/try"
require "rack/test"
require "minitest"
@@ -50,11 +49,16 @@ module ActionDispatch
# Follow a single redirect response. If the last response was not a
# redirect, an exception will be raised. Otherwise, the redirect is
- # performed on the location header. Any arguments are passed to the
- # underlying call to `get`.
+ # performed on the location header. If the redirection is a 307 redirect,
+ # the same HTTP verb will be used when redirecting, otherwise a GET request
+ # will be performed. Any arguments are passed to the
+ # underlying request.
def follow_redirect!(**args)
raise "not a redirect! #{status} #{status_message}" unless redirect?
- get(response.location, **args)
+
+ method = response.status == 307 ? request.method.downcase : :get
+ public_send(method, response.location, **args)
+
status
end
end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index cce229b30d..cb7c2467ac 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -213,6 +213,10 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
redirect_to action_url("get")
end
+ def redirect_307
+ redirect_to action_url("post"), status: 307
+ end
+
def remove_header
response.headers.delete params[:header]
head :ok, "c" => "3"
@@ -337,6 +341,15 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
end
+ def test_307_redirect_uses_the_same_http_verb
+ with_test_route_set do
+ post "/redirect_307"
+ assert_equal 307, status
+ follow_redirect!
+ assert_equal "POST", request.method
+ end
+ end
+
def test_redirect_reset_html_document
with_test_route_set do
get "/redirect"
@@ -530,6 +543,32 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
end
+ def test_setting_vary_header_when_request_is_xhr_with_accept_header
+ with_test_route_set do
+ get "/get", headers: { "Accept" => "application/json" }, xhr: true
+ assert_equal "Accept", response.headers["Vary"]
+ end
+ end
+
+ def test_not_setting_vary_header_when_format_is_provided
+ with_test_route_set do
+ get "/get", params: { format: "json" }
+ assert_nil response.headers["Vary"]
+ end
+ end
+
+ def test_not_setting_vary_header_when_ignore_accept_header_is_set
+ original_ignore_accept_header = ActionDispatch::Request.ignore_accept_header
+ ActionDispatch::Request.ignore_accept_header = true
+
+ with_test_route_set do
+ get "/get", headers: { "Accept" => "application/json" }, xhr: true
+ assert_nil response.headers["Vary"]
+ end
+ ensure
+ ActionDispatch::Request.ignore_accept_header = original_ignore_accept_header
+ end
+
private
def with_default_headers(headers)
original = ActionDispatch::Response.default_headers
diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb
index cb49eeb1b7..3d1538ff64 100644
--- a/actionpack/test/controller/parameters/accessors_test.rb
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -284,12 +284,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
params1 = ActionController::Parameters.new(a: 1, b: 2)
params2 = ActionController::Parameters.new(a: 1, b: 2)
assert(params1 == params2)
+ assert(params1.hash == params2.hash)
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)
+ assert(params1.hash == params2.hash)
end
test "is equal to Parameters instance with same different source params, but same permitted params" do
@@ -297,6 +299,8 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
params2 = ActionController::Parameters.new(a: 1, c: 3).permit(:a)
assert(params1 == params2)
assert(params2 == params1)
+ assert(params1.hash == params2.hash)
+ assert(params2.hash == params1.hash)
end
test "is not equal to an unpermitted Parameters instance with same params" do
@@ -304,6 +308,8 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
params2 = ActionController::Parameters.new(a: 1)
assert(params1 != params2)
assert(params2 != params1)
+ assert(params1.hash != params2.hash)
+ assert(params2.hash != params1.hash)
end
test "is not equal to Parameters instance with different permitted params" do
@@ -311,6 +317,8 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
assert(params1 != params2)
assert(params2 != params1)
+ assert(params1.hash != params2.hash)
+ assert(params2.hash != params1.hash)
end
test "equality with simple types works" do
diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb
index 31ee7964f5..ffffd2996f 100644
--- a/actionpack/test/controller/parameters/mutators_test.rb
+++ b/actionpack/test/controller/parameters/mutators_test.rb
@@ -127,4 +127,22 @@ class ParametersMutatorsTest < ActiveSupport::TestCase
test "deep_transform_keys! retains unpermitted status" do
assert_not_predicate @params.deep_transform_keys! { |k| k }, :permitted?
end
+
+ test "compact_blank retains permitted status" do
+ @params.permit!
+ assert_predicate @params.compact_blank, :permitted?
+ end
+
+ test "compact_blank retains unpermitted status" do
+ assert_not_predicate @params.compact_blank, :permitted?
+ end
+
+ test "compact_blank! retains permitted status" do
+ @params.permit!
+ assert_predicate @params.compact_blank!, :permitted?
+ end
+
+ test "compact_blank! retains unpermitted status" do
+ assert_not_predicate @params.compact_blank!, :permitted?
+ end
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index a2a6c69dd3..dd76824413 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -4,6 +4,11 @@ require "abstract_unit"
require "controller/fake_models"
class TestControllerWithExtraEtags < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "test/with_implicit_template.erb" => "Hello explicitly!",
+ "test/hello_world.erb" => "Hello world!"
+ )]
+
def self.controller_name; "test"; end
def self.controller_path; "test"; end
@@ -37,6 +42,11 @@ class TestControllerWithExtraEtags < ActionController::Base
end
class ImplicitRenderTestController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "implicit_render_test/hello_world.erb" => "Hello world!",
+ "implicit_render_test/empty_action_with_template.html.erb" => "<h1>Empty action rendered this implicitly.</h1>\n"
+ )]
+
def empty_action
end
@@ -46,6 +56,10 @@ end
module Namespaced
class ImplicitRenderTestController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "namespaced/implicit_render_test/hello_world.erb" => "Hello world!"
+ )]
+
def hello_world
fresh_when(etag: "abc")
end
@@ -293,13 +307,15 @@ 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!")
+ hash = @controller.view_paths.first.instance_variable_get(:@hash)
+ key = name + ".erb"
+ original = hash[key]
+ hash[key] = "#{original} Modified!"
ActionView::LookupContext::DetailsKey.clear
yield
ensure
- File.write(path, original)
+ hash[key] = original
+ ActionView::LookupContext::DetailsKey.clear
end
end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 1e42a3a90d..339025ec52 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -1,7 +1,6 @@
# 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"
@@ -1249,7 +1248,7 @@ class ResourcesTest < ActionController::TestCase
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]
+ shallow_prefix = options[:shallow] ? options[:namespace]&.gsub(/\//, "_") : options[:name_prefix]
new_action = "new"
edit_action = "edit"
diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb
index 30c340ae9e..3d60dc1661 100644
--- a/actionpack/test/dispatch/content_security_policy_test.rb
+++ b/actionpack/test/dispatch/content_security_policy_test.rb
@@ -128,12 +128,36 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
@policy.script_src false
assert_no_match %r{script-src}, @policy.build
+ @policy.script_src_attr :self
+ assert_match %r{script-src-attr 'self'}, @policy.build
+
+ @policy.script_src_attr false
+ assert_no_match %r{script-src-attr}, @policy.build
+
+ @policy.script_src_elem :self
+ assert_match %r{script-src-elem 'self'}, @policy.build
+
+ @policy.script_src_elem false
+ assert_no_match %r{script-src-elem}, @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.style_src_attr :self
+ assert_match %r{style-src-attr 'self'}, @policy.build
+
+ @policy.style_src_attr false
+ assert_no_match %r{style-src-attr}, @policy.build
+
+ @policy.style_src_elem :self
+ assert_match %r{style-src-elem 'self'}, @policy.build
+
+ @policy.style_src_elem false
+ assert_no_match %r{style-src-elem}, @policy.build
+
@policy.worker_src :self
assert_match %r{worker-src 'self'}, @policy.build
@@ -542,3 +566,57 @@ class DisabledContentSecurityPolicyIntegrationTest < ActionDispatch::Integration
assert_equal "default-src https://example.com", response.headers["Content-Security-Policy"]
end
end
+
+class NonceDirectiveContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ def index
+ head :ok
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "nonce_directive_content_security_policy_integration_test" do
+ get "/", to: "policy#index"
+ end
+ end
+
+ POLICY = ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src -> { :self }
+ p.script_src -> { :https }
+ p.style_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.content_security_policy_nonce_directives"] = %w(script-src)
+ 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_generate_nonce_only_specified_in_nonce_directives
+ get "/"
+
+ assert_response :success
+ assert_match "script-src https: 'nonce-iyhD0Yc0W+c='", response.headers["Content-Security-Policy"]
+ assert_no_match "style-src https: 'nonce-iyhD0Yc0W+c='", response.headers["Content-Security-Policy"]
+ end
+end
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index 68817ccdea..5d12b62fd4 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -466,6 +466,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
end
test "logs exception backtrace when all lines silenced" do
+ @app = DevelopmentApp
+
output = StringIO.new
backtrace_cleaner = ActiveSupport::BacktraceCleaner.new
backtrace_cleaner.add_silencer { true }
@@ -478,6 +480,27 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_operator((output.rewind && output.read).lines.count, :>, 10)
end
+ test "doesn't log the framework backtrace when error type is a routing error" do
+ @app = ProductionApp
+
+ 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 }
+
+ assert_raises ActionController::RoutingError do
+ get "/pass", headers: env
+ end
+
+ log = output.rewind && output.read
+
+ assert_includes log, "ActionController::RoutingError (No route matches [GET] \"/pass\")"
+ assert_equal 3, log.lines.count
+ end
+
test "display backtrace when error type is SyntaxError" do
@app = DevelopmentApp
@@ -521,8 +544,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
@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} }
+ bc.add_silencer { |line| line.match?(/method_that_raises/) }
+ bc.add_silencer { |line| !line.match?(%r{test/dispatch/debug_exceptions_test.rb}) }
end
get "/framework_raises", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
@@ -573,7 +596,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
@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} }
+ bc.add_silencer { |line| !line.match?(%r{test/dispatch/debug_exceptions_test.rb}) }
end
get "/nested_exceptions", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
@@ -608,7 +631,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
@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} }
+ bc.add_silencer { |line| !line.match?(%r{test/dispatch/debug_exceptions_test.rb}) }
end
get "/actionable_error", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb
index 668469a01d..3c395ca5ee 100644
--- a/actionpack/test/dispatch/exception_wrapper_test.rb
+++ b/actionpack/test/dispatch/exception_wrapper_test.rb
@@ -21,7 +21,7 @@ module ActionDispatch
setup do
@cleaner = ActiveSupport::BacktraceCleaner.new
@cleaner.remove_filters!
- @cleaner.add_silencer { |line| line !~ /^lib/ }
+ @cleaner.add_silencer { |line| !line.match?(/^lib/) }
end
test "#source_extracts fetches source fragments for every backtrace entry" do
diff --git a/actionpack/test/dispatch/feature_policy_test.rb b/actionpack/test/dispatch/feature_policy_test.rb
new file mode 100644
index 0000000000..ebcc8a8b6d
--- /dev/null
+++ b/actionpack/test/dispatch/feature_policy_test.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class FeaturePolicyTest < ActiveSupport::TestCase
+ def setup
+ @policy = ActionDispatch::FeaturePolicy.new
+ end
+
+ def test_mappings
+ @policy.midi :self
+ assert_equal "midi 'self'", @policy.build
+
+ @policy.midi :none
+ assert_equal "midi 'none'", @policy.build
+ end
+
+ def test_multiple_sources_for_a_single_directive
+ @policy.geolocation :self, "https://example.com"
+ assert_equal "geolocation 'self' https://example.com", @policy.build
+ end
+
+ def test_single_directive_for_multiple_directives
+ @policy.geolocation :self
+ @policy.usb :none
+ assert_equal "geolocation 'self'; usb 'none'", @policy.build
+ end
+
+ def test_multiple_directives_for_multiple_directives
+ @policy.geolocation :self, "https://example.com"
+ @policy.usb :none, "https://example.com"
+ assert_equal "geolocation 'self' https://example.com; usb 'none' https://example.com", @policy.build
+ end
+
+ def test_invalid_directive_source
+ exception = assert_raises(ArgumentError) do
+ @policy.vr [:non_existent]
+ end
+
+ assert_equal "Invalid HTTP feature policy source: [:non_existent]", exception.message
+ end
+end
+
+class FeaturePolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ feature_policy only: :index do |f|
+ f.gyroscope :none
+ end
+
+ feature_policy only: :sample_controller do |f|
+ f.gyroscope nil
+ f.usb :self
+ end
+
+ feature_policy only: :multiple_directives do |f|
+ f.gyroscope nil
+ f.usb :self
+ f.autoplay "https://example.com"
+ f.payment "https://secure.example.com"
+ end
+
+ def index
+ head :ok
+ end
+
+ def sample_controller
+ head :ok
+ end
+
+ def multiple_directives
+ head :ok
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "feature_policy_integration_test" do
+ get "/", to: "policy#index"
+ get "/sample_controller", to: "policy#sample_controller"
+ get "/multiple_directives", to: "policy#multiple_directives"
+ end
+ end
+
+ POLICY = ActionDispatch::FeaturePolicy.new do |p|
+ p.gyroscope :self
+ end
+
+ class PolicyConfigMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.feature_policy"] = POLICY
+ env["action_dispatch.show_exceptions"] = false
+
+ @app.call(env)
+ end
+ end
+
+ APP = build_app(ROUTES) do |middleware|
+ middleware.use PolicyConfigMiddleware
+ middleware.use ActionDispatch::FeaturePolicy::Middleware
+ end
+
+ def app
+ APP
+ end
+
+ def test_generates_feature_policy_header
+ get "/"
+ assert_policy "gyroscope 'none'"
+ end
+
+ def test_generates_per_controller_feature_policy_header
+ get "/sample_controller"
+ assert_policy "usb 'self'"
+ end
+
+ def test_generates_multiple_directives_feature_policy_header
+ get "/multiple_directives"
+ assert_policy "usb 'self'; autoplay https://example.com; payment https://secure.example.com"
+ end
+
+ private
+ def env_config
+ Rails.application.env_config
+ end
+
+ def feature_policy
+ env_config["action_dispatch.feature_policy"]
+ end
+
+ def feature_policy=(policy)
+ env_config["action_dispatch.feature_policy"] = policy
+ end
+
+ def assert_policy(expected)
+ assert_response :success
+ assert_equal expected, response.headers["Feature-Policy"]
+ end
+end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index 50f6c06fee..da417a1604 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -167,12 +167,12 @@ class MimeTypeTest < ActiveSupport::TestCase
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"
+ assert_match Mime[:js], "text/javascript"
+ assert_match Mime[:js], "application/javascript"
+ assert_no_match Mime[:js], "text/html"
+ assert_match Mime[:js], "text/javascript"
+ assert_match Mime[:js], "application/javascript"
+ assert_match Mime[:html], "application/xhtml+xml"
end
test "can be initialized with wildcards" do
diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb
index 63c147cb1b..f20043b9ac 100644
--- a/actionpack/test/dispatch/prefix_generation_test.rb
+++ b/actionpack/test/dispatch/prefix_generation_test.rb
@@ -304,7 +304,7 @@ module TestGenerationPrefix
assert_equal "/omg/blog/posts/1", path
end
- test "[OBJECT] generating engine's route with named helpers" do
+ test "[OBJECT] generating engine's route with named route helpers" do
path = engine_object.posts_path
assert_equal "/awesome/blog/posts", path
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index 0ec8dd25e0..806b7d0559 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -865,12 +865,28 @@ class RequestFormat < BaseRequestTest
assert_not_predicate request.format, :json?
end
- test "format does not throw exceptions when malformed parameters" do
+ test "format does not throw exceptions when malformed GET parameters" do
request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
assert request.formats
assert_predicate request.format, :html?
end
+ test "format does not throw exceptions when invalid POST parameters" do
+ body = "{record:{content:127.0.0.1}}"
+ request = stub_request(
+ "REQUEST_METHOD" => "POST",
+ "CONTENT_LENGTH" => body.length,
+ "CONTENT_TYPE" => "application/json",
+ "rack.input" => StringIO.new(body),
+ "action_dispatch.logger" => Logger.new(output = StringIO.new)
+ )
+ assert request.formats
+ assert request.format.html?
+
+ output.rewind && (err = output.read)
+ assert_match(/Error occurred while parsing request parameters/, err)
+ end
+
test "formats with xhr request" do
request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
"QUERY_STRING" => ""
diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb
index 7ef306d04b..d3b16d0328 100644
--- a/actionpack/test/dispatch/system_testing/driver_test.rb
+++ b/actionpack/test/dispatch/system_testing/driver_test.rb
@@ -120,4 +120,17 @@ class DriverTest < ActiveSupport::TestCase
driver.use
end
end
+
+ test "preloads browser's driver_path" do
+ called = false
+
+ original_driver_path = ::Selenium::WebDriver::Chrome::Service.driver_path
+ ::Selenium::WebDriver::Chrome::Service.driver_path = -> { called = true }
+
+ ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :chrome)
+
+ assert called
+ ensure
+ ::Selenium::WebDriver::Chrome::Service.driver_path = original_driver_path
+ 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
index 3319db1665..d235f7ad89 100644
--- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb
+++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
@@ -36,12 +36,10 @@ class SetDriverToSeleniumHeadlessFirefoxTest < DrivenBySeleniumWithHeadlessFiref
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_deprecated do
+ host! "http://example.com"
+ end
assert_equal "http://example.com", Capybara.app_host
end