0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 04:48:33 +08:00
discourse/spec/lib/upcoming_changes_spec.rb
Régis Hanol 2937da0e36
FEATURE: Upcoming change to lower the pending users reminder delay default (#42019)
Previously, on sites that require staff to approve new members,
moderators were not reminded about people waiting for approval until 8
hours (480 minutes) after they registered, leaving new members waiting
far longer than necessary.

This change adds an `update_pending_users_reminder_default` upcoming
change that lowers the `pending_users_reminder_delay_minutes` default to
30 when enabled, so staff are notified promptly. It uses the virtual
default-override mechanism rather than writing to the DB, so an admin
who has customized the setting keeps their value and disabling the
change restores the previous default. The change is only surfaced on
sites with `must_approve_users` enabled, since the reminder never fires
otherwise.
2026-07-28 14:24:30 +02:00

1400 lines
46 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe UpcomingChanges do
let(:setting_name) { :enable_upload_debug_mode }
# The sample plugin's change. Plugin-owned changes are gated on their owning
# plugin being enabled by default, so this is gated unless it opts out below.
def mock_plugin_change(status: :alpha, **extra)
mock_upcoming_change_metadata(
{
enable_experimental_sample_plugin_feature: {
impact: "feature,admins",
status: status,
impact_type: "feature",
impact_role: "admins",
**extra,
},
},
)
end
# The same change, opted out of the owning-plugin gate with
# requires_plugin_enabled: false, so it stays usable while the plugin is disabled.
def mock_opted_out_plugin_change(status: :alpha, **extra)
mock_plugin_change(status: status, requires_plugin_enabled: false, **extra)
end
before do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :experimental,
impact_type: "other",
impact_role: "developers",
},
conceptual_setting: {
status: :conceptual,
},
alpha_setting: {
status: :alpha,
},
beta_setting: {
status: :beta,
},
stable_setting: {
status: :stable,
},
permanent_setting: {
status: :permanent,
},
},
)
# There is a fixture image at spec/fixtures/images/upcoming_changes/enable_upload_debug_mode.png,
# but normally upcoming change images are at Rails.public_path + /images/upcoming_changes/
Rails.stubs(:public_path).returns(Rails.root.join("spec/fixtures").to_s)
end
describe ".image_exists?" do
it "returns true when the image file exists" do
expect(described_class.image_exists?(setting_name)).to eq(true)
end
it "returns false when the image file does not exist" do
expect(described_class.image_exists?("nonexistent_setting")).to eq(false)
end
end
describe ".image_path" do
it "returns the correct path for the image" do
expect(described_class.image_path(setting_name)).to eq(
"images/upcoming_changes/#{setting_name}.png",
)
end
it "returns the correct path for plugin images" do
plugin_setting = :enable_experimental_sample_plugin_feature
expect(described_class.image_path(plugin_setting)).to eq(
"plugins/discourse-sample-plugin/images/upcoming_changes/#{plugin_setting}.png",
)
end
end
describe ".image_data" do
it "returns image URL, width, and height" do
result = described_class.image_data(setting_name)
expect(result[:url]).to eq(
"#{Discourse.base_url}/images/upcoming_changes/#{setting_name}.png",
)
expect(result[:width]).to eq(244)
expect(result[:height]).to eq(66)
end
context "when include_file_path is true" do
it "returns the image URL, width, height, and file path" do
result = described_class.image_data(setting_name, include_file_path: true)
expect(result[:file_path]).to eq(
Rails
.root
.join("spec", "fixtures", "images", "upcoming_changes", "#{setting_name}.png")
.to_s,
)
end
end
end
describe ".change_metadata" do
it "returns the metadata hash for a setting with metadata" do
metadata = described_class.change_metadata(setting_name)
expect(metadata).to eq(
{
impact: "other,developers",
status: :experimental,
impact_type: "other",
impact_role: "developers",
},
)
end
it "returns an empty hash for a setting without metadata" do
metadata = described_class.change_metadata("nonexistent_setting")
expect(metadata).to eq({})
end
it "accepts string setting names" do
metadata = described_class.change_metadata(setting_name)
expect(metadata[:status]).to eq(:experimental)
end
it "accepts symbol setting names" do
metadata = described_class.change_metadata(setting_name.to_sym)
expect(metadata[:status]).to eq(:experimental)
end
end
describe ".not_yet_stable?" do
it "returns true for conceptual status" do
expect(described_class.not_yet_stable?("conceptual_setting")).to eq(true)
end
it "returns true for experimental status" do
expect(described_class.not_yet_stable?(setting_name)).to eq(true)
end
it "returns true for alpha status" do
expect(described_class.not_yet_stable?("alpha_setting")).to eq(true)
end
it "returns true for beta status" do
expect(described_class.not_yet_stable?("beta_setting")).to eq(true)
end
it "returns false for stable status" do
expect(described_class.not_yet_stable?("stable_setting")).to eq(false)
end
it "returns false for permanent status" do
expect(described_class.not_yet_stable?("permanent_setting")).to eq(false)
end
end
describe ".stable_or_permanent?" do
it "returns false for conceptual status" do
expect(described_class.stable_or_permanent?("conceptual_setting")).to eq(false)
end
it "returns false for experimental status" do
expect(described_class.stable_or_permanent?(setting_name)).to eq(false)
end
it "returns false for alpha status" do
expect(described_class.stable_or_permanent?("alpha_setting")).to eq(false)
end
it "returns false for beta status" do
expect(described_class.stable_or_permanent?("beta_setting")).to eq(false)
end
it "returns true for stable status" do
expect(described_class.stable_or_permanent?("stable_setting")).to eq(true)
end
it "returns true for permanent status" do
expect(described_class.stable_or_permanent?("permanent_setting")).to eq(true)
end
end
describe ".change_status_value" do
it "returns -100 for conceptual status" do
expect(described_class.change_status_value("conceptual_setting")).to eq(-100)
end
it "returns 0 for experimental status" do
expect(described_class.change_status_value(setting_name)).to eq(0)
end
it "returns 100 for alpha status" do
expect(described_class.change_status_value("alpha_setting")).to eq(100)
end
it "returns 200 for beta status" do
expect(described_class.change_status_value("beta_setting")).to eq(200)
end
it "returns 300 for stable status" do
expect(described_class.change_status_value("stable_setting")).to eq(300)
end
it "returns 500 for permanent status" do
expect(described_class.change_status_value("permanent_setting")).to eq(500)
end
end
describe ".change_status" do
it "returns :conceptual for conceptual status" do
expect(described_class.change_status("conceptual_setting")).to eq(:conceptual)
end
it "returns :experimental for experimental status" do
expect(described_class.change_status(setting_name)).to eq(:experimental)
end
it "returns :alpha for alpha status" do
expect(described_class.change_status("alpha_setting")).to eq(:alpha)
end
it "returns :beta for beta status" do
expect(described_class.change_status("beta_setting")).to eq(:beta)
end
it "returns :stable for stable status" do
expect(described_class.change_status("stable_setting")).to eq(:stable)
end
it "returns :permanent for permanent status" do
expect(described_class.change_status("permanent_setting")).to eq(:permanent)
end
end
describe ".meets_or_exceeds_status?" do
it "returns true when the change meets the required status" do
expect(described_class.meets_or_exceeds_status?("stable_setting", :beta)).to eq(true)
expect(described_class.meets_or_exceeds_status?("permanent_setting", :stable)).to eq(true)
end
it "returns false when the change does not meet the required status" do
expect(described_class.meets_or_exceeds_status?("alpha_setting", :beta)).to eq(false)
expect(described_class.meets_or_exceeds_status?("beta_setting", :stable)).to eq(false)
end
end
describe ".previous_status_value" do
it "returns -100 for conceptual (lowest status)" do
expect(described_class.previous_status_value(:conceptual)).to eq(-100)
end
it "returns -100 for experimental" do
expect(described_class.previous_status_value(:experimental)).to eq(-100)
end
it "returns 0 for alpha" do
expect(described_class.previous_status_value(:alpha)).to eq(0)
end
it "returns 100 for beta" do
expect(described_class.previous_status_value(:beta)).to eq(100)
end
it "returns 200 for stable" do
expect(described_class.previous_status_value(:stable)).to eq(200)
end
it "returns 300 for permanent" do
expect(described_class.previous_status_value(:permanent)).to eq(300)
end
it "accepts string status names" do
expect(described_class.previous_status_value("stable")).to eq(200)
end
end
describe ".previous_status" do
it "returns :conceptual for conceptual (lowest status)" do
expect(described_class.previous_status(:conceptual)).to eq(:conceptual)
end
it "returns :conceptual for experimental" do
expect(described_class.previous_status(:experimental)).to eq(:conceptual)
end
it "returns :experimental for alpha" do
expect(described_class.previous_status(:alpha)).to eq(:experimental)
end
it "returns :alpha for beta" do
expect(described_class.previous_status(:beta)).to eq(:alpha)
end
it "returns :beta for stable" do
expect(described_class.previous_status(:stable)).to eq(:beta)
end
it "returns :stable for permanent" do
expect(described_class.previous_status(:permanent)).to eq(:stable)
end
it "accepts string status names" do
expect(described_class.previous_status("stable")).to eq(:beta)
end
end
describe ".next_status" do
it "returns the next automatically promoted status", :aggregate_failures do
expect(described_class.next_status(:experimental)).to eq(:alpha)
expect(described_class.next_status(:alpha)).to eq(:beta)
expect(described_class.next_status(:beta)).to eq(:stable)
expect(described_class.next_status("beta")).to eq(:stable)
end
it "returns nil for statuses outside automatic promotion", :aggregate_failures do
expect(described_class.next_status(:conceptual)).to be_nil
expect(described_class.next_status(:stable)).to be_nil
expect(described_class.next_status(:permanent)).to be_nil
expect(described_class.next_status(:never)).to be_nil
expect(described_class.next_status(:unknown)).to be_nil
expect(described_class.next_status(nil)).to be_nil
end
end
describe ".history_for" do
fab!(:admin)
it "returns UserHistory records for the given setting" do
UserHistory.create!(
action: UserHistory.actions[:upcoming_change_toggled],
subject: setting_name,
acting_user_id: admin.id,
)
history = described_class.history_for(setting_name)
expect(history.count).to eq(1)
expect(history.first.subject).to eq(setting_name.to_s)
expect(history.first.action).to eq(UserHistory.actions[:upcoming_change_toggled])
end
it "returns records ordered by created_at descending" do
first_history =
UserHistory.create!(
action: UserHistory.actions[:upcoming_change_toggled],
subject: setting_name,
acting_user_id: admin.id,
created_at: 2.days.ago,
)
second_history =
UserHistory.create!(
action: UserHistory.actions[:upcoming_change_toggled],
subject: setting_name,
acting_user_id: admin.id,
created_at: 1.day.ago,
)
history = described_class.history_for(setting_name)
expect(history.first.id).to eq(second_history.id)
expect(history.last.id).to eq(first_history.id)
end
it "returns only records matching the setting name" do
UserHistory.create!(
action: UserHistory.actions[:upcoming_change_toggled],
subject: setting_name,
acting_user_id: admin.id,
)
UserHistory.create!(
action: UserHistory.actions[:upcoming_change_toggled],
subject: "different_setting",
acting_user_id: admin.id,
)
history = described_class.history_for(setting_name)
expect(history.count).to eq(1)
expect(history.first.subject).to eq(setting_name.to_s)
end
it "returns only records with upcoming_change_toggled action" do
UserHistory.create!(
action: UserHistory.actions[:upcoming_change_toggled],
subject: setting_name,
acting_user_id: admin.id,
)
UserHistory.create!(
action: UserHistory.actions[:change_site_setting],
subject: setting_name,
acting_user_id: admin.id,
)
history = described_class.history_for(setting_name)
expect(history.count).to eq(1)
expect(history.first.action).to eq(UserHistory.actions[:upcoming_change_toggled])
end
it "returns an empty relation when no history exists" do
history = described_class.history_for("nonexistent_setting")
expect(history.count).to eq(0)
expect(history).to be_a(ActiveRecord::Relation)
end
end
describe ".owning_plugin_configurable?" do
let(:plugin_setting_name) { :enable_experimental_sample_plugin_feature }
it "returns true for a core change with no owning plugin" do
expect(described_class.owning_plugin_configurable?(setting_name)).to eq(true)
end
it "returns true when the owning plugin is configurable" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(true)
expect(described_class.owning_plugin_configurable?(plugin_setting_name)).to eq(true)
end
it "returns false when the owning plugin is not configurable" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(false)
expect(described_class.owning_plugin_configurable?(plugin_setting_name)).to eq(false)
end
it "returns true when the owning plugin has not been loaded yet" do
Discourse.stubs(:plugins_by_name).returns({})
expect(described_class.owning_plugin_configurable?(plugin_setting_name)).to eq(true)
end
end
describe ".owning_plugin_enabled?" do
let(:plugin_setting_name) { :enable_experimental_sample_plugin_feature }
after { UpcomingChanges.clear_caches! }
it "returns true for a core change with no owning plugin" do
expect(described_class.owning_plugin_enabled?(setting_name)).to eq(true)
end
context "when the change is gated on its plugin by default" do
it "returns true when the owning plugin is enabled" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:enabled?).returns(true)
expect(described_class.owning_plugin_enabled?(plugin_setting_name)).to eq(true)
end
it "returns false when the owning plugin is disabled" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:enabled?).returns(false)
expect(described_class.owning_plugin_enabled?(plugin_setting_name)).to eq(false)
end
it "returns true when the owning plugin has not been loaded yet" do
Discourse.stubs(:plugins_by_name).returns({})
expect(described_class.owning_plugin_enabled?(plugin_setting_name)).to eq(true)
end
end
context "when the change opts out with requires_plugin_enabled: false" do
before { mock_opted_out_plugin_change }
it "returns true even when the owning plugin is disabled" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:enabled?).returns(false)
expect(described_class.owning_plugin_enabled?(plugin_setting_name)).to eq(true)
end
end
context "when the change is the owning plugin's own enabled_site_setting" do
# The integrity spec requires this change to opt out with
# requires_plugin_enabled: false, but Plugin::Instance#enabled? reads the setting
# back through .enabled?, so guard against the stack overflow even if it is left
# gated. #enabled? reads the ivar directly, hence setting it rather than stubbing.
before do
SiteSetting::SAMPLE_TEST_PLUGIN.instance_variable_set(
:@enabled_site_setting,
plugin_setting_name,
)
end
after do
SiteSetting::SAMPLE_TEST_PLUGIN.instance_variable_set(:@enabled_site_setting, nil)
SiteSetting.remove_override!(plugin_setting_name)
UpcomingChanges.clear_caches!
end
it "returns true rather than recursing through Plugin::Instance#enabled?" do
expect(described_class.owning_plugin_enabled?(plugin_setting_name)).to eq(true)
end
it "keeps the change displayed while the plugin is off, since that row is how admins opt in" do
expect(UpcomingChanges::ConditionalDisplay.should_display?(plugin_setting_name)).to eq(true)
end
it "still resolves the change normally" do
expect(described_class.enabled?(plugin_setting_name)).to eq(false)
SiteSetting.enable_experimental_sample_plugin_feature = true
expect(described_class.enabled?(plugin_setting_name)).to eq(true)
end
end
end
describe ".enabled?" do
after do
SiteSetting.remove_override!(setting_name)
SiteSetting.promote_upcoming_changes_on_status = :stable
UpcomingChanges.clear_caches!
end
context "when the owning plugin is not configurable" do
let(:plugin_setting_name) { :enable_experimental_sample_plugin_feature }
before { SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(false) }
it "returns false even when the change has been promoted" do
SiteSetting.promote_upcoming_changes_on_status = :alpha
expect(described_class.enabled?(plugin_setting_name)).to eq(false)
end
it "returns false even when the change is permanent" do
mock_upcoming_change_metadata(
{ enable_experimental_sample_plugin_feature: { status: :permanent } },
)
expect(described_class.enabled?(plugin_setting_name)).to eq(false)
end
it "returns true again once the plugin becomes configurable" do
SiteSetting.promote_upcoming_changes_on_status = :alpha
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(true)
expect(described_class.enabled?(plugin_setting_name)).to eq(true)
end
end
context "when the owning plugin is disabled" do
let(:plugin_setting_name) { :enable_experimental_sample_plugin_feature }
after do
SiteSetting.remove_override!(plugin_setting_name)
UpcomingChanges.clear_caches!
end
before { SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:enabled?).returns(false) }
it "returns true when the change opts out with requires_plugin_enabled: false" do
mock_opted_out_plugin_change
SiteSetting.enable_experimental_sample_plugin_feature = true
expect(described_class.enabled?(plugin_setting_name)).to eq(true)
end
it "returns false even when the admin has opted in" do
SiteSetting.enable_experimental_sample_plugin_feature = true
expect(described_class.enabled?(plugin_setting_name)).to eq(false)
end
it "returns false even when the change has been promoted" do
SiteSetting.promote_upcoming_changes_on_status = :alpha
expect(described_class.enabled?(plugin_setting_name)).to eq(false)
end
it "returns false even when the change is permanent" do
mock_plugin_change(status: :permanent)
expect(described_class.enabled?(plugin_setting_name)).to eq(false)
end
it "keeps the admin's opt-in, so the change resolves again once the plugin is enabled" do
SiteSetting.enable_experimental_sample_plugin_feature = true
expect(described_class.enabled?(plugin_setting_name)).to eq(false)
SiteSetting::SAMPLE_TEST_PLUGIN.unstub(:enabled?)
expect(described_class.enabled?(plugin_setting_name)).to eq(true)
end
end
context "when the change is not registered" do
it "raises ArgumentError" do
expect { described_class.enabled?(:not_an_upcoming_change) }.to raise_error(
ArgumentError,
/Unknown upcoming change/,
)
end
end
context "when the setting has no row in the database (admin has not saved it)" do
before { SiteSetting.remove_override!(setting_name) }
it "returns the yaml default when the change is below promote_upcoming_changes_on_status" do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :experimental,
impact_type: "other",
impact_role: "developers",
},
},
)
SiteSetting.promote_upcoming_changes_on_status = :stable
expect(described_class.enabled?(setting_name)).to eq(SiteSetting.defaults[setting_name])
end
it "returns true when the change meets or exceeds promote_upcoming_changes_on_status" do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :stable,
impact_type: "other",
impact_role: "developers",
},
},
)
SiteSetting.promote_upcoming_changes_on_status = :stable
expect(described_class.enabled?(setting_name)).to eq(true)
end
end
context "when an admin has saved a value to the database" do
it "returns the stored value when true" do
SiteSetting.enable_upload_debug_mode = true
expect(described_class.enabled?(setting_name)).to eq(true)
end
it "returns the stored value when false even when the change meets promote_upcoming_changes_on_status" do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :beta,
impact_type: "other",
impact_role: "developers",
},
},
)
SiteSetting.promote_upcoming_changes_on_status = :beta
SiteSetting.enable_upload_debug_mode = false
expect(described_class.enabled?(setting_name)).to eq(false)
end
end
context "when the change is permanent" do
before do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :permanent,
impact_type: "other",
impact_role: "developers",
},
},
)
end
it "returns true even when the database value is false" do
SiteSetting.enable_upload_debug_mode = false
expect(described_class.enabled?(setting_name)).to eq(true)
end
end
end
# Models the self-hoster upgrade that lowers the default
# promote_upcoming_changes_on_status from :stable to :beta. A beta change
# that previously sat below the promotion threshold now meets it, so we must
# be sure the transition only auto-promotes changes the admin never touched
# and never overrides an admin's explicit opt-in/opt-out. The opt-out case is
# the critical one: the stored value equals the YAML default (false), and it
# only survives because setting_modified_from_default? treats upcoming change
# settings as modified whenever a DB row exists (see SiteSettingExtension#refresh!).
describe "lowering promote_upcoming_changes_on_status from :stable to :beta" do
before do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :beta,
impact_type: "other",
impact_role: "developers",
},
},
)
SiteSetting.promote_upcoming_changes_on_status = :stable
UpcomingChanges.clear_caches!
end
after do
SiteSetting.remove_override!(setting_name)
SiteSetting.promote_upcoming_changes_on_status = :stable
UpcomingChanges.clear_caches!
end
it "auto-promotes a change the admin never touched" do
expect(described_class.enabled?(setting_name)).to eq(false)
SiteSetting.promote_upcoming_changes_on_status = :beta
expect(described_class.enabled?(setting_name)).to eq(true)
end
it "keeps a change the admin explicitly opted out of disabled" do
SiteSetting.enable_upload_debug_mode = false
expect(described_class.enabled?(setting_name)).to eq(false)
SiteSetting.promote_upcoming_changes_on_status = :beta
expect(described_class.enabled?(setting_name)).to eq(false)
end
it "keeps a change the admin explicitly opted into enabled" do
SiteSetting.enable_upload_debug_mode = true
expect(described_class.enabled?(setting_name)).to eq(true)
SiteSetting.promote_upcoming_changes_on_status = :beta
expect(described_class.enabled?(setting_name)).to eq(true)
end
end
describe ".change_dependencies_met?" do
it "returns true for a change with no dependencies" do
expect(described_class.change_dependencies_met?(:enable_upload_debug_mode)).to eq(true)
end
it "returns false when a boolean dependency is disabled" do
SiteSetting.allow_user_locale = false
expect(described_class.change_dependencies_met?(:set_locale_from_cookie)).to eq(false)
end
it "returns true when all boolean dependencies are enabled" do
SiteSetting.allow_user_locale = true
expect(described_class.change_dependencies_met?(:set_locale_from_cookie)).to eq(true)
end
context "with depends_on_values for a non-boolean dependency" do
before do
SiteSetting
.type_supervisor
.dependencies
.stubs(:[])
.with(:fake_change)
.returns([:desktop_category_page_style])
SiteSetting.stubs(:dependency_values).returns(
{ fake_change: { desktop_category_page_style: %w[categories_only] } },
)
end
it "returns true when the dependency matches an allowed value" do
SiteSetting.desktop_category_page_style = "categories_only"
expect(described_class.change_dependencies_met?(:fake_change)).to eq(true)
end
it "returns false when the dependency does not match an allowed value" do
SiteSetting.desktop_category_page_style = "categories_and_latest_topics"
expect(described_class.change_dependencies_met?(:fake_change)).to eq(false)
end
end
end
describe ".settings_hidden_while_enabled" do
# `enable_upload_debug_mode` stands in for the change; the two real settings
# below stand in for the legacy settings it would hide.
let(:hidden_setting_names) { %i[allow_uncategorized_topics suppress_uncategorized_badge] }
before do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :experimental,
impact_type: "other",
impact_role: "developers",
hide_settings: hidden_setting_names,
},
},
)
end
after do
SiteSetting.remove_override!(setting_name)
UpcomingChanges.clear_caches!
end
it "returns nothing when the change is not enabled" do
SiteSetting.remove_override!(setting_name)
expect(described_class.settings_hidden_while_enabled).to be_empty
end
it "returns the declared settings when the change is enabled" do
SiteSetting.enable_upload_debug_mode = true
expect(described_class.settings_hidden_while_enabled).to contain_exactly(
*hidden_setting_names,
)
end
it "ignores changes that do not declare hide_settings" do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :experimental,
impact_type: "other",
impact_role: "developers",
},
},
)
SiteSetting.enable_upload_debug_mode = true
expect(described_class.settings_hidden_while_enabled).to be_empty
end
context "when the change is owned by a plugin the admin has opted into and then disabled" do
let(:plugin_setting_name) { :enable_experimental_sample_plugin_feature }
before do
mock_plugin_change(hide_settings: hidden_setting_names)
SiteSetting.enable_experimental_sample_plugin_feature = true
end
after do
SiteSetting.remove_override!(plugin_setting_name)
UpcomingChanges.clear_caches!
end
it "hides the declared settings while the plugin is enabled" do
expect(described_class.settings_hidden_while_enabled).to contain_exactly(
*hidden_setting_names,
)
end
it "stops hiding them once the plugin is disabled, so the change leaves nothing behind" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:enabled?).returns(false)
expect(described_class.settings_hidden_while_enabled).to be_empty
end
end
it "feeds SiteSetting.hidden_settings so the settings are hidden while enabled" do
expect(SiteSetting.hidden_settings).not_to include(*hidden_setting_names)
SiteSetting.enable_upload_debug_mode = true
expect(SiteSetting.hidden_settings).to include(*hidden_setting_names)
end
end
describe ".enabled_for_with_groups" do
let(:setting_name) { :enable_upload_debug_mode }
let(:groups_hash) { { Group::AUTO_GROUPS[:staff] => "staff" } }
def mock_allow(allow)
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :experimental,
impact_type: "other",
impact_role: "developers",
allow_enabled_for: allow,
},
},
)
end
context "when the setting is disabled" do
it "returns no_one" do
result = described_class.enabled_for_with_groups(setting_name, false, groups_hash)
expect(result[:enabled_for]).to eq("no_one")
end
end
context "when the setting is enabled with no admin-configured groups" do
context "when allow_enabled_for is omitted" do
it "returns everyone" do
result = described_class.enabled_for_with_groups(setting_name, true, groups_hash)
expect(result[:enabled_for]).to eq("everyone")
end
end
context "when allow_enabled_for is [everyone]" do
before { mock_allow([:everyone]) }
it "returns everyone" do
result = described_class.enabled_for_with_groups(setting_name, true, groups_hash)
expect(result[:enabled_for]).to eq("everyone")
end
end
context "when allow_enabled_for is [staff, specific_groups]" do
before { mock_allow(%i[staff specific_groups]) }
it "returns the staff group name as the broadest allowed display target" do
result = described_class.enabled_for_with_groups(setting_name, true, groups_hash)
expect(result[:enabled_for]).to eq("staff")
end
end
context "when allow_enabled_for is [staff]" do
before { mock_allow([:staff]) }
it "returns the staff group name" do
result = described_class.enabled_for_with_groups(setting_name, true, groups_hash)
expect(result[:enabled_for]).to eq("staff")
end
end
context "when allow_enabled_for is [specific_groups]" do
before { mock_allow([:specific_groups]) }
it "returns groups" do
result = described_class.enabled_for_with_groups(setting_name, true, groups_hash)
expect(result[:enabled_for]).to eq("groups")
end
end
end
end
describe ".current_statuses" do
include ActiveSupport::Testing::TimeHelpers
before { described_class.clear_caches! }
after do
described_class.clear_caches!
UpcomingChangeEvent.where(upcoming_change_name: "timeline_status_setting").delete_all
end
it "returns an empty hash when there are no status_changed events" do
expect(described_class.current_statuses).to eq({})
end
it "maps each upcoming change to the latest status_changed new_value and timestamp" do
travel_to Time.zone.parse("2024-06-01 12:00:00") do
UpcomingChangeEvent.create!(
event_type: :status_changed,
upcoming_change_name: "timeline_status_setting",
event_data: {
"previous_value" => "alpha",
"new_value" => "beta",
},
)
end
latest_event =
travel_to(Time.zone.parse("2024-06-15 12:00:00")) do
UpcomingChangeEvent.create!(
event_type: :status_changed,
upcoming_change_name: "timeline_status_setting",
event_data: {
"previous_value" => "beta",
"new_value" => "stable",
},
)
end
result = described_class.current_statuses
expect(result["timeline_status_setting"]).to eq(
{ status: "stable", changed_at: latest_event.created_at },
)
end
it "caches the result so the SQL runs only once until the cache key is deleted" do
UpcomingChangeEvent.create!(
event_type: :status_changed,
upcoming_change_name: "timeline_status_setting",
event_data: {
"previous_value" => nil,
"new_value" => "experimental",
},
)
allow(DB).to receive(:query).and_call_original
2.times { described_class.current_statuses }
expect(DB).to have_received(:query).once
described_class.clear_caches!
described_class.current_statuses
expect(DB).to have_received(:query).twice
end
end
describe ".permanent_upcoming_changes" do
before do
described_class.clear_caches!
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :permanent,
impact_type: "other",
impact_role: "developers",
},
},
)
end
after { described_class.clear_caches! }
it "returns only changes whose metadata status is permanent" do
list = described_class.permanent_upcoming_changes
expect(list.all? { |c| described_class.change_status(c[:setting]) == :permanent }).to eq(true)
expect(list.map { |c| c[:setting] }).to include(:enable_upload_debug_mode)
end
it "caches the list so UpcomingChanges::List runs only once until the cache key is deleted" do
allow(UpcomingChanges::List).to receive(:call).and_call_original
2.times { described_class.permanent_upcoming_changes }
expect(UpcomingChanges::List).to have_received(:call).once
described_class.clear_caches!
described_class.permanent_upcoming_changes
expect(UpcomingChanges::List).to have_received(:call).twice
end
end
describe ".clear_caches!" do
it "clears the latest new feature created_at cache" do
Discourse.redis.set("latest_new_feature_created_at", Time.zone.now.iso8601)
described_class.clear_caches!
expect(Discourse.redis.get("latest_new_feature_created_at")).to be_nil
end
end
describe ".enabled_for_user?" do
context "for logged-in user" do
fab!(:user)
context "when the upcoming change is disabled" do
before { SiteSetting.enable_upload_debug_mode = false }
it "returns false" do
expect(UpcomingChanges.enabled_for_user?(setting_name, user)).to eq(false)
end
end
context "when the upcoming change is enabled for everyone" do
before { SiteSetting.enable_upload_debug_mode = true }
it "returns true" do
expect(UpcomingChanges.enabled_for_user?(setting_name, user)).to eq(true)
end
end
context "when the upcoming change is only enabled for certain groups" do
before do
SiteSetting.enable_upload_debug_mode = true
Fabricate(
:site_setting_group,
name: setting_name,
group_ids: Group::AUTO_GROUPS[:trust_level_4].to_s,
)
end
it "returns false" do
expect(UpcomingChanges.enabled_for_user?(setting_name, user)).to eq(false)
end
context "when the user is in that group" do
before do
trust_level_4_group = Group.find_by(id: Group::AUTO_GROUPS[:trust_level_4])
trust_level_4_group.add(user)
end
it "returns true" do
expect(UpcomingChanges.enabled_for_user?(setting_name, user)).to eq(true)
end
end
end
end
context "for anonymous user" do
let(:user) { nil }
context "when the upcoming change is disabled" do
before { SiteSetting.enable_upload_debug_mode = false }
it "returns false" do
expect(UpcomingChanges.enabled_for_user?(setting_name, user)).to eq(false)
end
end
context "when the upcoming change is enabled for everyone" do
before { SiteSetting.enable_upload_debug_mode = true }
it "returns true" do
expect(UpcomingChanges.enabled_for_user?(setting_name, user)).to eq(true)
end
end
context "when the upcoming change is only enabled for certain groups" do
before do
SiteSetting.enable_upload_debug_mode = true
Fabricate(
:site_setting_group,
name: setting_name,
group_ids: Group::AUTO_GROUPS[:trust_level_4].to_s,
)
end
it "returns false" do
expect(UpcomingChanges.enabled_for_user?(setting_name, user)).to eq(false)
end
end
end
end
describe "conceptual status filtering" do
it "excludes conceptual changes from all_settings with only_upcoming_changes" do
settings =
SiteSetting
.all_settings(only_upcoming_changes: true, include_hidden: true)
.map { |s| s[:setting] }
expect(settings).not_to include(:conceptual_setting)
expect(settings).to include(:enable_upload_debug_mode)
end
end
describe "conditional display" do
after do
DiscoursePluginRegistry.reset_register!(:upcoming_change_conditional_display_callbacks)
end
it "returns true when the conditional display method is undefined for an upcoming change" do
expect(UpcomingChanges::ConditionalDisplay.should_display?(:enable_upload_debug_mode)).to eq(
true,
)
end
it "returns true when the registered callback returns true" do
Plugin::Instance
.new
.register_upcoming_change_conditional_display(:enable_upload_debug_mode) { true }
expect(UpcomingChanges::ConditionalDisplay.should_display?(:enable_upload_debug_mode)).to eq(
true,
)
end
it "returns false when the registered callback returns false" do
Plugin::Instance
.new
.register_upcoming_change_conditional_display(:enable_upload_debug_mode) { false }
expect(UpcomingChanges::ConditionalDisplay.should_display?(:enable_upload_debug_mode)).to eq(
false,
)
end
it "returns false when any registered callback returns false" do
Plugin::Instance
.new
.register_upcoming_change_conditional_display(:enable_upload_debug_mode) { true }
Plugin::Instance
.new
.register_upcoming_change_conditional_display(:enable_upload_debug_mode) { false }
expect(UpcomingChanges::ConditionalDisplay.should_display?(:enable_upload_debug_mode)).to eq(
false,
)
end
it "ignores callbacks from disabled plugins" do
plugin = Plugin::Instance.new
plugin.stubs(:enabled?).returns(false)
plugin.register_upcoming_change_conditional_display(:enable_upload_debug_mode) { false }
expect(UpcomingChanges::ConditionalDisplay.should_display?(:enable_upload_debug_mode)).to eq(
true,
)
end
context "when the owning plugin is disabled" do
let(:plugin_setting_name) { :enable_experimental_sample_plugin_feature }
before { SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:enabled?).returns(false) }
after { UpcomingChanges.clear_caches! }
it "hides the change by default" do
expect(UpcomingChanges::ConditionalDisplay.should_display?(plugin_setting_name)).to eq(
false,
)
end
it "hides the change even when the plugin registered a conditional display callback" do
SiteSetting::SAMPLE_TEST_PLUGIN.register_upcoming_change_conditional_display(
plugin_setting_name,
) { true }
expect(UpcomingChanges::ConditionalDisplay.should_display?(plugin_setting_name)).to eq(
false,
)
end
context "when the change opts out with requires_plugin_enabled: false" do
before { mock_opted_out_plugin_change }
it "still displays the change" do
expect(UpcomingChanges::ConditionalDisplay.should_display?(plugin_setting_name)).to eq(
true,
)
end
end
end
context "when the owning plugin is not configurable" do
let(:plugin_setting_name) { :enable_experimental_sample_plugin_feature }
it "returns false" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(false)
expect(UpcomingChanges::ConditionalDisplay.should_display?(plugin_setting_name)).to eq(
false,
)
end
it "returns false without consulting the change's conditional display method" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(false)
UpcomingChanges::ConditionalDisplay.define_singleton_method(
:should_display_enable_experimental_sample_plugin_feature?,
) { raise "should not be called" }
begin
expect(UpcomingChanges::ConditionalDisplay.should_display?(plugin_setting_name)).to eq(
false,
)
ensure
UpcomingChanges::ConditionalDisplay.singleton_class.send(
:remove_method,
:should_display_enable_experimental_sample_plugin_feature?,
)
end
end
end
context "when the conditional display method is defined for an upcoming change" do
context "when the conditional display method returns true" do
before do
UpcomingChanges::ConditionalDisplay.define_singleton_method(
:should_display_enable_upload_debug_mode?,
) { true }
end
after do
UpcomingChanges::ConditionalDisplay.singleton_class.send(
:remove_method,
:should_display_enable_upload_debug_mode?,
)
end
it "returns true" do
expect(
UpcomingChanges::ConditionalDisplay.should_display?(:enable_upload_debug_mode),
).to eq(true)
end
it "takes precedence over registered callbacks" do
Plugin::Instance
.new
.register_upcoming_change_conditional_display(:enable_upload_debug_mode) { false }
expect(
UpcomingChanges::ConditionalDisplay.should_display?(:enable_upload_debug_mode),
).to eq(true)
end
end
context "when the conditional display method returns false" do
before do
UpcomingChanges::ConditionalDisplay.define_singleton_method(
:should_display_enable_upload_debug_mode?,
) { false }
end
after do
UpcomingChanges::ConditionalDisplay.singleton_class.send(
:remove_method,
:should_display_enable_upload_debug_mode?,
)
end
it "returns false" do
expect(
UpcomingChanges::ConditionalDisplay.should_display?(:enable_upload_debug_mode),
).to eq(false)
end
end
end
describe ".should_display_enable_local_logins_via_code?" do
it "returns true when local logins via email are possible" do
expect(
UpcomingChanges::ConditionalDisplay.should_display?(:enable_local_logins_via_code),
).to eq(true)
end
it "returns false when DiscourseConnect is enabled" do
SiteSetting.discourse_connect_url = "https://www.example.com/sso"
SiteSetting.discourse_connect_secret = "x" * 10
SiteSetting.enable_discourse_connect = true
expect(
UpcomingChanges::ConditionalDisplay.should_display?(:enable_local_logins_via_code),
).to eq(false)
end
it "returns false when local logins via email are disabled" do
SiteSetting.enable_local_logins_via_email = false
expect(
UpcomingChanges::ConditionalDisplay.should_display?(:enable_local_logins_via_code),
).to eq(false)
end
it "stays displayed when the change is already enabled even if email login is later disabled" do
SiteSetting.enable_local_logins_via_code = true
SiteSetting.enable_local_logins_via_email = false
expect(
UpcomingChanges::ConditionalDisplay.should_display?(:enable_local_logins_via_code),
).to eq(true)
end
end
describe ".should_display_update_pending_users_reminder_default?" do
it "returns false when must_approve_users is disabled" do
expect(
UpcomingChanges::ConditionalDisplay.should_display?(
:update_pending_users_reminder_default,
),
).to eq(false)
end
it "returns true when must_approve_users is enabled" do
SiteSetting.must_approve_users = true
expect(
UpcomingChanges::ConditionalDisplay.should_display?(
:update_pending_users_reminder_default,
),
).to eq(true)
end
it "returns false even when the change is enabled, since it has no effect without must_approve_users" do
SiteSetting.update_pending_users_reminder_default = true
expect(
UpcomingChanges::ConditionalDisplay.should_display?(
:update_pending_users_reminder_default,
),
).to eq(false)
end
end
end
end