0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/spec/lib/site_setting_extension_spec.rb
Régis Hanol 3e012af58e
FIX: Scope duplicate topic title check to what the user can see (#41871)
Previously, the duplicate topic title check compared new titles against
every topic on the site regardless of visibility: users were blocked by
titles in categories they couldn't see or unlisted topics they couldn't
find, the bare "Title has already been used" error gave no way to locate
the conflict (and doubled as an existence oracle for hidden titles), and
the behavior was controlled by two entangled boolean settings.

This change scopes the check to the destination category plus whatever
the acting user can actually see, links the conflicting topic in the
error — safe by construction, since being blocked now implies being able
to see it:

> This title has already been used by [another topic]().

It also consolidates the two booleans into a single
`duplicate_topic_titles` enum (`disallowed` /
`allowed_across_categories` / `allowed`), with existing values migrated
and the old names kept as hidden deprecated aliases that admin search
still resolves.

Reported in
https://meta.discourse.org/t/title-has-already-been-used-in-a-secure-category/123047

Note for self-hosters: env-provided settings can't be migrated —
`DISCOURSE_ALLOW_DUPLICATE_TOPIC_TITLES=true` configs need to switch to
`DISCOURSE_DUPLICATE_TOPIC_TITLES=allowed`.
2026-07-24 15:35:35 +02:00

2360 lines
78 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe SiteSettingExtension do
# We disable message bus here to avoid a large amount
# of unneeded messaging, tests are careful to call refresh
# when they need to.
#
# DistributedCache used by locale handler can under certain
# cases take a tiny bit to stabilize.
#
# TODO: refactor SiteSettingExtension not to rely on statics in
# DefaultsProvider
#
before { MessageBus.off }
after { MessageBus.on }
describe "#types" do
context "when verifying enum sequence" do
before { @types = SiteSetting.types }
it "'string' should be at 1st position" do
expect(@types[:string]).to eq(1)
end
it "'value_list' should be at 12th position" do
expect(@types[:value_list]).to eq(12)
end
end
end
let :provider_local do
SiteSettings::LocalProcessProvider.new
end
let :settings do
new_settings(provider_local)
end
let :settings2 do
new_settings(provider_local)
end
it "does not leak state cause changes are not linked" do
t1 =
Thread.new do
5.times do
settings = new_settings(SiteSettings::LocalProcessProvider.new)
settings.setting(:title, "test")
settings.title = "title1"
expect(settings.title).to eq "title1"
end
end
t2 =
Thread.new do
5.times do
settings = new_settings(SiteSettings::LocalProcessProvider.new)
settings.setting(:title, "test")
settings.title = "title2"
expect(settings.title).to eq "title2"
end
end
t1.join
t2.join
end
describe ".refresh!" do
it "ensures that the right MessageBus subscription has been set up" do
settings.expects(:ensure_listen_for_changes).once
settings.refresh!
end
it "will reset to default if provider vanishes" do
settings.setting(:hello, 1)
settings.hello = 100
expect(settings.hello).to eq(100)
settings.provider.clear
settings.refresh!
expect(settings.hello).to eq(1)
end
it "will set to new value if provider changes" do
settings.setting(:hello, 1)
settings.hello = 100
expect(settings.hello).to eq(100)
settings.provider.save(:hello, 99, SiteSetting.types[:integer])
settings.refresh!
expect(settings.hello).to eq(99)
end
it "picks up changes from provider on refresh across processes" do
settings.setting(:hello, 1)
settings2.setting(:hello, 1)
settings.hello = 100
settings2.refresh!
expect(settings2.hello).to eq(100)
settings.hello = 99
settings2.refresh!
expect(settings2.hello).to eq(99)
end
it "does not override types in the type supervisor" do
settings.setting(:foo, "bar")
settings.provider.save(:foo, "bar", SiteSetting.types[:enum])
settings.refresh!
expect(settings.foo).to eq("bar")
settings.foo = "baz"
expect(settings.foo).to eq("baz")
end
it "clears the cache for site setting uploads" do
settings.setting(:upload_type, "", type: :upload)
upload = Fabricate(:upload)
settings.upload_type = upload
expect(settings.upload_type).to eq(upload)
expect(settings.send(:uploads)[:upload_type]).to eq(upload)
upload2 = Fabricate(:upload)
settings.provider.save(:upload_type, upload2.id, SiteSetting.types[:upload])
expect do settings.refresh! end.to change { settings.send(:uploads)[:upload_type] }.from(
upload,
).to(nil)
expect(settings.upload_type).to eq(upload2)
end
it "refreshes the client_settings_json cache" do
upload = Fabricate(:upload)
settings.setting(:upload_type, upload.id.to_s, type: :upload, client: true)
settings.setting(:string_type, "haha", client: true)
settings.refresh!
expect(settings.client_settings_json).to eq(
%Q|{"default_locale":"#{SiteSetting.default_locale}","upload_type":"#{upload.url}","string_type":"haha"}|,
)
upload.update!(url: "a_new_url")
settings.string_type = "changed"
settings.refresh!
expect(settings.client_settings_json).to eq(
%Q|{"default_locale":"#{SiteSetting.default_locale}","upload_type":"a_new_url","string_type":"changed"}|,
)
end
context "when the provider value equals the YAML default" do
it "does not mark a normal setting as modified from default" do
settings.setting(:hello, 1)
settings.provider.save(:hello, 1, SiteSetting.types[:integer])
settings.refresh!
expect(settings.setting_modified_from_default?(:hello)).to eq(false)
end
it "still marks an upcoming change setting as modified from default so admins can opt out" do
settings.setting(
:upcoming_change_opt_out_flag,
false,
upcoming_change: {
status: :experimental,
impact: "feature,staff",
},
)
UpcomingChanges.stubs(:settings_provider).returns(settings)
settings.provider.save(:upcoming_change_opt_out_flag, false, SiteSetting.types[:bool])
settings.refresh!
expect(settings.setting_modified_from_default?(:upcoming_change_opt_out_flag)).to eq(true)
end
end
end
describe ".after_fork" do
it "refreshes the site settings" do
settings.setting(:hello, 1)
settings.hello = 100
settings.provider.save(:hello, 200, SiteSetting.types[:integer])
settings.after_fork
expect(settings.hello).to eq(200)
end
end
describe "DiscourseEvent" do
before do
settings.setting(:test_setting, 1)
settings.refresh!
end
it "triggers events correctly" do
settings.setting(:test_setting, 1)
settings.refresh!
override_events = DiscourseEvent.track_events { settings.test_setting = 2 }
no_change_events = DiscourseEvent.track_events { settings.test_setting = 2 }
default_events = DiscourseEvent.track_events { settings.test_setting = 1 }
expect(override_events.map { |e| e[:event_name] }).to contain_exactly(:site_setting_changed)
expect(no_change_events.map { |e| e[:event_name] }).to be_empty
expect(default_events.map { |e| e[:event_name] }).to contain_exactly(:site_setting_changed)
changed_event_1 = override_events.find { |e| e[:event_name] == :site_setting_changed }
changed_event_2 = default_events.find { |e| e[:event_name] == :site_setting_changed }
expect(changed_event_1[:params]).to eq([:test_setting, 1, 2])
expect(changed_event_2[:params]).to eq([:test_setting, 2, 1])
end
end
describe "int setting" do
before do
settings.setting(:test_setting, 77)
settings.refresh!
end
it "should have a key in all_settings" do
expect(settings.all_settings.detect { |s| s[:setting] == :test_setting }).to be_present
end
it "should have the correct desc" do
I18n.backend.store_translations(
:en,
site_settings: {
test_setting: "test description <a href='%{base_path}/admin'>/admin</a>",
},
)
expect(settings.description(:test_setting)).to eq(
"test description <a href='/admin'>/admin</a>",
)
Discourse.stubs(:base_path).returns("/forum")
expect(settings.description(:test_setting)).to eq(
"test description <a href='/forum/admin'>/admin</a>",
)
end
it "should have the correct default" do
expect(settings.test_setting).to eq(77)
end
context "when overridden" do
after :each do
settings.remove_override!(:test_setting)
end
it "should have the correct override" do
settings.test_setting = 100
expect(settings.test_setting).to eq(100)
end
it "should coerce correct string to int" do
settings.test_setting = "101"
expect(settings.test_setting).to eq(101)
end
it "should coerce incorrect string to 0" do
settings.test_setting = "pie"
expect(settings.test_setting).to eq(0)
end
it "should not set default when reset" do
settings.test_setting = 100
settings.setting(:test_setting, 77)
settings.refresh!
expect(settings.test_setting).not_to eq(77)
end
it "can be overridden with set" do
settings.set("test_setting", 12)
expect(settings.test_setting).to eq(12)
end
it "should publish changes to clients" do
settings.setting("test_setting", 100)
settings.setting("test_setting", nil, client: true)
message = MessageBus.track_publish("/client_settings") { settings.test_setting = 88 }.first
expect(message).to be_present
end
end
end
describe "remove_override" do
fab!(:upload)
before do
settings.setting(:test_override, "test")
settings.setting(:image_list_test, "", type: :uploaded_image_list)
settings.refresh!
end
it "correctly nukes overrides" do
settings.test_override = "bla"
settings.remove_override!(:test_override)
expect(settings.test_override).to eq("test")
end
it "correctly nukes overrides for image list type setting" do
settings.image_list_test = "#{upload.id}"
settings.remove_override!(:image_list_test)
expect(settings.image_list_test).to be_empty
end
end
describe "string setting" do
before do
settings.setting(:test_str, "str")
settings.refresh!
end
it "should have the correct default" do
expect(settings.test_str).to eq("str")
end
context "when overridden" do
after :each do
settings.remove_override!(:test_str)
end
it "should coerce int to string" do
settings.test_str = 100
expect(settings.test_str).to eq("100")
end
it "can be overridden with set" do
settings.set("test_str", "hi")
expect(settings.test_str).to eq("hi")
end
end
end
describe "string setting with regex" do
it "Supports custom validation errors" do
I18n.backend.store_translations(:en, { oops: "oops" })
settings.setting(:test_str, "bob", regex: "hi", regex_error: "oops")
settings.refresh!
begin
settings.test_str = "a"
rescue Discourse::InvalidParameters => e
message = e.message
end
expect(message).to match(/oops/)
end
end
describe "bool setting" do
before do
settings.setting(:test_hello?, false)
settings.refresh!
end
it "should have the correct default" do
expect(settings.test_hello?).to eq(false)
end
context "when overridden" do
after { settings.remove_override!(:test_hello?) }
it "should have the correct override" do
settings.test_hello = true
expect(settings.test_hello?).to eq(true)
end
it "should coerce true strings to true" do
settings.test_hello = "true"
expect(settings.test_hello?).to be(true)
end
it "should coerce all other strings to false" do
settings.test_hello = "f"
expect(settings.test_hello?).to be(false)
end
it "should not set default when reset" do
settings.test_hello = true
settings.setting(:test_hello?, false)
settings.refresh!
expect(settings.test_hello?).not_to eq(false)
end
it "can be overridden with set" do
settings.set("test_hello", true)
expect(settings.test_hello?).to eq(true)
end
end
end
describe "int enum" do
class TestIntEnumClass
def self.valid_value?(v)
true
end
def self.values
[1, 2, 3]
end
end
it "should coerce correctly" do
settings.setting(:test_int_enum, 1, enum: TestIntEnumClass)
settings.test_int_enum = "2"
settings.refresh!
expect(settings.defaults[:test_int_enum]).to eq(1)
expect(settings.test_int_enum).to eq(2)
end
end
describe "enum setting" do
class TestEnumClass
def self.valid_value?(v)
values.include?(v)
end
def self.values
["en"]
end
def self.translate_names?
false
end
end
let(:test_enum_class) { TestEnumClass }
before do
settings.setting(:test_enum, "en", enum: test_enum_class)
settings.refresh!
end
it "should have the correct default" do
expect(settings.test_enum).to eq("en")
end
it "should not hose all_settings" do
expect(settings.all_settings.detect { |s| s[:setting] == :test_enum }).to be_present
end
it "should report error when being set other values" do
expect { settings.test_enum = "not_in_enum" }.to raise_error(Discourse::InvalidParameters)
end
context "when overridden" do
after :each do
settings.remove_override!(:validated_setting)
end
it "stores valid values" do
test_enum_class.expects(:valid_value?).with("fr").returns(true)
settings.test_enum = "fr"
expect(settings.test_enum).to eq("fr")
end
it "rejects invalid values" do
test_enum_class.expects(:valid_value?).with("gg").returns(false)
expect { settings.test_enum = "gg" }.to raise_error(Discourse::InvalidParameters)
end
end
end
describe "a setting with a category" do
before do
settings.setting(:test_setting, 88, category: :tests)
settings.refresh!
end
it "should return the category in all_settings" do
expect(settings.all_settings.find { |s| s[:setting] == :test_setting }[:category]).to eq(
:tests,
)
end
context "when overridden" do
after :each do
settings.remove_override!(:test_setting)
end
it "should have the correct override" do
settings.test_setting = 101
expect(settings.test_setting).to eq(101)
end
it "should still have the correct category" do
settings.test_setting = 102
expect(settings.all_settings.find { |s| s[:setting] == :test_setting }[:category]).to eq(
:tests,
)
end
end
end
describe "a setting with an area" do
before do
settings.setting(:test_setting, 88, area: "flags")
settings.setting(:test_setting2, 89, area: "flags")
settings.setting(:test_setting4, 90)
settings.refresh!
end
it "should allow to filter by area" do
expect(settings.all_settings(filter_area: "flags").map { |s| s[:setting].to_sym }).to eq(
%i[default_locale test_setting test_setting2],
)
end
it "raised an error when area is invalid" do
expect {
settings.setting(:test_setting, 89, area: "invalid")
settings.refresh!
}.to raise_error(Discourse::InvalidParameters)
end
it "allows plugin to register valid areas" do
plugin = Plugin::Instance.new nil, "/tmp/test.rb"
plugin.register_site_setting_area("plugin_area")
settings.setting(:test_plugin_setting, 88, area: "plugin_area")
expect(
settings
.all_settings(filter_area: "plugin_area", include_locale_setting: false)
.map { |s| s[:setting].to_sym },
).to eq(%i[test_plugin_setting])
end
end
describe "setting with a validator" do
before do
settings.setting(:validated_setting, "info@example.com", type: "email")
settings.refresh!
end
after :each do
settings.remove_override!(:validated_setting)
end
it "stores valid values" do
EmailSettingValidator.any_instance.expects(:valid_value?).returns(true)
settings.validated_setting = "success@example.com"
expect(settings.validated_setting).to eq("success@example.com")
end
it "rejects invalid values" do
expect {
EmailSettingValidator.any_instance.expects(:valid_value?).returns(false)
settings.validated_setting = "nope"
}.to raise_error(Discourse::InvalidParameters)
expect(settings.validated_setting).to eq("info@example.com")
end
it "allows blank values" do
settings.validated_setting = ""
expect(settings.validated_setting).to eq("")
end
end
describe "datetime setting" do
before do
settings.setting(:datetime_setting, "2024-01-01T00:00:00Z", type: "datetime")
settings.refresh!
end
after :each do
settings.remove_override!(:datetime_setting)
end
it "stores valid datetime values" do
settings.datetime_setting = "2024-12-29T15:30:00Z"
expect(settings.datetime_setting).to eq("2024-12-29T15:30:00Z")
end
it "stores valid datetime values with timezone offset" do
settings.datetime_setting = "2024-12-29T15:30:00+05:30"
expect(settings.datetime_setting).to eq("2024-12-29T15:30:00+05:30")
end
it "stores valid datetime values with milliseconds" do
settings.datetime_setting = "2024-12-29T15:30:00.123Z"
expect(settings.datetime_setting).to eq("2024-12-29T15:30:00.123Z")
end
it "rejects date-only strings" do
expect { settings.datetime_setting = "2024-12-29" }.to raise_error(
Discourse::InvalidParameters,
)
end
it "rejects datetime without timezone" do
expect { settings.datetime_setting = "2024-12-29T15:30:00" }.to raise_error(
Discourse::InvalidParameters,
)
end
it "rejects invalid datetime strings" do
expect { settings.datetime_setting = "not a datetime" }.to raise_error(
Discourse::InvalidParameters,
)
expect { settings.datetime_setting = "2024-13-01T15:30:00Z" }.to raise_error(
Discourse::InvalidParameters,
)
end
it "allows blank values" do
settings.datetime_setting = ""
expect(settings.datetime_setting).to eq("")
end
end
describe "set for an invalid setting name" do
it "raises an error" do
settings.setting(:test_setting, 77)
settings.refresh!
expect { settings.set("provider", "haxxed") }.to raise_error(Discourse::InvalidParameters)
end
end
describe ".get" do
before do
settings.setting(:title, "Discourse v1")
settings.refresh!
end
it "works correctly" do
expect { settings.get("frogs_in_africa") }.to raise_error(Discourse::InvalidParameters)
expect(settings.get(:title)).to eq("Discourse v1")
expect(settings.get("title")).to eq("Discourse v1")
end
end
describe ".set_and_log" do
before do
settings.setting(:s3_secret_access_key, "old_secret_key", secret: true)
settings.setting(:title, "Discourse v1")
settings.refresh!
end
it "raises an error when set for an invalid setting name" do
expect { settings.set_and_log("provider", "haxxed") }.to raise_error(
Discourse::InvalidParameters,
)
end
it "scrubs secret setting values from logs" do
settings.set_and_log("s3_secret_access_key", "new_secret_key")
expect(UserHistory.last.previous_value).to eq("[FILTERED]")
expect(UserHistory.last.new_value).to eq("[FILTERED]")
end
it "works" do
settings.set_and_log("title", "Discourse v2")
expect(settings.title).to eq("Discourse v2")
expect(UserHistory.last.previous_value).to eq("Discourse v1")
expect(UserHistory.last.new_value).to eq("Discourse v2")
end
it "does not create an entry in the staff action logs when new value is the same" do
expect { settings.set_and_log("title", "Discourse v1") }.not_to change { UserHistory.count }
end
context "when a detailed message is provided" do
let(:message) { "We really need to do this, see https://meta.discourse.org/t/123" }
it "adds the detailed message to the user history record" do
expect {
settings.set_and_log("title", "Discourse v2", Discourse.system_user, message)
}.to change { UserHistory.last.try(:details) }.to(message)
end
end
end
describe "filter domain name" do
before do
settings.setting(:allowed_spam_host_domains, "www.example.com")
settings.refresh!
end
it "filters domain" do
settings.set("allowed_spam_host_domains", "http://www.discourse.org/")
expect(settings.allowed_spam_host_domains).to eq("www.discourse.org")
end
it "returns invalid domain as is, without throwing exception" do
settings.set("allowed_spam_host_domains", "test!url")
expect(settings.allowed_spam_host_domains).to eq("test!url")
end
end
describe "dependent settings" do
context "when a dependent setting depends_behavior is not set" do
before do
settings.setting(:cool_thing_image, nil, depends_on: [:enable_cool_thing])
settings.refresh!
end
it "is present in all_settings" do
expect(settings.all_settings.find { |s| s[:setting] == :cool_thing_image }).not_to be_blank
end
end
context "when a dependent setting depends_behavior is hidden" do
before do
settings.setting(
:cool_thing_image,
nil,
depends_on: [:enable_cool_thing],
depends_behavior: :hidden,
)
settings.refresh!
end
context "when the depends_on setting is an upcoming change" do
context "when the upcoming change is enabled by an admin" do
before do
settings.setting(
:enable_cool_thing,
true,
upcoming_change: {
status: :alpha,
impact: "feature,staff",
},
)
settings.refresh!
allow(UpcomingChanges).to receive(:enabled?).and_return(false)
allow(UpcomingChanges).to receive(:enabled?).with(:enable_cool_thing).and_return(true)
end
it "is present in all_settings" do
expect(
settings.all_settings.find { |s| s[:setting] == :cool_thing_image },
).not_to be_blank
end
end
context "when the upcoming change is automatically enabled because of the promotion status" do
before do
settings.setting(
:enable_cool_thing,
true,
upcoming_change: {
status: :alpha,
impact: "feature,staff",
},
)
settings.refresh!
allow(UpcomingChanges).to receive(:enabled?).and_return(false)
allow(UpcomingChanges).to receive(:enabled?).with(:enable_cool_thing).and_return(true)
end
it "is present in all_settings" do
expect(
settings.all_settings.find { |s| s[:setting] == :cool_thing_image },
).not_to be_blank
end
end
end
context "when the depends_on setting is true" do
before do
settings.setting(:enable_cool_thing, true)
settings.refresh!
end
it "is present in all_settings" do
expect(
settings.all_settings.find { |s| s[:setting] == :cool_thing_image },
).not_to be_blank
end
it "serializes depends_on and the matching humanized names" do
setting = settings.all_settings.find { |s| s[:setting] == :cool_thing_image }
expect(setting[:depends_on]).to eq([:enable_cool_thing])
expect(setting[:depends_on_humanized_names]).to eq(["Enable cool thing"])
end
end
context "when the dependent setting declares depends_on_values" do
before do
settings.setting(:cool_thing_scope, "public")
settings.setting(
:cool_thing_categories,
"",
depends_on: [:cool_thing_scope],
depends_on_values: {
cool_thing_scope: %w[include exclude],
},
depends_behavior: :hidden,
dependent_setting_display: :inline,
)
settings.refresh!
end
it "serializes the values and display mode for the dependent setting" do
setting = settings.all_settings.find { |s| s[:setting] == :cool_thing_categories }
expect(setting[:depends_on]).to eq([:cool_thing_scope])
expect(setting[:depends_on_values]).to eq(cool_thing_scope: %w[include exclude])
expect(setting[:dependent_setting_display]).to eq("inline")
end
end
context "when the depends_on setting is false" do
before do
settings.setting(:enable_cool_thing, false)
settings.refresh!
end
it "is still present in all_settings so the UI can reactively hide/disable it" do
setting = settings.all_settings.find { |s| s[:setting] == :cool_thing_image }
expect(setting).not_to be_blank
expect(setting[:depends_on]).to eq([:enable_cool_thing])
expect(setting[:depends_behavior]).to eq(:hidden)
end
end
context "when the depends_on setting does not exist" do
before do
settings.setting(
:orphan_setting,
nil,
depends_on: [:nonexistent_setting],
depends_behavior: :hidden,
)
settings.refresh!
end
it "is still present in all_settings (visibility handled client-side)" do
expect(settings.all_settings.find { |s| s[:setting] == :orphan_setting }).not_to be_blank
end
end
context "when the setting is also explicitly hidden" do
before do
settings.setting(:enable_cool_thing, true)
settings.refresh!
end
it "remains hidden even when depends_on settings are true" do
settings.setting(
:hidden_cool_thing_image,
nil,
hidden: true,
depends_on: [:enable_cool_thing],
depends_behavior: :hidden,
)
settings.refresh!
expect(settings.hidden_settings).to include(:hidden_cool_thing_image)
expect(
settings.all_settings.find { |s| s[:setting] == :hidden_cool_thing_image },
).to be_blank
end
end
end
end
describe "hidden" do
before do
settings.setting(:other_setting, "Blah")
settings.setting(:superman_identity, "Clark Kent", hidden: true)
settings.refresh!
end
after { DiscoursePluginRegistry.reset! }
it "is in the `hidden_settings` collection" do
expect(settings.hidden_settings.include?(:superman_identity)).to eq(true)
end
it "can be retrieved" do
expect(settings.superman_identity).to eq("Clark Kent")
end
it "is not present in all_settings by default" do
expect(settings.all_settings.find { |s| s[:setting] == :superman_identity }).to be_blank
end
it "is present in all_settings when we ask for hidden" do
expect(
settings.all_settings(include_hidden: true).find { |s| s[:setting] == :superman_identity },
).to be_present
end
it "does not call the hidden_site_settings plugin modifier in a loop" do
called = 0
plugin = Plugin::Instance.new
plugin.register_modifier(:hidden_site_settings) do |defaults|
called += 1
defaults + [:other_setting]
end
settings.all_settings(include_hidden: true)
expect(called).to eq(1)
end
it "calls the site_setting_result modifier for each setting" do
plugin = Plugin::Instance.new
plugin.register_modifier(:site_setting_result) do |opts|
opts[:custom_attribute] = "test_value" if opts[:setting] == :other_setting
opts
end
result = settings.all_settings
other_setting = result.find { |s| s[:setting] == :other_setting }
expect(other_setting[:custom_attribute]).to eq("test_value")
end
end
describe "global override" do
context "with default_locale" do
it "supports adding a default locale via a global" do
global_setting :default_locale, "zh_CN"
settings.default_locale = "en"
expect(settings.default_locale).to eq("zh_CN")
end
end
context "without global setting" do
before do
settings.setting(:trout_api_key, "evil")
settings.refresh!
end
it "should not add the key to the shadowed_settings collection" do
expect(settings.shadowed_settings.include?(:trout_api_key)).to eq(false)
end
it "can return the default value" do
expect(settings.trout_api_key).to eq("evil")
end
it "can overwrite the default" do
settings.trout_api_key = "tophat"
settings.refresh!
expect(settings.trout_api_key).to eq("tophat")
end
end
context "with blank global setting" do
before do
GlobalSetting.stubs(:nada).returns("")
settings.setting(:nada, "nothing")
settings.refresh!
end
it "should return default cause nothing is set" do
expect(settings.nada).to eq("nothing")
end
end
context "with a false override" do
before do
GlobalSetting.stubs(:bool).returns(false)
settings.setting(:bool, true)
settings.refresh!
end
it "should return default cause nothing is set" do
expect(settings.bool).to eq(false)
end
it "should not trigger any message bus work if you try to set it" do
m =
MessageBus.track_publish("/site_settings") do
settings.bool = true
expect(settings.bool).to eq(false)
end
expect(m.length).to eq(0)
end
end
context "with global setting" do
before do
GlobalSetting.stubs(:trout_api_key).returns("purringcat")
settings.setting(:trout_api_key, "evil")
settings.refresh!
end
it "should return the global setting instead of default" do
expect(settings.trout_api_key).to eq("purringcat")
end
it "should return the global setting after a refresh" do
settings.refresh!
expect(settings.trout_api_key).to eq("purringcat")
end
it "should add the key to the hidden_settings collection" do
expect(settings.hidden_settings.include?(:trout_api_key)).to eq(true)
["", nil].each_with_index do |setting, index|
GlobalSetting.stubs(:"trout_api_key_#{index}").returns(setting)
settings.setting(:"trout_api_key_#{index}", "evil")
settings.refresh!
expect(settings.hidden_settings.include?(:"trout_api_key_#{index}")).to eq(false)
end
end
it "should add the key to the shadowed_settings collection" do
expect(settings.shadowed_settings.include?(:trout_api_key)).to eq(true)
end
end
end
describe "secret" do
before do
settings.setting(:superman_identity, "Clark Kent", secret: true)
settings.refresh!
end
it "is in the `secret_settings` collection" do
expect(settings.secret_settings.include?(:superman_identity)).to eq(true)
end
it "can be retrieved" do
expect(settings.superman_identity).to eq("Clark Kent")
end
it "is present in all_settings by default" do
secret_setting = settings.all_settings.find { |s| s[:setting] == :superman_identity }
expect(secret_setting).to be_present
expect(secret_setting[:secret]).to eq(true)
end
end
describe "locale default overrides are respected" do
before do
settings.setting(:test_override, "default", locale_default: { zh_CN: "cn" })
settings.refresh!
end
after { settings.remove_override!(:test_override) }
it "ensures the default cache expired after overriding the default_locale" do
expect(settings.test_override).to eq("default")
settings.default_locale = "zh_CN"
expect(settings.test_override).to eq("cn")
end
it "returns the saved setting even locale default exists" do
expect(settings.test_override).to eq("default")
settings.default_locale = "zh_CN"
settings.test_override = "saved"
expect(settings.test_override).to eq("saved")
end
end
describe ".requires_refresh?" do
it "always refresh default_locale always require refresh" do
expect(settings.requires_refresh?(:default_locale)).to be_truthy
end
end
describe ".default_locale" do
it "is always loaded" do
expect(settings.default_locale).to eq("en")
end
end
describe ".default_locale=" do
it "can be changed" do
settings.default_locale = "zh_CN"
expect(settings.default_locale).to eq "zh_CN"
end
it "refresh!" do
settings.expects(:refresh!)
settings.default_locale = "zh_CN"
end
it "expires the cache" do
settings.default_locale = "zh_CN"
expect(Discourse.cache.exist?(SiteSettingExtension.client_settings_cache_key)).to be_falsey
end
it "refreshes the client" do
Discourse.expects(:request_refresh!)
settings.default_locale = "zh_CN"
end
end
describe "get_hostname" do
it "properly extracts the hostname" do
# consider testing this through a public interface, this tests implementation details
expect(settings.send(:get_hostname, "discourse.org")).to eq("discourse.org")
expect(settings.send(:get_hostname, "@discourse.org")).to eq("discourse.org")
expect(settings.send(:get_hostname, "https://discourse.org")).to eq("discourse.org")
end
end
describe "upcoming changes owned by a non-configurable plugin" do
let(:setting_name) { :enable_experimental_sample_plugin_feature }
before do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(false)
SiteSetting.promote_upcoming_changes_on_status = :alpha
end
after do
SiteSetting.promote_upcoming_changes_on_status = :stable
UpcomingChanges.clear_caches!
end
it "reports the change as disabled even though it has been promoted" do
expect(UpcomingChanges.enabled?(setting_name)).to eq(false)
end
it "keeps the setting getter in agreement with UpcomingChanges.enabled?" do
expect(SiteSetting.public_send(setting_name)).to eq(UpcomingChanges.enabled?(setting_name))
end
end
describe ".all_settings" do
describe "non-configurable plugin filtering" do
it "includes plugin site settings when the plugin is configurable" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(true)
expect(SiteSetting.all_settings.map { |s| s[:setting] }).to include(:plugin_setting)
end
it "excludes plugin site settings when the plugin is not configurable" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(false)
expect(SiteSetting.all_settings.map { |s| s[:setting] }).not_to include(:plugin_setting)
end
context "with only_upcoming_changes" do
it "excludes a non-configurable plugin's upcoming change when no upsell has un-hidden it" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(false)
settings =
SiteSetting
.all_settings(only_upcoming_changes: true, include_hidden: true)
.map { |s| s[:setting] }
expect(settings).not_to include(:enable_experimental_sample_plugin_feature)
end
it "surfaces a non-configurable plugin's upcoming change when the :hidden_site_settings modifier removes it from hidden (upsell pattern)" do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(false)
plugin = Plugin::Instance.new
modifier = ->(hidden) { hidden - [:enable_experimental_sample_plugin_feature] }
plugin.register_modifier(:hidden_site_settings, &modifier)
begin
settings =
SiteSetting
.all_settings(only_upcoming_changes: true, include_hidden: true)
.map { |s| s[:setting] }
expect(settings).to include(:enable_experimental_sample_plugin_feature)
ensure
DiscoursePluginRegistry.unregister_modifier(plugin, :hidden_site_settings, &modifier)
end
end
end
end
describe "uploads settings" do
it "should return the right values" do
negative_upload_id = [(Upload.minimum(:id) || 0) - 1, -10].min
system_upload = Fabricate(:upload, id: negative_upload_id)
settings.setting(:logo, system_upload.id, type: :upload)
settings.refresh!
setting = settings.all_settings.last
expect(setting[:value]).to eq(system_upload.url)
expect(setting[:default]).to eq(system_upload.url)
upload = Fabricate(:upload)
settings.logo = upload
settings.refresh!
setting = settings.all_settings.last
expect(setting[:value]).to eq(upload.url)
expect(setting[:default]).to eq(system_upload.url)
end
end
describe "objects settings with uploads" do
it "should hydrate upload IDs to URLs" do
upload1 = Fabricate(:upload)
upload2 = Fabricate(:upload)
upload3 = Fabricate(:upload)
schema = {
name: "section",
properties: {
title: {
type: "string",
},
image: {
type: "upload",
},
links: {
type: "objects",
schema: {
name: "link",
properties: {
link_image: {
type: "upload",
},
},
},
},
},
}
settings.setting(:test_objects_with_uploads, "[]", type: :objects, schema: schema)
settings.test_objects_with_uploads = [
{
"title" => "Section 1",
"image" => upload1.id,
"links" => [{ "link_image" => upload3.id }],
},
{ "title" => "Section 2", "image" => upload2.id },
].to_json
settings.refresh!
setting = settings.all_settings.last
value = JSON.parse(setting[:value])
expect(value[0]["image"]).to eq(upload1.url)
expect(value[1]["image"]).to eq(upload2.url)
expect(value[0]["title"]).to eq("Section 1")
expect(value[1]["title"]).to eq("Section 2")
expect(value[0]["links"][0]["link_image"]).to eq(upload3.url)
end
it "should batch uploads query" do
upload1 = Fabricate(:upload)
upload2 = Fabricate(:upload)
upload3 = Fabricate(:upload)
schema = {
name: "section",
properties: {
title: {
type: "string",
},
image: {
type: "upload",
},
},
}
settings.setting(:test_objects_with_uploads, "[]", type: :objects, schema: schema)
settings.test_objects_with_uploads = [
{ "title" => "Section 1", "image" => upload1.id },
{ "title" => "Section 2", "image" => upload2.id },
{ "title" => "Section 3", "image" => upload3.id },
].to_json
settings.refresh!
queries =
track_sql_queries do
setting = settings.all_settings.last
JSON.parse(setting[:value])
end
upload_queries = queries.select { |q| q.include?("SELECT") && q.include?("uploads") }
expect(upload_queries.length).to eq(1)
end
end
context "with the filter_allowed_hidden argument" do
it "includes the specified hidden settings only if include_hidden is true" do
result =
SiteSetting
.all_settings(include_hidden: true, filter_allowed_hidden: [:about_banner_image])
.map { |ss| ss[:setting] }
expect(result).to include(:about_banner_image)
expect(result).not_to include(:community_owner)
result =
SiteSetting
.all_settings(include_hidden: false, filter_allowed_hidden: [:about_banner_image])
.map { |ss| ss[:setting] }
expect(result).not_to include(:about_banner_image)
expect(result).not_to include(:community_owner)
result =
SiteSetting
.all_settings(include_hidden: true, filter_allowed_hidden: [:community_owner])
.map { |ss| ss[:setting] }
expect(result).not_to include(:about_banner_image)
expect(result).to include(:community_owner)
result =
SiteSetting
.all_settings(
include_hidden: true,
filter_allowed_hidden: %i[about_banner_image community_owner],
)
.map { |ss| ss[:setting] }
expect(result).to include(:about_banner_image)
expect(result).to include(:community_owner)
end
end
end
describe ".client_settings_json_uncached" do
it "should return the right json value" do
upload = Fabricate(:upload)
settings.setting(:upload_type, upload.id.to_s, type: :upload, client: true)
settings.setting(:string_type, "haha", client: true)
settings.refresh!
expect(settings.client_settings_json_uncached).to eq(
%Q|{"default_locale":"#{SiteSetting.default_locale}","upload_type":"#{upload.url}","string_type":"haha"}|,
)
end
it "settings with html type are not sanitized" do
settings.setting(:with_html, "<script></script>rest", type: :html, client: true)
client_settings = JSON.parse settings.client_settings_json_uncached
expect(client_settings["with_html"]).to eq("<script></script>rest")
end
it "does not include themeable site settings" do
SiteSetting.refresh!
expect(SiteSetting.client_settings_json_uncached).not_to include("enable_welcome_banner")
expect(SiteSetting.client_settings_json_uncached).not_to include("search_experience")
end
context "when error occurs" do
it "re-raises the exception instead of returning nil" do
settings.setting(:test_setting, "value", client: true)
settings.refresh!
allow(settings).to receive(:public_send).with(:default_locale).and_return(
SiteSetting.default_locale,
)
allow(settings).to receive(:public_send).with(:test_setting).and_raise(
PG::ConnectionBad,
"connection lost",
)
expect { settings.client_settings_json_uncached }.to raise_error(
PG::ConnectionBad,
/connection lost/,
)
end
end
end
describe ".client_settings_json" do
it "returns valid JSON when successful" do
settings.setting(:test_setting, "value", client: true)
settings.refresh!
result = settings.client_settings_json
expect { JSON.parse(result) }.not_to raise_error
parsed = JSON.parse(result)
expect(parsed["test_setting"]).to eq("value")
end
context "when error occurs" do
it "returns empty string without caching the error" do
settings.setting(:test_setting, "value", client: true)
settings.refresh!
allow(settings).to receive(:client_settings_json_uncached).and_raise(
PG::ConnectionBad,
"connection lost",
)
result = settings.client_settings_json
expect(result).to eq("")
end
it "does not cache the error and retries on next call" do
settings.setting(:test_setting, "value", client: true)
settings.refresh!
cache_key = SiteSettingExtension.client_settings_cache_key
call_count = 0
allow(settings).to receive(:client_settings_json_uncached) do
call_count += 1
if call_count == 1
raise PG::ConnectionBad, "connection lost"
else
'{"default_locale":"en","test_setting":"value"}'
end
end
# First call fails
result1 = settings.client_settings_json
expect(result1).to eq("")
# Verify error was NOT cached in Redis
expect(Discourse.cache.exist?(cache_key)).to be_falsey
# Second call should retry (not use cached error) and succeed
result2 = settings.client_settings_json
expect(result2).to eq('{"default_locale":"en","test_setting":"value"}')
expect(call_count).to eq(2) # Both calls executed, error was not cached
# Verify success was cached in Redis
expect(Discourse.cache.exist?(cache_key)).to be_truthy
expect(Discourse.cache.read(cache_key)).to eq(
'{"default_locale":"en","test_setting":"value"}',
)
end
end
end
describe ".setup_methods" do
describe "for uploads site settings" do
fab!(:upload)
fab!(:upload2, :upload)
it "should return the upload record" do
settings.setting(:some_upload, upload.id.to_s, type: :upload)
expect(settings.some_upload).to eq(upload)
# Ensure that we cache the upload record
expect(settings.some_upload.object_id).to eq(settings.some_upload.object_id)
settings.some_upload = upload2
expect(settings.some_upload).to eq(upload2)
end
end
end
describe "mandatory_values for group list settings" do
it "adds mandatory values" do
expect(SiteSetting.embedded_media_post_allowed_groups).to eq("1|2|10")
SiteSetting.embedded_media_post_allowed_groups = 14
expect(SiteSetting.embedded_media_post_allowed_groups).to eq("1|2|14")
SiteSetting.embedded_media_post_allowed_groups = ""
expect(SiteSetting.embedded_media_post_allowed_groups).to eq("1|2")
test_provider = SiteSetting.provider
SiteSetting.provider = SiteSettings::DbProvider.new(SiteSetting)
SiteSetting.embedded_media_post_allowed_groups = "13|14"
expect(SiteSetting.embedded_media_post_allowed_groups).to eq("1|2|13|14")
expect(SiteSetting.find_by(name: "embedded_media_post_allowed_groups").value).to eq(
"1|2|13|14",
)
ensure
SiteSetting.find_by(name: "embedded_media_post_allowed_groups").destroy
SiteSetting.provider = test_provider
end
end
describe "disallowed_groups for group list settings" do
it "strips disallowed groups when setting a value" do
SiteSetting.whispers_allowed_groups = "0|1|2"
expect(SiteSetting.whispers_allowed_groups).to eq("1|2")
SiteSetting.whispers_allowed_groups = "0"
expect(SiteSetting.whispers_allowed_groups).to eq("")
SiteSetting.whispers_allowed_groups = "1|0|2|0"
expect(SiteSetting.whispers_allowed_groups).to eq("1|2")
end
it "is included in all_settings output" do
setting = SiteSetting.all_settings.find { |s| s[:setting] == :whispers_allowed_groups }
expect(setting[:disallowed_groups]).to eq("0|4|5")
end
end
describe "group settings" do
fab!(:group)
it "stores a valid group id as a string" do
settings.setting(:test_group_setting, "", type: :group)
settings.test_group_setting = group.id.to_s
expect(settings.test_group_setting).to eq(group.id.to_s)
end
it "rejects a value that does not match an existing group" do
settings.setting(:test_group_setting, "", type: :group)
expect { settings.test_group_setting = "-9999" }.to raise_error(Discourse::InvalidParameters)
end
it "allows a blank value" do
settings.setting(:test_group_setting, "", type: :group)
settings.test_group_setting = ""
expect(settings.test_group_setting).to eq("")
end
end
describe "requires_confirmation settings" do
it "returns 'simple' for settings that require confirmation with 'simple' type" do
expect(
SiteSetting.all_settings.find { |s| s[:setting] == :min_password_length }[
:requires_confirmation
],
).to eq("simple")
end
it "returns 'simple_on_disable' for settings that require confirmation with 'simple_on_disable' type" do
expect(
SiteSetting.all_settings.find { |s| s[:setting] == :content_security_policy }[
:requires_confirmation
],
).to eq("simple_on_disable")
end
it "returns nil for settings that do not require confirmation" do
expect(
SiteSetting.all_settings.find { |s| s[:setting] == :display_local_time_in_user_card }[
:requires_confirmation
],
).to eq(nil)
end
end
describe "site setting groups" do
before do
SiteSettingGroup.create!(
name: "enable_upload_debug_mode",
group_ids: "#{Group::AUTO_GROUPS[:trust_level_0]}|#{Group::AUTO_GROUPS[:trust_level_1]}",
)
end
it "returns the correct group for a setting" do
SiteSetting.refresh!
expect(SiteSetting.site_setting_group_ids[:enable_upload_debug_mode]).to eq([10, 11])
end
end
describe "site setting singleton methods" do
describe "for an upcoming change site setting" do
let(:setting_name) { :enable_upload_debug_mode }
let(:default_value) { SiteSetting.defaults[setting_name] }
before do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :beta,
impact_type: "other",
impact_role: "developers",
},
},
)
SiteSetting.send(:setup_methods, setting_name)
SiteSetting.refresh!
end
after do
SiteSetting.remove_override!(setting_name)
SiteSetting.refresh!
end
it "returns the stored value when it differs from the default" do
SiteSetting.public_send("#{setting_name}=", !default_value)
expect(SiteSetting.public_send(setting_name)).to eq(!default_value)
end
it "returns true when the setting meets the promotion status" do
SiteSetting.promote_upcoming_changes_on_status = :beta
expect(SiteSetting.public_send(setting_name)).to eq(true)
end
it "returns false when admins have changed the setting to false even if the promotion status is met" do
SiteSetting.public_send("#{setting_name}=", false)
expect(SiteSetting.public_send(setting_name)).to eq(false)
SiteSetting.promote_upcoming_changes_on_status = :beta
expect(SiteSetting.public_send(setting_name)).to eq(false)
end
it "returns the default when the setting does not meet the promotion status" do
SiteSetting.promote_upcoming_changes_on_status = :stable
expect(SiteSetting.public_send(setting_name)).to eq(default_value)
end
context "when the upcoming 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" do
expect(SiteSetting.public_send(setting_name)).to eq(true)
end
it "return true even if the setting value is false in the database" do
SiteSetting.public_send("#{setting_name}=", false)
expect(SiteSetting.public_send(setting_name)).to eq(true)
end
end
end
end
describe "#notify_clients!" do
context "when the site setting is an upcoming change" do
before do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: :experimental,
impact_type: "other",
impact_role: "developers",
},
some_other_upcoming_setting: {
impact: "feature,staff",
status: :alpha,
impact_type: "feature",
impact_role: "staff",
},
},
)
SiteSetting.send(:setup_methods, :enable_upload_debug_mode)
SiteSetting.refresh!
end
after { SiteSetting.refresh! }
context "with site setting groups assigned" do
before do
SiteSettingGroup.create!(
name: "enable_upload_debug_mode",
group_ids:
"#{Group::AUTO_GROUPS[:trust_level_0]}|#{Group::AUTO_GROUPS[:trust_level_1]}",
)
SiteSetting.refresh!
end
after { SiteSetting.refresh! }
it "does not publish to MessageBus for the client settings channel" do
messages =
MessageBus.track_publish(SiteSettingExtension::CLIENT_SETTINGS_CHANNEL) do
SiteSetting.notify_clients!(:enable_upload_debug_mode)
end
expect(messages.length).to eq(0)
end
end
context "without site setting groups assigned" do
it "publishes to MessageBus with the setting name and value" do
messages =
MessageBus.track_publish(SiteSettingExtension::CLIENT_SETTINGS_CHANNEL) do
SiteSetting.notify_clients!(:enable_upload_debug_mode)
end
expect(messages.length).to eq(1)
expect(messages.first.data[:name]).to eq(:enable_upload_debug_mode)
expect(messages.first.data[:value]).to eq(SiteSetting.enable_upload_debug_mode)
end
end
end
context "when the site setting is not an upcoming change" do
it "publishes to MessageBus with the setting name and value" do
messages =
MessageBus.track_publish(SiteSettingExtension::CLIENT_SETTINGS_CHANNEL) do
SiteSetting.notify_clients!(:title)
end
expect(messages.length).to eq(1)
expect(messages.first.data[:name]).to eq(:title)
expect(messages.first.data[:value]).to eq(SiteSetting.title)
end
it "includes scoped_to parameter when provided" do
messages =
MessageBus.track_publish(SiteSettingExtension::CLIENT_SETTINGS_CHANNEL) do
SiteSetting.notify_clients!(:title, { theme_id: 123 })
end
expect(messages.length).to eq(1)
expect(messages.first.data[:scoped_to]).to eq({ theme_id: 123 })
end
end
context "with default_locale setting" do
it "uses the custom getter for default_locale" do
messages =
MessageBus.track_publish(SiteSettingExtension::CLIENT_SETTINGS_CHANNEL) do
SiteSetting.notify_clients!(:default_locale)
end
expect(messages.length).to eq(1)
expect(messages.first.data[:name]).to eq(:default_locale)
expect(messages.first.data[:value]).to eq(SiteSetting.default_locale)
end
end
end
describe "themeable settings" do
fab!(:theme_1, :theme)
fab!(:theme_2, :theme)
fab!(:tss_1) do
Fabricate(
:theme_site_setting_with_service,
name: "enable_welcome_banner",
value: false,
theme: theme_1,
)
end
fab!(:tss_2) do
Fabricate(
:theme_site_setting_with_service,
name: "search_experience",
value: "search_field",
theme: theme_2,
)
end
it "has the site setting default values when there are no theme site settings for the theme" do
SiteSetting.refresh!
expect(SiteSetting.theme_site_settings[theme_1.id][:search_experience]).to eq("search_icon")
expect(SiteSetting.theme_site_settings[theme_2.id][:enable_welcome_banner]).to eq(true)
end
it "returns true for settings that are themeable" do
expect(SiteSetting.themeable[:enable_welcome_banner]).to eq(true)
end
it "returns false for settings that are not themeable" do
expect(SiteSetting.themeable[:title]).to eq(false)
end
it "caches the theme site setting values on a per theme basis" do
SiteSetting.refresh!
expect(SiteSetting.theme_site_settings[theme_1.id][:enable_welcome_banner]).to eq(false)
expect(SiteSetting.theme_site_settings[theme_2.id][:search_experience]).to eq("search_field")
end
it "overrides the site setting value with the theme site setting" do
SiteSetting.create!(
name: "enable_welcome_banner",
data_type: SiteSettings::TypeSupervisor.types[:bool],
value: "t",
)
SiteSetting.create!(
name: "search_experience",
data_type: SiteSettings::TypeSupervisor.types[:enum],
value: SiteSetting.type_supervisor.to_db_value(:search_experience, "search_icon"),
)
SiteSetting.refresh!
expect(SiteSetting.enable_welcome_banner(theme_id: theme_1.id)).to eq(false)
expect(SiteSetting.enable_welcome_banner(theme_id: theme_2.id)).to eq(true)
expect(SiteSetting.search_experience(theme_id: theme_1.id)).to eq("search_icon")
expect(SiteSetting.search_experience(theme_id: theme_2.id)).to eq("search_field")
end
it "publishes the right MessageBus message when a theme site setting is updated" do
settings_tss_instance_1 = new_settings(provider_local)
settings_tss_instance_1.load_settings(Rails.root.join("config/site_settings.yml").to_s)
settings_tss_instance_1.refresh!
expect(settings_tss_instance_1.enable_welcome_banner(theme_id: theme_1.id)).to eq(false)
tss_1.update!(value: true)
messages =
MessageBus.track_publish(described_class::SITE_SETTINGS_CHANNEL) do
settings_tss_instance_1.change_themeable_site_setting(
theme_1.id,
:enable_welcome_banner,
true,
)
end
expect(messages.length).to eq(1)
message = messages.first
expect(message.data[:process]).to eq(settings_tss_instance_1.process_id)
end
describe ".theme_site_settings_json_uncached" do
it "returns the correct JSON" do
SiteSetting.refresh!
expect(SiteSetting.theme_site_settings_json_uncached(theme_1.id)).to eq(
%Q|{"enable_welcome_banner":false,"search_experience":"search_icon"}|,
)
end
it "returns default JSON when the theme_id is null" do
SiteSetting.refresh!
expect(SiteSetting.theme_site_settings_json_uncached(nil)).to eq(
%Q|{"enable_welcome_banner":true,"search_experience":"search_icon"}|,
)
end
end
end
describe "_map extension for list settings" do
it "handles splitting group_list settings" do
SiteSetting.personal_message_enabled_groups = "1|2"
expect(SiteSetting.personal_message_enabled_groups_map).to eq([1, 2])
end
it "handles splitting category_list settings" do
SiteSetting.digest_suppress_categories = "3|4"
expect(SiteSetting.digest_suppress_categories_map).to eq([3, 4])
end
it "handles splitting compact list settings" do
SiteSetting.markdown_linkify_tlds = "com|net"
expect(SiteSetting.markdown_linkify_tlds_map).to eq(%w[com net])
end
it "handles splitting simple list settings" do
SiteSetting.ga_universal_auto_link_domains = "test.com|xy.com"
expect(SiteSetting.ga_universal_auto_link_domains_map).to eq(%w[test.com xy.com])
end
it "handles splitting locale list settings" do
SiteSetting.content_localization_supported_locales = "ja|pt_BR"
expect(SiteSetting.content_localization_supported_locales_map).to eq(%w[ja pt_BR])
end
it "handles splitting list settings with no type" do
SiteSetting.post_menu = "read|like"
expect(SiteSetting.post_menu_map).to eq(%w[read like])
end
it "does not handle splitting secret list settings" do
SiteSetting.discourse_connect_provider_secrets = "test|secret1\ntest2|secret2"
expect(SiteSetting.respond_to?(:discourse_connect_provider_secrets_map)).to eq(false)
end
it "handles splitting emoji_list settings" do
SiteSetting.emoji_deny_list = "smile|frown"
expect(SiteSetting.emoji_deny_list_map).to eq(%w[smile frown])
end
it "handles splitting tag_list settings" do
SiteSetting.digest_suppress_tags = "blah|blah2"
expect(SiteSetting.digest_suppress_tags_map).to eq(%w[blah blah2])
end
it "handles blank values for settings" do
SiteSetting.ga_universal_auto_link_domains = ""
SiteSetting.pm_tags_allowed_for_groups = ""
SiteSetting.exclude_rel_nofollow_domains = ""
expect(SiteSetting.ga_universal_auto_link_domains_map).to eq([])
expect(SiteSetting.pm_tags_allowed_for_groups_map).to eq([])
expect(SiteSetting.exclude_rel_nofollow_domains_map).to eq([])
end
end
describe "keywords" do
it "gets the list of I18n keywords for the setting" do
expect(SiteSetting.keywords(:clean_up_inactive_users_after_days)).to eq(
I18n.t("site_settings.keywords.clean_up_inactive_users_after_days").split("|"),
)
end
it "gets the current locale keywords and the english keywords for the setting" do
I18n.locale = :de
expect(SiteSetting.keywords(:clean_up_inactive_users_after_days)).to match_array(
(
I18n.t("site_settings.keywords.clean_up_inactive_users_after_days").split("|") +
I18n.t("site_settings.keywords.clean_up_inactive_users_after_days", locale: :en).split(
"|",
)
).flatten,
)
end
context "when a setting also has an alias after renaming" do
before { SiteSetting.stubs(:deprecated_setting_aliases).returns(["some_old_setting"]) }
it "is included with the keywords" do
expect(SiteSetting.keywords(:clean_up_inactive_users_after_days)).to include(
"some_old_setting",
)
end
end
end
describe "humanized_name" do
it "returns the humanized name for a setting" do
expect(SiteSetting.humanized_name(:clean_up_inactive_users_after_days)).to eq(
"Clean up inactive users after days",
)
end
it "handles acronyms in setting names" do
expect(SiteSetting.humanized_name(:enable_linkedin_oidc_logins)).to eq(
"Enable LinkedIn OIDC logins",
)
end
it "handles mixed case in setting names" do
expect(SiteSetting.humanized_name(:opengraph_image)).to eq("OpenGraph image")
end
end
describe "linkify" do
it "returns an html_safe anchor with the humanized name as the label and the setting's area/category as data attributes" do
result = SiteSettings::LabelFormatter.linkify(:enable_linkedin_oidc_logins)
expect(result).to eq(
'<a class="site-setting-link" href="/admin/site_settings/category/all_results?filter=enable_linkedin_oidc_logins" data-setting-name="enable_linkedin_oidc_logins" data-setting-area="authenticators" data-setting-category="login">Enable LinkedIn OIDC logins</a>',
)
expect(result).to be_html_safe
end
it "accepts a string and omits the area attribute when the setting has none" do
expect(SiteSettings::LabelFormatter.linkify("opengraph_image")).to eq(
'<a class="site-setting-link" href="/admin/site_settings/category/all_results?filter=opengraph_image" data-setting-name="opengraph_image" data-setting-category="branding">OpenGraph image</a>',
)
end
it "omits the metadata attributes for an unknown setting" do
expect(SiteSettings::LabelFormatter.linkify(:not_a_real_setting)).to eq(
'<a class="site-setting-link" href="/admin/site_settings/category/all_results?filter=not_a_real_setting" data-setting-name="not_a_real_setting">Not a real setting</a>',
)
end
it "honors the configured base path" do
Discourse.stubs(:base_path).returns("/forum")
expect(SiteSettings::LabelFormatter.linkify(:title)).to eq(
'<a class="site-setting-link" href="/forum/admin/site_settings/category/all_results?filter=title" data-setting-name="title" data-setting-area="about" data-setting-category="required">Title</a>',
)
end
end
describe "linkify_settings" do
it "links to the all-settings page filtered to every name with the given label" do
result =
SiteSettings::LabelFormatter.linkify_settings(
%w[title logo],
label: "View related settings",
)
expect(result).to eq(
'<a class="site-setting-link" href="/admin/site_settings/category/all_results?filter=any%3Atitle%7Clogo">View related settings</a>',
)
expect(result).to be_html_safe
end
it "falls back to the humanized names joined with commas when no label is given" do
expect(SiteSettings::LabelFormatter.linkify_settings(%w[title opengraph_image])).to eq(
'<a class="site-setting-link" href="/admin/site_settings/category/all_results?filter=any%3Atitle%7Copengraph_image">Title, OpenGraph image</a>',
)
end
end
describe "expand_setting_links" do
it "expands {{setting:foo}} markers into linkified HTML" do
expanded =
SiteSettings::LabelFormatter.expand_setting_links(
"Configure {{setting:title}} before enabling.",
)
expect(expanded).to eq(
'Configure <a class="site-setting-link" href="/admin/site_settings/category/all_results?filter=title" data-setting-name="title" data-setting-area="about" data-setting-category="required">Title</a> before enabling.',
)
expect(expanded).to be_html_safe
end
it "expands multiple markers in the same string" do
expanded =
SiteSettings::LabelFormatter.expand_setting_links(
"Use {{setting:title}} and {{setting:logo}}.",
)
expect(expanded).to include('data-setting-name="title"')
expect(expanded).to include(">Title</a>")
expect(expanded).to include('data-setting-name="logo"')
expect(expanded).to include(">Logo</a>")
end
it "expands {{settings:a,b|label}} markers into a single link filtered to every setting" do
expanded =
SiteSettings::LabelFormatter.expand_setting_links(
"Something went wrong. {{settings:title,logo|View related settings}}",
)
expect(expanded).to eq(
'Something went wrong. <a class="site-setting-link" href="/admin/site_settings/category/all_results?filter=any%3Atitle%7Clogo">View related settings</a>',
)
expect(expanded).to be_html_safe
end
it "expands {{settings:...}} markers without a label using the humanized names" do
expanded = SiteSettings::LabelFormatter.expand_setting_links("See {{settings:title,logo}}.")
expect(expanded).to include(">Title, Logo</a>")
end
it "expands singular and plural markers in the same string" do
expanded =
SiteSettings::LabelFormatter.expand_setting_links(
"Enable {{setting:title}} first. {{settings:title,logo|View related settings}}",
)
expect(expanded).to include('data-setting-name="title"')
expect(expanded).to include(">View related settings</a>")
end
it "returns input unchanged when no markers are present" do
expect(SiteSettings::LabelFormatter.expand_setting_links("nothing to expand")).to eq(
"nothing to expand",
)
end
it "escapes the surrounding text with escape_text so only generated anchors are HTML" do
expanded =
SiteSettings::LabelFormatter.expand_setting_links(
"<img src=x onerror=alert(1)> {{settings:title,logo|View & fix}}",
escape_text: true,
)
expect(expanded).to eq(
'&lt;img src=x onerror=alert(1)&gt; <a class="site-setting-link" href="/admin/site_settings/category/all_results?filter=any%3Atitle%7Clogo">View &amp; fix</a>',
)
end
it "handles blank input safely" do
expect(SiteSettings::LabelFormatter.expand_setting_links("")).to eq("")
expect(SiteSettings::LabelFormatter.expand_setting_links(nil)).to be_nil
end
end
describe "contains_setting_links?" do
it "detects both marker forms and rejects lookalikes" do
expect(SiteSettings::LabelFormatter.contains_setting_links?("See {{setting:title}}.")).to eq(
true,
)
expect(
SiteSettings::LabelFormatter.contains_setting_links?("See {{settings:title,logo|All}}."),
).to eq(true)
expect(SiteSettings::LabelFormatter.contains_setting_links?("See {{settings}}.")).to eq(false)
expect(SiteSettings::LabelFormatter.contains_setting_links?("plain text")).to eq(false)
expect(SiteSettings::LabelFormatter.contains_setting_links?(nil)).to eq(false)
end
end
describe "plain_setting_links" do
it "renders singular markers as quoted humanized names and plural markers as their label" do
expect(
SiteSettings::LabelFormatter.plain_setting_links(
"Enable {{setting:title}} first. {{settings:title,logo|View related settings}}",
),
).to eq("Enable 'Title' first. View related settings")
end
it "falls back to the humanized name list when a plural marker has no label" do
expect(
SiteSettings::LabelFormatter.plain_setting_links("See {{settings:title,logo}}."),
).to eq("See Title, Logo.")
end
end
describe "description" do
it "expands {{setting:foo}} markers in the translated description" do
expect(SiteSetting.description(:logo_dark)).to eq(
'Dark scheme alternative for the <a class="site-setting-link" href="/admin/site_settings/category/all_results?filter=logo" data-setting-name="logo" data-setting-category="branding">Logo</a> site setting.',
)
end
end
describe "logging Site Settings via the Rails Console" do
around do |example|
# Ensure Rails::Console is defined for the duration of each example.
if !Rails.const_defined?(:Console)
Rails.const_set("Console", Module.new)
example.run
Rails.send(:remove_const, "Console")
else
example.run
end
end
before do
settings.setting(:log_test, "initial")
settings.refresh!
end
context "when using the direct setter" do
it "logs the change exactly once" do
logger_spy = instance_spy(StaffActionLogger)
allow(StaffActionLogger).to receive(:new).with(Discourse.system_user).and_return(logger_spy)
settings.log_test = "changed"
expect(settings.log_test).to eq("changed")
expect(logger_spy).to have_received(:log_site_setting_change).with(
:log_test,
"initial",
"changed",
{ details: "Updated via Rails console" },
).once
end
end
context "when using set_and_log" do
it "logs the change exactly once without double logging" do
logger_spy = instance_spy(StaffActionLogger)
allow(StaffActionLogger).to receive(:new).with(Discourse.system_user).and_return(logger_spy)
settings.set_and_log("log_test", "changed", Discourse.system_user)
expect(settings.log_test).to eq("changed")
expect(logger_spy).to have_received(:log_site_setting_change).with(
:log_test,
"initial",
"changed",
{ details: "Updated via Rails console" },
).once
end
end
context "for secret settings" do
before do
settings.setting(:secret_test, "old_secret", secret: true)
settings.refresh!
end
it "logs filtered values" do
logger_spy = instance_spy(StaffActionLogger)
allow(StaffActionLogger).to receive(:new).with(Discourse.system_user).and_return(logger_spy)
settings.secret_test = "new_secret"
expect(settings.secret_test).to eq("new_secret")
expect(logger_spy).to have_received(:log_site_setting_change).with(
:secret_test,
"[FILTERED]",
"[FILTERED]",
{ details: "Updated via Rails console" },
).once
end
end
context "for hidden settings" do
before do
settings.setting(:hidden_test, "old_hidden", hidden: true)
settings.refresh!
end
it "does not log the change" do
logger_spy = instance_spy(StaffActionLogger)
allow(StaffActionLogger).to receive(:new).with(Discourse.system_user).and_return(logger_spy)
settings.hidden_test = "changed"
expect(settings.hidden_test).to eq("changed")
expect(logger_spy).not_to have_received(:log_site_setting_change)
end
end
context "with plugin modifiers for log details" do
before do
settings.setting(:plugin_test, "initial")
settings.refresh!
end
it "uses the default log details when no plugin modifiers exist" do
logger_spy = instance_spy(StaffActionLogger)
allow(StaffActionLogger).to receive(:new).with(Discourse.system_user).and_return(logger_spy)
settings.plugin_test = "changed"
expect(settings.plugin_test).to eq("changed")
expect(logger_spy).to have_received(:log_site_setting_change).with(
:plugin_test,
"initial",
"changed",
{ details: "Updated via Rails console" },
).once
end
it "applies plugin modifiers to log details" do
# Allow all apply_modifier calls to pass through normally
allow(DiscoursePluginRegistry).to receive(:apply_modifier).and_call_original
# But specifically mock our target call
allow(DiscoursePluginRegistry).to receive(:apply_modifier).with(
:site_setting_log_details,
"Updated via Rails console",
).and_return("Updated via Rails console via test plugin")
logger_spy = instance_spy(StaffActionLogger)
allow(StaffActionLogger).to receive(:new).with(Discourse.system_user).and_return(logger_spy)
settings.plugin_test = "changed"
expect(settings.plugin_test).to eq("changed")
expect(logger_spy).to have_received(:log_site_setting_change).with(
:plugin_test,
"initial",
"changed",
{ details: "Updated via Rails console via test plugin" },
).once
end
end
end
describe "upcoming_change_default_override" do
before do
settings.setting(
:increase_suggested_topics_max_days_old_default,
false,
upcoming_change: {
status: :experimental,
impact: "site_setting_default,all_members",
},
)
settings.setting(
:suggested_topics_max_days_old,
365,
upcoming_change_default_override: {
upcoming_change: "increase_suggested_topics_max_days_old_default",
new_default: 1000,
},
)
settings.setting(:promote_upcoming_changes_on_status, "stable")
UpcomingChanges.stubs(:settings_provider).returns(settings)
end
context "when the linked upcoming change is active" do
before do
settings.provider.save(
:increase_suggested_topics_max_days_old_default,
true,
SiteSetting.types[:bool],
)
settings.refresh!
end
it "returns the override default" do
expect(settings.suggested_topics_max_days_old).to eq(1000)
end
end
context "when the linked upcoming change is not active" do
it "returns the YAML default" do
expect(settings.suggested_topics_max_days_old).to eq(365)
end
end
context "when the linked upcoming change is enabled via add_override!" do
it "immediately applies the override default without needing refresh!" do
settings.add_override!(:increase_suggested_topics_max_days_old_default, true)
expect(settings.suggested_topics_max_days_old).to eq(1000)
end
it "immediately reverts the override default when disabled" do
settings.add_override!(:increase_suggested_topics_max_days_old_default, true)
expect(settings.suggested_topics_max_days_old).to eq(1000)
settings.add_override!(:increase_suggested_topics_max_days_old_default, false)
expect(settings.suggested_topics_max_days_old).to eq(365)
end
it "does not override a manually set target setting value" do
settings.add_override!(:suggested_topics_max_days_old, 730)
settings.add_override!(:increase_suggested_topics_max_days_old_default, true)
expect(settings.suggested_topics_max_days_old).to eq(730)
end
end
context "when the linked upcoming change is reset via remove_override!" do
it "immediately reverts the override default" do
settings.add_override!(:increase_suggested_topics_max_days_old_default, true)
expect(settings.suggested_topics_max_days_old).to eq(1000)
settings.remove_override!(:increase_suggested_topics_max_days_old_default)
expect(settings.increase_suggested_topics_max_days_old_default).to eq(false)
expect(settings.suggested_topics_max_days_old).to eq(365)
expect(
settings.defaults.upcoming_change_override_metadata(:suggested_topics_max_days_old),
).to be_nil
end
it "reapplies the override default when a manual opt out is removed after auto-promotion" do
settings.add_override!(:promote_upcoming_changes_on_status, "experimental")
settings.refresh!
expect(settings.suggested_topics_max_days_old).to eq(1000)
settings.add_override!(:increase_suggested_topics_max_days_old_default, false)
expect(settings.suggested_topics_max_days_old).to eq(365)
settings.remove_override!(:increase_suggested_topics_max_days_old_default)
expect(settings.increase_suggested_topics_max_days_old_default).to eq(true)
expect(settings.suggested_topics_max_days_old).to eq(1000)
end
end
context "when admin opts out of the target setting after the override is active" do
before do
settings.provider.save(
:increase_suggested_topics_max_days_old_default,
true,
SiteSetting.types[:bool],
)
settings.refresh!
end
it "preserves the admin's explicit value across refresh, even when it equals the original default" do
expect(settings.suggested_topics_max_days_old).to eq(1000)
settings.provider.save(:suggested_topics_max_days_old, 365, SiteSetting.types[:integer])
settings.refresh!
expect(settings.suggested_topics_max_days_old).to eq(365)
end
end
describe "all_settings effective default" do
context "when the linked upcoming change is active" do
before do
UpcomingChanges.stubs(:settings_provider).returns(settings)
settings.provider.save(
:increase_suggested_topics_max_days_old_default,
true,
SiteSetting.types[:bool],
)
settings.refresh!
end
it "shows the override default in all_settings" do
setting = settings.all_settings.find { |s| s[:setting] == :suggested_topics_max_days_old }
expect(setting[:default]).to eq("1000")
end
it "does not show the setting as overridden when value matches effective default" do
setting = settings.all_settings.find { |s| s[:setting] == :suggested_topics_max_days_old }
expect(setting[:value]).to eq(setting[:default])
end
end
context "when the linked upcoming change is not active" do
it "shows the original YAML default in all_settings" do
setting = settings.all_settings.find { |s| s[:setting] == :suggested_topics_max_days_old }
expect(setting[:default]).to eq("365")
end
end
end
end
end