aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack')
-rw-r--r--actionpack/CHANGELOG.md104
-rw-r--r--actionpack/README.rdoc6
-rw-r--r--actionpack/lib/action_controller.rb1
-rw-r--r--actionpack/lib/action_controller/base.rb1
-rw-r--r--actionpack/lib/action_controller/metal/content_security_policy.rb26
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb10
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb2
-rw-r--r--actionpack/lib/action_dispatch.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb231
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb13
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb1
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb4
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb10
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb8
-rw-r--r--actionpack/lib/action_dispatch/system_testing/driver.rb13
-rw-r--r--actionpack/lib/action_dispatch/system_testing/server.rb2
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb16
-rw-r--r--actionpack/lib/action_pack/gem_version.rb2
-rw-r--r--actionpack/test/abstract_unit.rb4
-rw-r--r--actionpack/test/controller/metal_test.rb2
-rw-r--r--actionpack/test/controller/parameters/accessors_test.rb16
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb13
-rw-r--r--actionpack/test/controller/routing_test.rb2
-rw-r--r--actionpack/test/controller/send_file_test.rb2
-rw-r--r--actionpack/test/dispatch/content_security_policy_test.rb368
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb10
-rw-r--r--actionpack/test/dispatch/response_test.rb8
-rw-r--r--actionpack/test/dispatch/routing_test.rb37
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb125
-rw-r--r--actionpack/test/dispatch/system_testing/driver_test.rb8
-rw-r--r--actionpack/test/dispatch/system_testing/screenshot_helper_test.rb7
-rw-r--r--actionpack/test/dispatch/system_testing/server_test.rb19
-rw-r--r--actionpack/test/dispatch/system_testing/system_test_case_test.rb6
33 files changed, 1006 insertions, 72 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index e01f88e902..384546d7b4 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,107 @@
+* Changed the system tests to set Puma as default server only when the
+ user haven't specified manually another server.
+
+ *Guillermo Iguaran*
+
+* Add secure `X-Download-Options` and `X-Permitted-Cross-Domain-Policies` to
+ default headers set.
+
+ *Guillermo Iguaran*
+
+* Add headless firefox support to System Tests.
+
+ *bogdanvlviv*
+
+* Changed the default system test screenshot output from `inline` to `simple`.
+
+ `inline` works well for iTerm2 but not everyone uses iTerm2. Some terminals like
+ Terminal.app ignore the `inline` and output the path to the file since it can't
+ render the image. Other terminals, like those on Ubuntu, cannot handle the image
+ inline, but also don't handle it gracefully and instead of outputting the file
+ path, it dumps binary into the terminal.
+
+ Commit 9d6e28 fixes this by changing the default for screenshot to be `simple`.
+
+ *Eileen M. Uchitelle*
+
+* Register most popular audio/video/font mime types supported by modern browsers.
+
+ *Guillermo Iguaran*
+
+* Fix optimized url helpers when using relative url root
+
+ Fixes #31220.
+
+ *Andrew White*
+
+
+## Rails 5.2.0.beta2 (November 28, 2017) ##
+
+* No changes.
+
+
+## Rails 5.2.0.beta1 (November 27, 2017) ##
+
+* Add DSL for configuring Content-Security-Policy header
+
+ The DSL allows you to configure a global Content-Security-Policy
+ header and then override within a controller. For more information
+ about the Content-Security-Policy header see MDN:
+
+ https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+
+ Example global policy:
+
+ # config/initializers/content_security_policy.rb
+ Rails.application.config.content_security_policy do |p|
+ p.default_src :self, :https
+ p.font_src :self, :https, :data
+ p.img_src :self, :https, :data
+ p.object_src :none
+ p.script_src :self, :https
+ p.style_src :self, :https, :unsafe_inline
+ end
+
+ Example controller overrides:
+
+ # Override policy inline
+ class PostsController < ApplicationController
+ content_security_policy do |p|
+ p.upgrade_insecure_requests true
+ end
+ end
+
+ # Using literal values
+ class PostsController < ApplicationController
+ content_security_policy do |p|
+ p.base_uri "https://www.example.com"
+ end
+ end
+
+ # Using mixed static and dynamic values
+ class PostsController < ApplicationController
+ content_security_policy do |p|
+ p.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
+ end
+ end
+
+ Allows you to also only report content violations for migrating
+ legacy content using the `content_security_policy_report_only`
+ configuration attribute, e.g;
+
+ # config/initializers/content_security_policy.rb
+ Rails.application.config.content_security_policy_report_only = true
+
+ # controller override
+ class PostsController < ApplicationController
+ self.content_security_policy_report_only = true
+ end
+
+ Note that this feature does not validate the header for performance
+ reasons since the header is calculated at runtime.
+
+ *Andrew White*
+
* Make `assert_recognizes` to traverse mounted engines
*Yuichiro Kaneko*
diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc
index 93b2a0932a..f56230ffa0 100644
--- a/actionpack/README.rdoc
+++ b/actionpack/README.rdoc
@@ -30,7 +30,7 @@ The latest version of Action Pack can be installed with RubyGems:
$ gem install actionpack
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/actionpack
@@ -44,11 +44,11 @@ Action Pack is released under the MIT license:
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index bd19b8cd5d..f43784f9f2 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -22,6 +22,7 @@ module ActionController
autoload_under "metal" do
autoload :ConditionalGet
+ autoload :ContentSecurityPolicy
autoload :Cookies
autoload :DataStreaming
autoload :EtagWithTemplateDigest
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index b73269871b..204a3d400c 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -225,6 +225,7 @@ module ActionController
Flash,
FormBuilder,
RequestForgeryProtection,
+ ContentSecurityPolicy,
ForceSSL,
Streaming,
DataStreaming,
diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb
new file mode 100644
index 0000000000..48a7109bea
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/content_security_policy.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ActionController #:nodoc:
+ module ContentSecurityPolicy
+ # TODO: Documentation
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def content_security_policy(**options, &block)
+ before_action(options) do
+ if block_given?
+ policy = request.content_security_policy.clone
+ yield policy
+ request.content_security_policy = policy
+ end
+ end
+ end
+
+ def content_security_policy_report_only(report_only = true, **options)
+ before_action(options) do
+ request.content_security_policy_report_only = report_only
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 04fadc90e2..767eddb361 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -415,11 +415,21 @@ module ActionController #:nodoc:
allow_forgery_protection
end
+ NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc
+ The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
+ means you have the 'no-referrer' Referrer-Policy header enabled, or that you the request came from a site that
+ refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
+ best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin.
+ If you cannot change the referrer policy, you can disable origin checking with the
+ Rails.application.config.action_controller.forgery_protection_origin_check setting.
+ MSG
+
# Checks if the request originated from the same origin by looking at the
# Origin header.
def valid_request_origin? # :doc:
if forgery_protection_origin_check
# We accept blank origin headers because some user agents don't send it.
+ raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
request.origin.nil? || request.origin == request.base_url
else
true
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index ef7c4c4c16..a56ac749f8 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -335,7 +335,7 @@ module ActionController
# the same way as <tt>Hash#each_pair</tt>.
def each_pair(&block)
@parameters.each_pair do |key, value|
- yield key, convert_hashes_to_parameters(key, value)
+ yield [key, convert_hashes_to_parameters(key, value)]
end
end
alias_method :each, :each_pair
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 34937f3229..6fed911d0a 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -42,6 +42,7 @@ module ActionDispatch
eager_autoload do
autoload_under "http" do
+ autoload :ContentSecurityPolicy
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
new file mode 100644
index 0000000000..4883e23d24
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -0,0 +1,231 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+
+module ActionDispatch #:nodoc:
+ class ContentSecurityPolicy
+ class Middleware
+ CONTENT_TYPE = "Content-Type".freeze
+ POLICY = "Content-Security-Policy".freeze
+ POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
+
+ 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.content_security_policy
+ headers[header_name(request)] = policy.build(request.controller_instance)
+ end
+
+ response
+ end
+
+ private
+
+ def html_response?(headers)
+ if content_type = headers[CONTENT_TYPE]
+ content_type =~ /html/
+ end
+ end
+
+ def header_name(request)
+ if request.content_security_policy_report_only
+ POLICY_REPORT_ONLY
+ else
+ POLICY
+ end
+ end
+
+ def policy_present?(headers)
+ headers[POLICY] || headers[POLICY_REPORT_ONLY]
+ end
+ end
+
+ module Request
+ POLICY = "action_dispatch.content_security_policy".freeze
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze
+
+ def content_security_policy
+ get_header(POLICY)
+ end
+
+ def content_security_policy=(policy)
+ set_header(POLICY, policy)
+ end
+
+ def content_security_policy_report_only
+ get_header(POLICY_REPORT_ONLY)
+ end
+
+ def content_security_policy_report_only=(value)
+ set_header(POLICY_REPORT_ONLY, value)
+ end
+ end
+
+ MAPPINGS = {
+ self: "'self'",
+ unsafe_eval: "'unsafe-eval'",
+ unsafe_inline: "'unsafe-inline'",
+ none: "'none'",
+ http: "http:",
+ https: "https:",
+ data: "data:",
+ mediastream: "mediastream:",
+ blob: "blob:",
+ filesystem: "filesystem:",
+ report_sample: "'report-sample'",
+ strict_dynamic: "'strict-dynamic'"
+ }.freeze
+
+ DIRECTIVES = {
+ base_uri: "base-uri",
+ child_src: "child-src",
+ connect_src: "connect-src",
+ default_src: "default-src",
+ font_src: "font-src",
+ form_action: "form-action",
+ frame_ancestors: "frame-ancestors",
+ frame_src: "frame-src",
+ img_src: "img-src",
+ manifest_src: "manifest-src",
+ media_src: "media-src",
+ object_src: "object-src",
+ script_src: "script-src",
+ style_src: "style-src",
+ worker_src: "worker-src"
+ }.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 block_all_mixed_content(enabled = true)
+ if enabled
+ @directives["block-all-mixed-content"] = true
+ else
+ @directives.delete("block-all-mixed-content")
+ end
+ end
+
+ def plugin_types(*types)
+ if types.first
+ @directives["plugin-types"] = types
+ else
+ @directives.delete("plugin-types")
+ end
+ end
+
+ def report_uri(uri)
+ @directives["report-uri"] = [uri]
+ end
+
+ def require_sri_for(*types)
+ if types.first
+ @directives["require-sri-for"] = types
+ else
+ @directives.delete("require-sri-for")
+ end
+ end
+
+ def sandbox(*values)
+ if values.empty?
+ @directives["sandbox"] = true
+ elsif values.first
+ @directives["sandbox"] = values
+ else
+ @directives.delete("sandbox")
+ end
+ end
+
+ def upgrade_insecure_requests(enabled = true)
+ if enabled
+ @directives["upgrade-insecure-requests"] = true
+ else
+ @directives.delete("upgrade-insecure-requests")
+ 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 content security policy source: #{source.inspect}"
+ end
+ end
+ end
+
+ def apply_mapping(source)
+ MAPPINGS.fetch(source) do
+ raise ArgumentError, "Unknown content security 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 content security policy source: #{source.inspect}"
+ else
+ context.instance_exec(&source)
+ end
+ else
+ raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
index f8e6fca36d..342e6de312 100644
--- a/actionpack/lib/action_dispatch/http/mime_types.rb
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -10,6 +10,7 @@ Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
Mime::Type.register "text/vcard", :vcf
+Mime::Type.register "text/vtt", :vtt, %w(vtt)
Mime::Type.register "image/png", :png, [], %w(png)
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
@@ -20,6 +21,18 @@ Mime::Type.register "image/svg+xml", :svg
Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)
+Mime::Type.register "audio/mpeg", :mp3, [], %w(mp1 mp2 mp3)
+Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus)
+Mime::Type.register "audio/aac", :m4a, %w( audio/mp4 ), %w(m4a mpg4 aac)
+
+Mime::Type.register "video/webm", :webm, [], %w(webm)
+Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v)
+
+Mime::Type.register "font/otf", :otf, [], %w(otf)
+Mime::Type.register "font/ttf", :ttf, [], %w(ttf)
+Mime::Type.register "font/woff", :woff, [], %w(woff)
+Mime::Type.register "font/woff2", :woff2, [], %w(woff2)
+
Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
Mime::Type.register "application/rss+xml", :rss
Mime::Type.register "application/atom+xml", :atom
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index d631281e4b..3838b84a7a 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -22,6 +22,7 @@ module ActionDispatch
include ActionDispatch::Http::Parameters
include ActionDispatch::Http::FilterParameters
include ActionDispatch::Http::URL
+ include ActionDispatch::ContentSecurityPolicy::Request
include Rack::Request::Env
autoload :Session, "action_dispatch/request/session"
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 855f2ffa47..95e99987a0 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -26,7 +26,9 @@ module ActionDispatch
config.action_dispatch.default_headers = {
"X-Frame-Options" => "SAMEORIGIN",
"X-XSS-Protection" => "1; mode=block",
- "X-Content-Type-Options" => "nosniff"
+ "X-Content-Type-Options" => "nosniff",
+ "X-Download-Options" => "noopen",
+ "X-Permitted-Cross-Domain-Policies" => "none"
}
config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 987e709f6f..9eff30fa53 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -199,6 +199,16 @@ module ActionDispatch
if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
options = t.url_options.merge @options
options[:path] = optimized_helper(args)
+
+ original_script_name = options.delete(:original_script_name)
+ script_name = t._routes.find_script_name(options)
+
+ if original_script_name
+ script_name = original_script_name + script_name
+ end
+
+ options[:script_name] = script_name
+
url_strategy.call options
else
super
diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb
index 7246e01cff..99d0c06751 100644
--- a/actionpack/lib/action_dispatch/system_test_case.rb
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -121,11 +121,15 @@ module ActionDispatch
#
# driven_by :poltergeist
#
- # driven_by :selenium, using: :firefox
+ # driven_by :selenium, screen_size: [800, 800]
+ #
+ # driven_by :selenium, using: :chrome
#
# driven_by :selenium, using: :headless_chrome
#
- # driven_by :selenium, screen_size: [800, 800]
+ # driven_by :selenium, using: :firefox
+ #
+ # driven_by :selenium, using: :headless_firefox
def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {})
self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options)
end
diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb
index 2687772b4b..280989a146 100644
--- a/actionpack/lib/action_dispatch/system_testing/driver.rb
+++ b/actionpack/lib/action_dispatch/system_testing/driver.rb
@@ -38,13 +38,24 @@ module ActionDispatch
browser_options.args << "--disable-gpu"
@options.merge(options: browser_options)
+ elsif @browser == :headless_firefox
+ browser_options = Selenium::WebDriver::Firefox::Options.new
+ browser_options.args << "-headless"
+
+ @options.merge(options: browser_options)
else
@options
end
end
def browser
- @browser == :headless_chrome ? :chrome : @browser
+ if @browser == :headless_chrome
+ :chrome
+ elsif @browser == :headless_firefox
+ :firefox
+ else
+ @browser
+ end
end
def register_selenium(app)
diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb
index 8f1b6725b1..4fc1f33767 100644
--- a/actionpack/lib/action_dispatch/system_testing/server.rb
+++ b/actionpack/lib/action_dispatch/system_testing/server.rb
@@ -20,7 +20,7 @@ module ActionDispatch
end
def set_server
- Capybara.server = :puma, { Silent: self.class.silence_puma }
+ Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default]
end
def set_port
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
index 6c337cdc31..df0c5d3f0e 100644
--- a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
@@ -15,12 +15,11 @@ module ActionDispatch
#
# You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT+ environment variable to
# control the output. Possible values are:
- # * [+inline+ (default)] display the screenshot in the terminal using the
+ # * [+simple+ (default)] Only displays the screenshot path.
+ # This is the default value.
+ # * [+inline+] Display the screenshot in the terminal using the
# iTerm image protocol (https://iterm2.com/documentation-images.html).
- # * [+simple+] only display the screenshot path.
- # This is the default value if the +CI+ environment variables
- # is defined.
- # * [+artifact+] display the screenshot in the terminal, using the terminal
+ # * [+artifact+] Display the screenshot in the terminal, using the terminal
# artifact format (https://buildkite.github.io/terminal/inline-images/).
def take_screenshot
save_image
@@ -59,11 +58,8 @@ module ActionDispatch
# Environment variables have priority
output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] || ENV["CAPYBARA_INLINE_SCREENSHOT"]
- # If running in a CI environment, default to simple
- output_type ||= "simple" if ENV["CI"]
-
- # Default
- output_type ||= "inline"
+ # Default to outputting a path to the screenshot
+ output_type ||= "simple"
output_type
end
diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb
index 28bc153f4d..97f4934b58 100644
--- a/actionpack/lib/action_pack/gem_version.rb
+++ b/actionpack/lib/action_pack/gem_version.rb
@@ -10,7 +10,7 @@ module ActionPack
MAJOR = 5
MINOR = 2
TINY = 0
- PRE = "alpha"
+ PRE = "beta2"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 5262e85a28..55ad9c245e 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -453,3 +453,7 @@ end
class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome
end
+
+class DrivenBySeleniumWithHeadlessFirefox < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :headless_firefox
+end
diff --git a/actionpack/test/controller/metal_test.rb b/actionpack/test/controller/metal_test.rb
index c235c9df86..c3ebcb22b8 100644
--- a/actionpack/test/controller/metal_test.rb
+++ b/actionpack/test/controller/metal_test.rb
@@ -9,7 +9,7 @@ class MetalControllerInstanceTests < ActiveSupport::TestCase
end
end
- def test_response_has_default_headers
+ def test_response_does_not_have_default_headers
original_default_headers = ActionDispatch::Response.default_headers
ActionDispatch::Response.default_headers = {
diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb
index 43cabae7d2..154430d4b0 100644
--- a/actionpack/test/controller/parameters/accessors_test.rb
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -51,6 +51,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
@params.each { |key, value| assert_not(value.permitted?) if key == "person" }
end
+ test "each returns key,value array for block with arity 1" do
+ @params.each do |arg|
+ assert_kind_of Array, arg
+ assert_equal "person", arg[0]
+ assert_kind_of ActionController::Parameters, arg[1]
+ end
+ end
+
test "each_pair carries permitted status" do
@params.permit!
@params.each_pair { |key, value| assert(value.permitted?) if key == "person" }
@@ -60,6 +68,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
@params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" }
end
+ test "each_pair returns key,value array for block with arity 1" do
+ @params.each_pair do |arg|
+ assert_kind_of Array, arg
+ assert_equal "person", arg[0]
+ assert_kind_of ActionController::Parameters, arg[1]
+ end
+ end
+
test "empty? returns true when params contains no key/value pairs" do
params = ActionController::Parameters.new
assert params.empty?
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index eb3d2f34a8..4822d85bcb 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -446,6 +446,19 @@ module RequestForgeryProtectionTests
end
end
+ def test_should_raise_for_post_with_null_origin
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ exception = assert_raises(ActionController::InvalidAuthenticityToken) do
+ @request.set_header "HTTP_ORIGIN", "null"
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ assert_match "The browser returned a 'null' origin for a request", exception.message
+ end
+ end
+ end
+
def test_should_block_post_with_origin_checking_and_wrong_origin
old_logger = ActionController::Base.logger
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index f09051b306..71b01c36a7 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -213,7 +213,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
assert_equal expected, ActiveSupport::JSON.decode(get(u))
end
- def test_regexp_precidence
+ def test_regexp_precedence
rs.draw do
get "/whois/:domain", constraints: {
domain: /\w+\.[\w\.]+/ },
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index fd2399e433..7b1a52b277 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -178,7 +178,7 @@ class SendFileTest < ActionController::TestCase
"image.jpg" => "image/jpeg",
"image.tif" => "image/tiff",
"image.gif" => "image/gif",
- "movie.mpg" => "video/mpeg",
+ "movie.mp4" => "video/mp4",
"file.zip" => "application/zip",
"file.unk" => "application/octet-stream",
"zip" => "application/octet-stream"
diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb
new file mode 100644
index 0000000000..7c4a65a633
--- /dev/null
+++ b/actionpack/test/dispatch/content_security_policy_test.rb
@@ -0,0 +1,368 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ContentSecurityPolicyTest < ActiveSupport::TestCase
+ def setup
+ @policy = ActionDispatch::ContentSecurityPolicy.new
+ end
+
+ def test_build
+ assert_equal ";", @policy.build
+
+ @policy.script_src :self
+ assert_equal "script-src 'self';", @policy.build
+ end
+
+ def test_dup
+ @policy.img_src :self
+ @policy.block_all_mixed_content
+ @policy.upgrade_insecure_requests
+ @policy.sandbox
+ copied = @policy.dup
+ assert_equal copied.build, @policy.build
+ end
+
+ def test_mappings
+ @policy.script_src :data
+ assert_equal "script-src data:;", @policy.build
+
+ @policy.script_src :mediastream
+ assert_equal "script-src mediastream:;", @policy.build
+
+ @policy.script_src :blob
+ assert_equal "script-src blob:;", @policy.build
+
+ @policy.script_src :filesystem
+ assert_equal "script-src filesystem:;", @policy.build
+
+ @policy.script_src :self
+ assert_equal "script-src 'self';", @policy.build
+
+ @policy.script_src :unsafe_inline
+ assert_equal "script-src 'unsafe-inline';", @policy.build
+
+ @policy.script_src :unsafe_eval
+ assert_equal "script-src 'unsafe-eval';", @policy.build
+
+ @policy.script_src :none
+ assert_equal "script-src 'none';", @policy.build
+
+ @policy.script_src :strict_dynamic
+ assert_equal "script-src 'strict-dynamic';", @policy.build
+
+ @policy.script_src :none, :report_sample
+ assert_equal "script-src 'none' 'report-sample';", @policy.build
+ end
+
+ def test_fetch_directives
+ @policy.child_src :self
+ assert_match %r{child-src 'self'}, @policy.build
+
+ @policy.child_src false
+ assert_no_match %r{child-src}, @policy.build
+
+ @policy.connect_src :self
+ assert_match %r{connect-src 'self'}, @policy.build
+
+ @policy.connect_src false
+ assert_no_match %r{connect-src}, @policy.build
+
+ @policy.default_src :self
+ assert_match %r{default-src 'self'}, @policy.build
+
+ @policy.default_src false
+ assert_no_match %r{default-src}, @policy.build
+
+ @policy.font_src :self
+ assert_match %r{font-src 'self'}, @policy.build
+
+ @policy.font_src false
+ assert_no_match %r{font-src}, @policy.build
+
+ @policy.frame_src :self
+ assert_match %r{frame-src 'self'}, @policy.build
+
+ @policy.frame_src false
+ assert_no_match %r{frame-src}, @policy.build
+
+ @policy.img_src :self
+ assert_match %r{img-src 'self'}, @policy.build
+
+ @policy.img_src false
+ assert_no_match %r{img-src}, @policy.build
+
+ @policy.manifest_src :self
+ assert_match %r{manifest-src 'self'}, @policy.build
+
+ @policy.manifest_src false
+ assert_no_match %r{manifest-src}, @policy.build
+
+ @policy.media_src :self
+ assert_match %r{media-src 'self'}, @policy.build
+
+ @policy.media_src false
+ assert_no_match %r{media-src}, @policy.build
+
+ @policy.object_src :self
+ assert_match %r{object-src 'self'}, @policy.build
+
+ @policy.object_src false
+ assert_no_match %r{object-src}, @policy.build
+
+ @policy.script_src :self
+ assert_match %r{script-src 'self'}, @policy.build
+
+ @policy.script_src false
+ assert_no_match %r{script-src}, @policy.build
+
+ @policy.style_src :self
+ assert_match %r{style-src 'self'}, @policy.build
+
+ @policy.style_src false
+ assert_no_match %r{style-src}, @policy.build
+
+ @policy.worker_src :self
+ assert_match %r{worker-src 'self'}, @policy.build
+
+ @policy.worker_src false
+ assert_no_match %r{worker-src}, @policy.build
+ end
+
+ def test_document_directives
+ @policy.base_uri "https://example.com"
+ assert_match %r{base-uri https://example\.com;}, @policy.build
+
+ @policy.plugin_types "application/x-shockwave-flash"
+ assert_match %r{plugin-types application/x-shockwave-flash;}, @policy.build
+
+ @policy.sandbox
+ assert_match %r{sandbox;}, @policy.build
+
+ @policy.sandbox "allow-scripts", "allow-modals"
+ assert_match %r{sandbox allow-scripts allow-modals;}, @policy.build
+
+ @policy.sandbox false
+ assert_no_match %r{sandbox}, @policy.build
+ end
+
+ def test_navigation_directives
+ @policy.form_action :self
+ assert_match %r{form-action 'self';}, @policy.build
+
+ @policy.frame_ancestors :self
+ assert_match %r{frame-ancestors 'self';}, @policy.build
+ end
+
+ def test_reporting_directives
+ @policy.report_uri "/violations"
+ assert_match %r{report-uri /violations;}, @policy.build
+ end
+
+ def test_other_directives
+ @policy.block_all_mixed_content
+ assert_match %r{block-all-mixed-content;}, @policy.build
+
+ @policy.block_all_mixed_content false
+ assert_no_match %r{block-all-mixed-content}, @policy.build
+
+ @policy.require_sri_for :script, :style
+ assert_match %r{require-sri-for script style;}, @policy.build
+
+ @policy.require_sri_for "script", "style"
+ assert_match %r{require-sri-for script style;}, @policy.build
+
+ @policy.require_sri_for
+ assert_no_match %r{require-sri-for}, @policy.build
+
+ @policy.upgrade_insecure_requests
+ assert_match %r{upgrade-insecure-requests;}, @policy.build
+
+ @policy.upgrade_insecure_requests false
+ assert_no_match %r{upgrade-insecure-requests}, @policy.build
+ end
+
+ def test_multiple_sources
+ @policy.script_src :self, :https
+ assert_equal "script-src 'self' https:;", @policy.build
+ end
+
+ def test_multiple_directives
+ @policy.script_src :self, :https
+ @policy.style_src :self, :https
+ assert_equal "script-src 'self' https:; style-src 'self' https:;", @policy.build
+ end
+
+ def test_dynamic_directives
+ request = Struct.new(:host).new("www.example.com")
+ controller = Struct.new(:request).new(request)
+
+ @policy.script_src -> { request.host }
+ assert_equal "script-src www.example.com;", @policy.build(controller)
+ end
+
+ def test_mixed_static_and_dynamic_directives
+ @policy.script_src :self, -> { "foo.com" }, "bar.com"
+ assert_equal "script-src 'self' foo.com bar.com;", @policy.build(Object.new)
+ end
+
+ def test_invalid_directive_source
+ exception = assert_raises(ArgumentError) do
+ @policy.script_src [:self]
+ end
+
+ assert_equal "Invalid content security policy source: [:self]", exception.message
+ end
+
+ def test_missing_context_for_dynamic_source
+ @policy.script_src -> { request.host }
+
+ exception = assert_raises(RuntimeError) do
+ @policy.build
+ end
+
+ assert_match %r{\AMissing context for the dynamic content security policy source:}, exception.message
+ end
+
+ def test_raises_runtime_error_when_unexpected_source
+ @policy.plugin_types [:flash]
+
+ exception = assert_raises(RuntimeError) do
+ @policy.build
+ end
+
+ assert_match %r{\AUnexpected content security policy source:}, exception.message
+ end
+end
+
+class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ content_security_policy only: :inline do |p|
+ p.default_src "https://example.com"
+ end
+
+ content_security_policy only: :conditional, if: :condition? do |p|
+ p.default_src "https://true.example.com"
+ end
+
+ content_security_policy only: :conditional, unless: :condition? do |p|
+ p.default_src "https://false.example.com"
+ end
+
+ content_security_policy only: :report_only do |p|
+ p.report_uri "/violations"
+ end
+
+ content_security_policy_report_only only: :report_only
+
+ def index
+ head :ok
+ end
+
+ def inline
+ head :ok
+ end
+
+ def conditional
+ head :ok
+ end
+
+ def report_only
+ head :ok
+ end
+
+ private
+ def condition?
+ params[:condition] == "true"
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "content_security_policy_integration_test" do
+ get "/", to: "policy#index"
+ get "/inline", to: "policy#inline"
+ get "/conditional", to: "policy#conditional"
+ get "/report-only", to: "policy#report_only"
+ end
+ end
+
+ POLICY = ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src :self
+ end
+
+ class PolicyConfigMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.content_security_policy"] = POLICY
+ env["action_dispatch.content_security_policy_report_only"] = false
+ env["action_dispatch.show_exceptions"] = false
+
+ @app.call(env)
+ end
+ end
+
+ APP = build_app(ROUTES) do |middleware|
+ middleware.use PolicyConfigMiddleware
+ middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
+ def app
+ APP
+ end
+
+ def test_generates_content_security_policy_header
+ get "/"
+ assert_policy "default-src 'self';"
+ end
+
+ def test_generates_inline_content_security_policy
+ get "/inline"
+ assert_policy "default-src https://example.com;"
+ end
+
+ def test_generates_conditional_content_security_policy
+ get "/conditional", params: { condition: "true" }
+ assert_policy "default-src https://true.example.com;"
+
+ get "/conditional", params: { condition: "false" }
+ assert_policy "default-src https://false.example.com;"
+ end
+
+ def test_generates_report_only_content_security_policy
+ get "/report-only"
+ assert_policy "default-src 'self'; report-uri /violations;", report_only: true
+ end
+
+ private
+
+ def env_config
+ Rails.application.env_config
+ end
+
+ def content_security_policy
+ env_config["action_dispatch.content_security_policy"]
+ end
+
+ def content_security_policy=(policy)
+ env_config["action_dispatch.content_security_policy"] = policy
+ end
+
+ def assert_policy(expected, report_only: false)
+ assert_response :success
+
+ if report_only
+ expected_header = "Content-Security-Policy-Report-Only"
+ unexpected_header = "Content-Security-Policy"
+ else
+ expected_header = "Content-Security-Policy"
+ unexpected_header = "Content-Security-Policy-Report-Only"
+ end
+
+ assert_nil response.headers[unexpected_header]
+ assert_equal expected, response.headers[expected_header]
+ end
+end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index 90e95e972d..6854783386 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -30,21 +30,21 @@ class MimeTypeTest < ActiveSupport::TestCase
test "parse text with trailing star at the beginning" do
accept = "text/*, text/html, application/json, multipart/form-data"
- expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]]
+ expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, parsed
+ assert_equal expect.map(&:to_s), parsed.map(&:to_s)
end
test "parse text with trailing star in the end" do
accept = "text/html, application/json, multipart/form-data, text/*"
- expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml]]
+ expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, parsed
+ assert_equal expect.map(&:to_s), parsed.map(&:to_s)
end
test "parse text with trailing star" do
accept = "text/*"
- expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json]]
+ expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json]]
parsed = Mime::Type.parse(accept)
assert_equal expect.map(&:to_s).sort!, parsed.map(&:to_s).sort!
end
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index c4ee3add2a..4e350162c9 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -311,13 +311,15 @@ class ResponseTest < ActiveSupport::TestCase
end
end
- test "read x_frame_options, x_content_type_options and x_xss_protection" do
+ test "read x_frame_options, x_content_type_options, x_xss_protection, x_download_options and x_permitted_cross_domain_policies" do
original_default_headers = ActionDispatch::Response.default_headers
begin
ActionDispatch::Response.default_headers = {
"X-Frame-Options" => "DENY",
"X-Content-Type-Options" => "nosniff",
- "X-XSS-Protection" => "1;"
+ "X-XSS-Protection" => "1;",
+ "X-Download-Options" => "noopen",
+ "X-Permitted-Cross-Domain-Policies" => "none"
}
resp = ActionDispatch::Response.create.tap { |response|
response.body = "Hello"
@@ -327,6 +329,8 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal("DENY", resp.headers["X-Frame-Options"])
assert_equal("nosniff", resp.headers["X-Content-Type-Options"])
assert_equal("1;", resp.headers["X-XSS-Protection"])
+ assert_equal("noopen", resp.headers["X-Download-Options"])
+ assert_equal("none", resp.headers["X-Permitted-Cross-Domain-Policies"])
ensure
ActionDispatch::Response.default_headers = original_default_headers
end
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 44f902c163..b2d2bf0416 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -5057,3 +5057,40 @@ class TestRecognizePath < ActionDispatch::IntegrationTest
Routes.recognize_path(*args)
end
end
+
+class TestRelativeUrlRootGeneration < ActionDispatch::IntegrationTest
+ config = ActionDispatch::Routing::RouteSet::Config.new("/blog", false)
+
+ stub_controllers(config) do |routes|
+ Routes = routes
+
+ routes.draw do
+ get "/", to: "posts#index", as: :posts
+ get "/:id", to: "posts#show", as: :post
+ end
+ end
+
+ include Routes.url_helpers
+
+ APP = build_app Routes
+
+ def app
+ APP
+ end
+
+ def test_url_helpers
+ assert_equal "/blog/", posts_path({})
+ assert_equal "/blog/", Routes.url_helpers.posts_path({})
+
+ assert_equal "/blog/1", post_path(id: "1")
+ assert_equal "/blog/1", Routes.url_helpers.post_path(id: "1")
+ end
+
+ def test_optimized_url_helpers
+ assert_equal "/blog/", posts_path
+ assert_equal "/blog/", Routes.url_helpers.posts_path
+
+ assert_equal "/blog/1", post_path("1")
+ assert_equal "/blog/1", Routes.url_helpers.post_path("1")
+ end
+end
diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb
index 2c43a8c29f..e34426a471 100644
--- a/actionpack/test/dispatch/session/cookie_store_test.rb
+++ b/actionpack/test/dispatch/session/cookie_store_test.rb
@@ -8,11 +8,14 @@ require "active_support/messages/rotation_configuration"
class CookieStoreTest < ActionDispatch::IntegrationTest
SessionKey = "_myapp_session"
SessionSecret = "b3c631c314c0bbca50c1b2843150fe33"
- Generator = ActiveSupport::LegacyKeyGenerator.new(SessionSecret)
+ SessionSalt = "authenticated encrypted cookie"
+
+ Generator = ActiveSupport::KeyGenerator.new(SessionSecret, iterations: 1000)
Rotations = ActiveSupport::Messages::RotationConfiguration.new
- Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, digest: "SHA1")
- SignedBar = Verifier.generate(foo: "bar", session_id: SecureRandom.hex(16))
+ Encryptor = ActiveSupport::MessageEncryptor.new(
+ Generator.generate_key(SessionSalt, 32), cipher: "aes-256-gcm", serializer: Marshal
+ )
class TestController < ActionController::Base
def no_session_access
@@ -25,12 +28,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def set_session_value
session[:foo] = "bar"
- render plain: Rack::Utils.escape(Verifier.generate(session.to_hash))
- end
-
- def set_session_value_expires_in_five_hours
- session[:foo] = "bar"
- render plain: Rack::Utils.escape(Verifier.generate(session.to_hash, expires_in: 5.hours))
+ render body: nil
end
def get_session_value
@@ -72,19 +70,35 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
end
end
+ def parse_cookie_from_header
+ cookie_matches = headers["Set-Cookie"].match(/#{SessionKey}=([^;]+)/)
+ cookie_matches && cookie_matches[1]
+ end
+
+ def assert_session_cookie(cookie_string, contents)
+ assert_includes headers["Set-Cookie"], cookie_string
+
+ session_value = parse_cookie_from_header
+ session_data = Encryptor.decrypt_and_verify(Rack::Utils.unescape(session_value)) rescue nil
+
+ assert_not_nil session_data, "session failed to decrypt"
+ assert_equal session_data.slice(*contents.keys), contents
+ end
+
def test_setting_session_value
with_test_route_set do
get "/set_session_value"
+
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
end
end
def test_getting_session_value
with_test_route_set do
- cookies[SessionKey] = SignedBar
+ get "/set_session_value"
get "/get_session_value"
+
assert_response :success
assert_equal 'foo: "bar"', response.body
end
@@ -92,8 +106,9 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_getting_session_id
with_test_route_set do
- cookies[SessionKey] = SignedBar
+ get "/set_session_value"
get "/persistent_session_id"
+
assert_response :success
assert_equal 32, response.body.size
session_id = response.body
@@ -106,8 +121,12 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_disregards_tampered_sessions
with_test_route_set do
- cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--123456780"
+ encryptor = ActiveSupport::MessageEncryptor.new("A" * 32, cipher: "aes-256-gcm", serializer: Marshal)
+
+ cookies[SessionKey] = encryptor.encrypt_and_sign("foo" => "bar", "session_id" => "abc")
+
get "/get_session_value"
+
assert_response :success
assert_equal "foo: nil", response.body
end
@@ -135,19 +154,19 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_does_set_secure_cookies_over_https
with_test_route_set(secure: true) do
get "/set_session_value", headers: { "HTTPS" => "on" }
+
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; secure; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; secure; HttpOnly", "foo" => "bar"
end
end
# {:foo=>#<SessionAutoloadTest::Foo bar:"baz">, :session_id=>"ce8b0752a6ab7c7af3cdb8a80e6b9e46"}
- SignedSerializedCookie = "BAh7BzoIZm9vbzodU2Vzc2lvbkF1dG9sb2FkVGVzdDo6Rm9vBjoJQGJhciIIYmF6Og9zZXNzaW9uX2lkIiVjZThiMDc1MmE2YWI3YzdhZjNjZGI4YTgwZTZiOWU0Ng==--2bf3af1ae8bd4e52b9ac2099258ace0c380e601c"
+ EncryptedSerializedCookie = "9RZ2Fij0qLveUwM4s+CCjGqhpjyUC8jiBIf/AiBr9M3TB8xh2vQZtvSOMfN3uf6oYbbpIDHAcOFIEl69FcW1ozQYeSrCLonYCazoh34ZdYskIQfGwCiSYleVXG1OD9Z4jFqeVArw4Ewm0paOOPLbN1rc6A==--I359v/KWdZ1ok0ey--JFFhuPOY7WUo6tB/eP05Aw=="
def test_deserializes_unloaded_classes_on_get_id
with_test_route_set do
with_autoload_path "session_autoload_test" do
- cookies[SessionKey] = SignedSerializedCookie
+ cookies[SessionKey] = EncryptedSerializedCookie
get "/get_session_id"
assert_response :success
assert_equal "id: ce8b0752a6ab7c7af3cdb8a80e6b9e46", response.body, "should auto-load unloaded class"
@@ -158,7 +177,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_deserializes_unloaded_classes_on_get_value
with_test_route_set do
with_autoload_path "session_autoload_test" do
- cookies[SessionKey] = SignedSerializedCookie
+ cookies[SessionKey] = EncryptedSerializedCookie
get "/get_session_value"
assert_response :success
assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class"
@@ -197,8 +216,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
get "/set_session_value"
assert_response :success
session_payload = response.body
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
get "/call_reset_session"
assert_response :success
@@ -216,8 +234,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
with_test_route_set do
get "/set_session_value"
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
get "/get_class_after_reset_session"
assert_response :success
@@ -239,8 +256,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
with_test_route_set do
get "/set_session_value"
assert_response :success
- assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; HttpOnly", "foo" => "bar"
get "/call_session_clear"
assert_response :success
@@ -253,7 +269,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_persistent_session_id
with_test_route_set do
- cookies[SessionKey] = SignedBar
+ get "/set_session_value"
get "/persistent_session_id"
assert_response :success
assert_equal 32, response.body.size
@@ -268,8 +284,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def test_setting_session_id_to_nil_is_respected
with_test_route_set do
- cookies[SessionKey] = SignedBar
-
+ get "/set_session_value"
get "/get_session_id"
sid = response.body
assert_equal 36, sid.size
@@ -283,31 +298,53 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
with_test_route_set(expire_after: 5.hours) do
# First request accesses the session
time = Time.local(2008, 4, 24)
+
Time.stub :now, time do
expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
- cookies[SessionKey] = SignedBar
+ get "/set_session_value"
- get "/set_session_value_expires_in_five_hours"
assert_response :success
-
- cookie_body = response.body
- assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly",
- headers["Set-Cookie"]
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
end
# Second request does not access the session
- time = Time.local(2008, 4, 25)
+ time = time + 3.hours
Time.stub :now, time do
expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
- cookies[SessionKey] = SignedBar
-
get "/no_session_access"
+
assert_response :success
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
+ end
+ end
+ end
+
+ def test_session_store_with_expire_after_does_not_accept_expired_session
+ with_test_route_set(expire_after: 5.hours) do
+ # First request accesses the session
+ time = Time.local(2017, 11, 12)
+
+ Time.stub :now, time do
+ expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
- assert_equal "_myapp_session=#{cookies[SessionKey]}; path=/; expires=#{expected_expiry}; HttpOnly",
- headers["Set-Cookie"]
+ get "/set_session_value"
+ get "/get_session_value"
+
+ assert_response :success
+ assert_equal 'foo: "bar"', response.body
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
+ end
+
+ # Second request is beyond the expiry time and the session is invalidated
+ time += 5.hours + 1.minute
+
+ Time.stub :now, time do
+ get "/get_session_value"
+
+ assert_response :success
+ assert_equal "foo: nil", response.body
end
end
end
@@ -347,8 +384,14 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def get(path, *args)
args[0] ||= {}
args[0][:headers] ||= {}
- args[0][:headers]["action_dispatch.key_generator"] ||= Generator
- args[0][:headers]["action_dispatch.cookies_rotations"] ||= Rotations
+ args[0][:headers].tap do |config|
+ config["action_dispatch.secret_key_base"] = SessionSecret
+ config["action_dispatch.authenticated_encrypted_cookie_salt"] = SessionSalt
+ config["action_dispatch.use_authenticated_cookie_encryption"] = true
+
+ config["action_dispatch.key_generator"] ||= Generator
+ config["action_dispatch.cookies_rotations"] ||= Rotations
+ end
super(path, *args)
end
diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb
index 75feae6fe0..fcdaf7fb4c 100644
--- a/actionpack/test/dispatch/system_testing/driver_test.rb
+++ b/actionpack/test/dispatch/system_testing/driver_test.rb
@@ -25,6 +25,14 @@ class DriverTest < ActiveSupport::TestCase
assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
end
+ test "initializing the driver with a headless firefox" do
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_firefox, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" })
+ assert_equal :selenium, driver.instance_variable_get(:@name)
+ assert_equal :headless_firefox, driver.instance_variable_get(:@browser)
+ assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
+ end
+
test "initializing the driver with a poltergeist" do
driver = ActionDispatch::SystemTesting::Driver.new(:poltergeist, screen_size: [1400, 1400], options: { js_errors: false })
assert_equal :poltergeist, driver.instance_variable_get(:@name)
diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
index 2afda31cf5..264844fc7d 100644
--- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
+++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
@@ -35,6 +35,11 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
end
end
+ test "defaults to simple output for the screenshot" do
+ new_test = DrivenBySeleniumWithChrome.new("x")
+ assert_equal "simple", new_test.send(:output_type)
+ end
+
test "display_image return artifact format when specify RAILS_SYSTEM_TESTING_SCREENSHOT environment" do
begin
original_output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"]
@@ -42,6 +47,8 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
new_test = DrivenBySeleniumWithChrome.new("x")
+ assert_equal "artifact", new_test.send(:output_type)
+
Rails.stub :root, Pathname.getwd do
new_test.stub :passed?, false do
assert_match %r|url=artifact://.+?tmp/screenshots/failures_x\.png|, new_test.send(:display_image)
diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb
index 1866225fc1..95e411faf4 100644
--- a/actionpack/test/dispatch/system_testing/server_test.rb
+++ b/actionpack/test/dispatch/system_testing/server_test.rb
@@ -6,10 +6,27 @@ require "action_dispatch/system_testing/server"
class ServerTest < ActiveSupport::TestCase
setup do
- ActionDispatch::SystemTesting::Server.new.run
+ @old_capybara_server = Capybara.server
end
test "port is always included" do
+ ActionDispatch::SystemTesting::Server.new.run
assert Capybara.always_include_port, "expected Capybara.always_include_port to be true"
end
+
+ test "server is changed from `default` to `puma`" do
+ Capybara.server = :default
+ ActionDispatch::SystemTesting::Server.new.run
+ refute_equal Capybara.server, Capybara.servers[:default]
+ end
+
+ test "server is not changed to `puma` when is different than default" do
+ Capybara.server = :webrick
+ ActionDispatch::SystemTesting::Server.new.run
+ assert_equal Capybara.server, Capybara.servers[:webrick]
+ end
+
+ teardown do
+ Capybara.server = @old_capybara_server
+ end
end
diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
index c6a6aef92b..b078a5abc5 100644
--- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb
+++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
@@ -28,6 +28,12 @@ class SetDriverToSeleniumHeadlessChromeTest < DrivenBySeleniumWithHeadlessChrome
end
end
+class SetDriverToSeleniumHeadlessFirefoxTest < DrivenBySeleniumWithHeadlessFirefox
+ test "uses selenium headless firefox" do
+ assert_equal :selenium, Capybara.current_driver
+ end
+end
+
class SetHostTest < DrivenByRackTest
test "sets default host" do
assert_equal "http://127.0.0.1", Capybara.app_host