mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
b1f74ab59e
The strict-dynamic CSP directive is supported in all our target browsers, and makes for a much simpler configuration. Instead of allowlisting paths, we use a per-request nonce to authorize `<script>` tags, and then those scripts are allowed to load additional scripts (or add additional inline scripts) without restriction. This becomes especially useful when admins want to add external scripts like Google Tag Manager, or advertising scripts, which then go on to load a ton of other scripts. All script tags introduced via themes will automatically have the nonce attribute applied, so it should be zero-effort for theme developers. Plugins *may* need some changes if they are inserting their own script tags. This commit introduces a strict-dynamic-based CSP behind an experimental `content_security_policy_strict_dynamic` site setting.
219 lines
7.1 KiB
Ruby
219 lines
7.1 KiB
Ruby
# frozen_string_literal: true
|
|
require "highline/import"
|
|
|
|
module SystemHelpers
|
|
PLATFORM_KEY_MODIFIER = RUBY_PLATFORM =~ /darwin/i ? :meta : :control
|
|
|
|
def pause_test
|
|
result =
|
|
ask(
|
|
"\n\e[33mTest paused, press enter to resume, type `d` and press enter to start debugger.\e[0m",
|
|
)
|
|
binding.pry if result == "d" # rubocop:disable Lint/Debugger
|
|
self
|
|
end
|
|
|
|
def sign_in(user)
|
|
visit File.join(
|
|
GlobalSetting.relative_url_root || "",
|
|
"/session/#{user.encoded_username}/become.json?redirect=false",
|
|
)
|
|
|
|
expect(page).to have_content("Signed in to #{user.encoded_username} successfully")
|
|
end
|
|
|
|
# Uploads a theme from a directory.
|
|
#
|
|
# @param set_theme_as_default [Boolean] Whether to set the uploaded theme as the default theme for the site. Defaults to true.
|
|
#
|
|
# @return [Theme] The uploaded theme model given by `models/theme.rb`.
|
|
#
|
|
# @example Upload a theme and set it as default
|
|
# upload_theme("/path/to/theme")
|
|
def upload_theme(set_theme_as_default: true)
|
|
theme = RemoteTheme.import_theme_from_directory(theme_dir_from_caller)
|
|
|
|
if theme.component
|
|
raise "Uploaded theme is a theme component, please use the `upload_theme_component` method instead."
|
|
end
|
|
|
|
theme.set_default! if set_theme_as_default
|
|
theme
|
|
end
|
|
|
|
# Uploads a theme component from a directory.
|
|
#
|
|
# @param parent_theme_id [Integer] The ID of the theme to add the theme component to. Defaults to `SiteSetting.default_theme_id`.
|
|
#
|
|
# @return [Theme] The uploaded theme model given by `models/theme.rb`.
|
|
#
|
|
# @example Upload a theme component
|
|
# upload_theme_component("/path/to/theme_component")
|
|
#
|
|
# @example Upload a theme component and add it to a specific theme
|
|
# upload_theme_component("/path/to/theme_component", parent_theme_id: 123)
|
|
def upload_theme_component(parent_theme_id: SiteSetting.default_theme_id)
|
|
theme = RemoteTheme.import_theme_from_directory(theme_dir_from_caller)
|
|
|
|
if !theme.component
|
|
raise "Uploaded theme is not a theme component, please use the `upload_theme` method instead."
|
|
end
|
|
|
|
Theme.find(parent_theme_id).child_themes << theme
|
|
theme
|
|
end
|
|
|
|
def setup_system_test
|
|
SiteSetting.login_required = false
|
|
SiteSetting.has_login_hint = false
|
|
SiteSetting.force_hostname = Capybara.server_host
|
|
SiteSetting.port = Capybara.server_port
|
|
SiteSetting.external_system_avatars_enabled = false
|
|
SiteSetting.disable_avatar_education_message = true
|
|
SiteSetting.enable_user_tips = false
|
|
SiteSetting.splash_screen = false
|
|
SiteSetting.allowed_internal_hosts =
|
|
(
|
|
SiteSetting.allowed_internal_hosts.to_s.split("|") +
|
|
MinioRunner.config.minio_urls.map { |url| URI.parse(url).host }
|
|
).join("|")
|
|
end
|
|
|
|
def try_until_success(timeout: Capybara.default_max_wait_time, frequency: 0.01)
|
|
start ||= Time.zone.now
|
|
backoff ||= frequency
|
|
yield
|
|
rescue RSpec::Expectations::ExpectationNotMetError,
|
|
Capybara::ExpectationNotMet,
|
|
Capybara::ElementNotFound
|
|
raise if Time.zone.now >= start + timeout.seconds
|
|
sleep backoff
|
|
backoff += frequency
|
|
retry
|
|
end
|
|
|
|
def wait_for_attribute(
|
|
element,
|
|
attribute,
|
|
value,
|
|
timeout: Capybara.default_max_wait_time,
|
|
frequency: 0.01
|
|
)
|
|
try_until_success(timeout: timeout, frequency: frequency) do
|
|
expect(element[attribute.to_sym]).to eq(value)
|
|
end
|
|
end
|
|
|
|
# Waits for an element to stop animating up to timeout seconds,
|
|
# then raises a Capybara error if it does not stop.
|
|
#
|
|
# This is based on getBoundingClientRect, where Y is the distance
|
|
# from the top of the element to the top of the viewport, and X
|
|
# is the distance from the leftmost edge of the element to the
|
|
# left of the viewport. The viewpoint origin (0, 0) is at the
|
|
# top left of the page.
|
|
#
|
|
# Once X and Y stop changing based on the current vs previous position,
|
|
# then we know the animation has stopped and the element is stabilised,
|
|
# at which point we can click on it without fear of Capybara mis-clicking.
|
|
#
|
|
# c.f. https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
|
|
def wait_for_animation(element, timeout: Capybara.default_max_wait_time)
|
|
old_element_x = nil
|
|
old_element_y = nil
|
|
|
|
try_until_success(timeout: timeout) do
|
|
current_element_x = element.rect.x
|
|
current_element_y = element.rect.y
|
|
|
|
stopped_moving = current_element_x == old_element_x && current_element_y == old_element_y
|
|
|
|
old_element_x = current_element_x
|
|
old_element_y = current_element_y
|
|
|
|
raise Capybara::ExpectationNotMet if !stopped_moving
|
|
end
|
|
end
|
|
|
|
def resize_window(width: nil, height: nil)
|
|
original_size = page.driver.browser.manage.window.size
|
|
page.driver.browser.manage.window.resize_to(
|
|
width || original_size.width,
|
|
height || original_size.height,
|
|
)
|
|
yield
|
|
ensure
|
|
page.driver.browser.manage.window.resize_to(original_size.width, original_size.height)
|
|
end
|
|
|
|
def using_browser_timezone(timezone, &example)
|
|
using_session(timezone) do
|
|
page.driver.browser.devtools.emulation.set_timezone_override(timezone_id: timezone)
|
|
freeze_time(&example)
|
|
end
|
|
end
|
|
|
|
# When using parallelism, Capybara's `using_session` method can cause
|
|
# intermittent failures as two sessions can be created with the same name
|
|
# in different tests and be run at the same time.
|
|
def using_session(name, &block)
|
|
Capybara.using_session(name.to_s + self.method_name, &block)
|
|
end
|
|
|
|
def select_text_range(selector, start = 0, offset = 5)
|
|
js = <<-JS
|
|
const node = document.querySelector(arguments[0]).childNodes[0];
|
|
const selection = window.getSelection();
|
|
const range = document.createRange();
|
|
range.selectNodeContents(node);
|
|
range.setStart(node, arguments[1]);
|
|
range.setEnd(node, arguments[1] + arguments[2]);
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
JS
|
|
|
|
page.execute_script(js, selector, start, offset)
|
|
end
|
|
|
|
def setup_s3_system_test(enable_secure_uploads: false, enable_direct_s3_uploads: true)
|
|
SiteSetting.enable_s3_uploads = true
|
|
|
|
SiteSetting.s3_upload_bucket = "discoursetest"
|
|
SiteSetting.enable_upload_debug_mode = true
|
|
|
|
SiteSetting.s3_access_key_id = MinioRunner.config.minio_root_user
|
|
SiteSetting.s3_secret_access_key = MinioRunner.config.minio_root_password
|
|
SiteSetting.s3_endpoint = MinioRunner.config.minio_server_url
|
|
|
|
SiteSetting.enable_direct_s3_uploads = enable_direct_s3_uploads
|
|
SiteSetting.secure_uploads = enable_secure_uploads
|
|
|
|
MinioRunner.start
|
|
end
|
|
|
|
def skip_unless_s3_system_specs_enabled!
|
|
if ENV["CI"]
|
|
return(
|
|
skip(
|
|
"S3 system specs are temporarily disabled in this environment to address parallel spec issues",
|
|
)
|
|
)
|
|
end
|
|
if !ENV["CI"] && !ENV["RUN_S3_SYSTEM_SPECS"]
|
|
skip(
|
|
"S3 system specs are disabled in this environment, set CI=1 or RUN_S3_SYSTEM_SPECS=1 to enable them.",
|
|
)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def theme_dir_from_caller
|
|
caller.each do |line|
|
|
if (split = line.split(%r{/spec/system/.+_spec.rb})).length > 1
|
|
return split.first
|
|
end
|
|
end
|
|
end
|
|
end
|