0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/spec/integrity/upcoming_change_metadata_spec.rb
David Battersby a04a642492
DEV: hide and disable upcoming changes for disabled plugins (#41657)
The upcoming changes page displays changes belonging to plugins that are
disabled. The same applies to the weekly "new upcoming change available"
notification, which names changes for plugins not enabled.

The proposal is that a plugin's upcoming changes are part of the plugin
itself. If the plugin is disabled, its changes are neither advertised
nor active. If the admin had opted in to a plugin's upcoming change
already, that choice is remembered and restored when they re-enable the
plugin.

The one exception is when the upcoming change is the plugin's on/off
switch — it stays visible, because that row is how the admin opts into
the plugin in the first place.
2026-07-16 14:35:20 +04:00

183 lines
7.4 KiB
Ruby
Vendored

# frozen_string_literal: true
def upcoming_change_setting_files
[
Rails.root.join("config/site_settings.yml").to_s,
*Dir["#{Rails.root.join("plugins/*/config/settings.yml")}"].sort,
]
end
def each_upcoming_change_setting
upcoming_change_setting_files.each do |file|
SiteSettings::YamlLoader
.new(file)
.load do |category, setting_name, default, opts|
next if opts[:upcoming_change].blank?
setting = {
file: file.delete_prefix("#{Rails.root.join("")}"),
setting_name: setting_name,
default: default,
options: opts,
upcoming_change: opts[:upcoming_change],
}
yield setting
end
end
end
def upcoming_change_setting_label(setting)
"#{setting[:setting_name]} in #{setting[:file]}"
end
def valid_upcoming_change_impact_types
%w[feature other site_setting_default]
end
def valid_upcoming_change_impact_roles
%w[staff admins moderators all_members developers]
end
# Derived from the file path rather than Discourse.plugins_by_name, so the checks
# hold regardless of which plugins happen to be loaded in the current test run.
def upcoming_change_owning_plugin_dir(setting)
setting[:file][%r{plugins/([^/]+)/}, 1]
end
def upcoming_change_plugin_enabled_site_setting(plugin_dir)
plugin_rb = Rails.root.join("plugins", plugin_dir, "plugin.rb")
return nil unless File.exist?(plugin_rb)
File.read(plugin_rb)[/enabled_site_setting\s+:(\w+)/, 1]&.to_sym
end
RSpec.describe "upcoming change metadata integrity checks" do
each_upcoming_change_setting do |setting|
label = upcoming_change_setting_label(setting)
it "#{label} is valid" do
metadata = setting[:upcoming_change]
allowed_keys = %i[
status
impact
learn_more_url
allow_enabled_for
body_class
permanent_warning
hide_settings
requires_plugin_enabled
]
required_keys = %i[status impact]
unsupported_keys = metadata.keys - allowed_keys
missing_keys = required_keys - metadata.keys
valid_statuses = UpcomingChanges.statuses.keys
status = metadata[:status].to_sym
impact = metadata[:impact]
impact_parts = impact.is_a?(String) ? impact.split(",") : []
learn_more_url = metadata[:learn_more_url]
allow_enabled_for = metadata[:allow_enabled_for]
body_class = metadata[:body_class]
permanent_warning = metadata[:permanent_warning]
hide_settings = metadata[:hide_settings]
requires_plugin_enabled = metadata[:requires_plugin_enabled]
setting_name_sym = setting[:setting_name].to_sym
owning_plugin_dir = upcoming_change_owning_plugin_dir(setting)
is_own_enabled_site_setting =
owning_plugin_dir.present? &&
upcoming_change_plugin_enabled_site_setting(owning_plugin_dir) == setting_name_sym
aggregate_failures do
expect(setting[:options][:hidden]).to eq(true), "#{label} must set `hidden: true`"
expect(setting[:options][:client]).to eq(true), "#{label} must set `client: true`"
expect(setting[:default]).to eq(false), "#{label} must set `default: false`"
expect(unsupported_keys).to be_empty,
"#{label} has unsupported upcoming_change keys: #{unsupported_keys.join(", ")}. Allowed keys: #{allowed_keys.join(", ")}"
expect(missing_keys).to be_empty,
"#{label} is missing required upcoming_change keys: #{missing_keys.join(", ")}"
expect(valid_statuses).to include(status),
"#{label} has invalid upcoming_change status #{status.inspect}. Valid statuses: #{valid_statuses.join(", ")}"
expect(impact_parts.length).to eq(2),
"#{label} must set upcoming_change.impact as `type,role`, got #{impact.inspect}"
if impact_parts.length == 2
impact_type, impact_role = impact_parts
expect(valid_upcoming_change_impact_types).to include(impact_type),
"#{label} has invalid upcoming_change impact type #{impact_type.inspect}. Valid types: #{valid_upcoming_change_impact_types.join(", ")}"
expect(valid_upcoming_change_impact_roles).to include(impact_role),
"#{label} has invalid upcoming_change impact role #{impact_role.inspect}. Valid roles: #{valid_upcoming_change_impact_roles.join(", ")}"
end
if status != :conceptual
aggregate_failures do
expect(learn_more_url).to be_present,
"#{label} must set `upcoming_change.learn_more_url` when status is not `conceptual`"
if learn_more_url.present?
expect(learn_more_url).to match(%r{\Ahttps://meta\.discourse\.org/t/-/\d+\z}),
"#{label} upcoming_change.learn_more_url must match https://meta.discourse.org/t/-/NNNN, do not include the topic slug"
end
end
end
if allow_enabled_for.present?
valid_values = %w[everyone staff specific_groups]
allow_strings = Array(allow_enabled_for).map(&:to_s)
expect(allow_enabled_for).to be_an(Array),
"#{label} `upcoming_change.allow_enabled_for` must be an array"
expect(allow_strings).not_to be_empty,
"#{label} `upcoming_change.allow_enabled_for` must not be empty"
expect(allow_strings - valid_values).to be_empty,
"#{label} `upcoming_change.allow_enabled_for` contains invalid values: #{(allow_strings - valid_values).join(", ")}. Valid values: #{valid_values.join(", ")}"
if allow_strings.include?("everyone")
expect(allow_strings).to eq(["everyone"]),
"#{label} `upcoming_change.allow_enabled_for` may not combine `everyone` with other values"
end
end
unless body_class.nil?
expect([true, false]).to include(body_class),
"#{label} `upcoming_change.body_class` must be a boolean"
end
unless permanent_warning.nil?
expect([true, false]).to include(permanent_warning),
"#{label} `upcoming_change.permanent_warning` must be a boolean"
end
unless requires_plugin_enabled.nil?
expect([true, false]).to include(requires_plugin_enabled),
"#{label} `upcoming_change.requires_plugin_enabled` must be a boolean"
expect(owning_plugin_dir).to be_present,
"#{label} sets `upcoming_change.requires_plugin_enabled` but is not owned by a plugin"
end
# Plugin-owned changes are gated on their plugin by default. A change that
# is its plugin's own `enabled_site_setting` must opt out with
# `requires_plugin_enabled: false`, otherwise the default gate would gate the
# change on itself and it could never be reached (enabling it is how admins
# turn the plugin on in the first place).
if is_own_enabled_site_setting
expect(requires_plugin_enabled).to eq(false),
"#{label} is its plugin's own `enabled_site_setting` and must set `upcoming_change.requires_plugin_enabled: false`, otherwise the change gates itself"
end
if hide_settings.present?
expect(hide_settings).to be_an(Array),
"#{label} `upcoming_change.hide_settings` must be an array"
unknown_settings =
Array(hide_settings).map(&:to_s).reject { |s| SiteSetting.respond_to?(s) }
expect(unknown_settings).to be_empty,
"#{label} `upcoming_change.hide_settings` references unknown site settings: #{unknown_settings.join(", ")}"
end
end
end
end
end