2019-04-30 08:27:42 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2022-07-28 10:27:38 +08:00
|
|
|
RSpec.describe Plugin::Instance do
|
2024-01-08 07:57:25 +08:00
|
|
|
subject(:plugin_instance) { described_class.new(metadata) }
|
|
|
|
|
|
|
|
let(:metadata) { Plugin::Metadata.parse <<TEXT }
|
|
|
|
# name: discourse-sample-plugin
|
|
|
|
# about: about: my plugin
|
|
|
|
# version: 0.1
|
|
|
|
# authors: Frank Zappa
|
|
|
|
# contact emails: frankz@example.com
|
|
|
|
# url: http://discourse.org
|
|
|
|
# required version: 1.3.0beta6+48
|
|
|
|
# meta_topic_id: 1234
|
|
|
|
# label: experimental
|
|
|
|
|
|
|
|
some_ruby
|
|
|
|
TEXT
|
2023-06-21 22:00:19 +08:00
|
|
|
|
2024-05-24 22:15:53 +08:00
|
|
|
around { |example| allow_missing_translations(&example) }
|
|
|
|
|
2014-12-10 03:20:53 +08:00
|
|
|
after { DiscoursePluginRegistry.reset! }
|
2014-04-02 13:22:12 +08:00
|
|
|
|
2024-01-08 07:57:25 +08:00
|
|
|
# NOTE: sample_plugin_site_settings.yml is always loaded in tests in site_setting.rb
|
|
|
|
|
|
|
|
describe ".humanized_name" do
|
|
|
|
before do
|
|
|
|
TranslationOverride.upsert!(
|
|
|
|
"en",
|
|
|
|
"admin_js.admin.site_settings.categories.discourse_sample_plugin",
|
|
|
|
"Sample Plugin Category Name",
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "defaults to using the plugin name with the discourse- prefix removed" do
|
|
|
|
expect(plugin_instance.humanized_name).to eq("sample-plugin")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "uses the plugin setting category name if it exists" do
|
|
|
|
plugin_instance.enabled_site_setting(:discourse_sample_plugin_enabled)
|
|
|
|
expect(plugin_instance.humanized_name).to eq("Sample Plugin Category Name")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "the plugin name the plugin site settings are still under the generic plugins: category" do
|
2024-02-29 19:24:37 +08:00
|
|
|
plugin_instance.stubs(:setting_category).returns("plugins")
|
2024-01-08 07:57:25 +08:00
|
|
|
expect(plugin_instance.humanized_name).to eq("sample-plugin")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "removes any Discourse prefix from the setting category name" do
|
|
|
|
TranslationOverride.upsert!(
|
|
|
|
"en",
|
|
|
|
"admin_js.admin.site_settings.categories.discourse_sample_plugin",
|
|
|
|
"Discourse Sample Plugin Category Name",
|
|
|
|
)
|
|
|
|
plugin_instance.enabled_site_setting(:discourse_sample_plugin_enabled)
|
|
|
|
expect(plugin_instance.humanized_name).to eq("Sample Plugin Category Name")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-07-28 00:14:14 +08:00
|
|
|
describe "find_all" do
|
2013-08-26 09:04:16 +08:00
|
|
|
it "can find plugins correctly" do
|
|
|
|
plugins = Plugin::Instance.find_all("#{Rails.root}/spec/fixtures/plugins")
|
2021-03-13 00:17:42 +08:00
|
|
|
expect(plugins.count).to eq(5)
|
2020-08-06 21:46:17 +08:00
|
|
|
plugin = plugins[3]
|
2013-08-26 09:04:16 +08:00
|
|
|
|
2015-01-10 00:34:37 +08:00
|
|
|
expect(plugin.name).to eq("plugin-name")
|
|
|
|
expect(plugin.path).to eq("#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb")
|
2023-06-26 12:39:57 +08:00
|
|
|
|
|
|
|
plugin.git_repo.stubs(:latest_local_commit).returns("123456")
|
|
|
|
plugin.git_repo.stubs(:url).returns("http://github.com/discourse/discourse-plugin")
|
|
|
|
|
|
|
|
expect(plugin.commit_hash).to eq("123456")
|
|
|
|
expect(plugin.commit_url).to eq("http://github.com/discourse/discourse-plugin/commit/123456")
|
2023-11-21 07:37:11 +08:00
|
|
|
expect(plugin.discourse_owned?).to eq(true)
|
2013-08-26 09:04:16 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "does not blow up on missing directory" do
|
|
|
|
plugins = Plugin::Instance.find_all("#{Rails.root}/frank_zappa")
|
2015-01-10 00:34:37 +08:00
|
|
|
expect(plugins.count).to eq(0)
|
2013-08-26 09:04:16 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-12-16 03:47:20 +08:00
|
|
|
describe "stats" do
|
|
|
|
after { DiscoursePluginRegistry.reset! }
|
|
|
|
|
|
|
|
it "returns core stats" do
|
|
|
|
stats = Plugin::Instance.stats
|
|
|
|
expect(stats.keys).to contain_exactly(
|
|
|
|
:topics_last_day,
|
|
|
|
:topics_7_days,
|
|
|
|
:topics_30_days,
|
|
|
|
:topics_count,
|
|
|
|
:posts_last_day,
|
|
|
|
:posts_7_days,
|
|
|
|
:posts_30_days,
|
|
|
|
:posts_count,
|
|
|
|
:users_last_day,
|
|
|
|
:users_7_days,
|
|
|
|
:users_30_days,
|
|
|
|
:users_count,
|
|
|
|
:active_users_last_day,
|
|
|
|
:active_users_7_days,
|
|
|
|
:active_users_30_days,
|
|
|
|
:likes_last_day,
|
|
|
|
:likes_7_days,
|
|
|
|
:likes_30_days,
|
|
|
|
:likes_count,
|
2024-08-13 05:47:13 +08:00
|
|
|
:participating_users_last_day,
|
|
|
|
:participating_users_7_days,
|
|
|
|
:participating_users_30_days,
|
2023-12-16 03:47:20 +08:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "returns stats registered by plugins" do
|
|
|
|
plugin = Plugin::Instance.new
|
|
|
|
stats_name = "plugin_stats"
|
|
|
|
plugin.register_stat(stats_name) do
|
|
|
|
{ :last_day => 1, "7_days" => 10, "30_days" => 100, :count => 1000 }
|
|
|
|
end
|
|
|
|
|
|
|
|
stats = Plugin::Instance.stats
|
|
|
|
|
|
|
|
expect(stats.with_indifferent_access).to match(
|
|
|
|
hash_including(
|
|
|
|
"#{stats_name}_last_day": 1,
|
|
|
|
"#{stats_name}_7_days": 10,
|
|
|
|
"#{stats_name}_30_days": 100,
|
|
|
|
"#{stats_name}_count": 1000,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-11-21 07:37:11 +08:00
|
|
|
describe "git repo details" do
|
|
|
|
describe ".discourse_owned?" do
|
|
|
|
it "returns true if the plugin is on github in discourse-org or discourse orgs" do
|
|
|
|
plugin = Plugin::Instance.find_all("#{Rails.root}/spec/fixtures/plugins")[3]
|
|
|
|
plugin.git_repo.stubs(:latest_local_commit).returns("123456")
|
|
|
|
plugin.git_repo.stubs(:url).returns("http://github.com/discourse/discourse-plugin")
|
|
|
|
expect(plugin.discourse_owned?).to eq(true)
|
|
|
|
|
|
|
|
plugin.git_repo.stubs(:url).returns("http://github.com/discourse-org/discourse-plugin")
|
|
|
|
expect(plugin.discourse_owned?).to eq(true)
|
|
|
|
|
|
|
|
plugin.git_repo.stubs(:url).returns("http://github.com/someguy/someguy-plugin")
|
|
|
|
expect(plugin.discourse_owned?).to eq(false)
|
|
|
|
end
|
2023-11-24 10:08:10 +08:00
|
|
|
|
|
|
|
it "returns false if the commit_url is missing because of git command issues" do
|
|
|
|
plugin = Plugin::Instance.find_all("#{Rails.root}/spec/fixtures/plugins")[3]
|
|
|
|
plugin.git_repo.stubs(:latest_local_commit).returns(nil)
|
|
|
|
expect(plugin.discourse_owned?).to eq(false)
|
|
|
|
end
|
2023-11-21 07:37:11 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-07-28 00:14:14 +08:00
|
|
|
describe "enabling/disabling" do
|
2015-02-05 05:23:39 +08:00
|
|
|
it "is enabled by default" do
|
|
|
|
expect(Plugin::Instance.new.enabled?).to eq(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
context "with a plugin that extends things" do
|
2019-12-05 01:26:23 +08:00
|
|
|
class Trout
|
|
|
|
attr_accessor :data
|
|
|
|
end
|
|
|
|
|
2019-08-27 16:21:53 +08:00
|
|
|
class TroutSerializer < ApplicationSerializer
|
|
|
|
attribute :name
|
|
|
|
|
|
|
|
def name
|
|
|
|
"a trout"
|
|
|
|
end
|
|
|
|
end
|
2023-12-15 23:46:04 +08:00
|
|
|
|
2019-08-27 16:21:53 +08:00
|
|
|
class TroutJuniorSerializer < TroutSerializer
|
|
|
|
attribute :i_am_child
|
|
|
|
|
|
|
|
def name
|
|
|
|
"a trout jr"
|
|
|
|
end
|
|
|
|
|
|
|
|
def i_am_child
|
|
|
|
true
|
|
|
|
end
|
|
|
|
end
|
2015-02-05 05:23:39 +08:00
|
|
|
|
|
|
|
class TroutPlugin < Plugin::Instance
|
|
|
|
attr_accessor :enabled
|
2024-08-01 02:38:10 +08:00
|
|
|
|
2018-12-04 11:48:13 +08:00
|
|
|
def enabled?
|
|
|
|
@enabled
|
|
|
|
end
|
2015-02-05 05:23:39 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
@plugin = TroutPlugin.new
|
|
|
|
@trout = Trout.new
|
|
|
|
|
2019-08-27 16:21:53 +08:00
|
|
|
poison = TroutSerializer.new(@trout)
|
|
|
|
poison.attributes
|
|
|
|
|
|
|
|
poison = TroutJuniorSerializer.new(@trout)
|
|
|
|
poison.attributes
|
|
|
|
|
2015-02-05 05:23:39 +08:00
|
|
|
# New method
|
|
|
|
@plugin.add_to_class(:trout, :status?) { "evil" }
|
|
|
|
|
|
|
|
# DiscourseEvent
|
|
|
|
@hello_count = 0
|
2016-09-05 15:48:59 +08:00
|
|
|
@increase_count = -> { @hello_count += 1 }
|
2016-09-05 16:10:03 +08:00
|
|
|
@set = @plugin.on(:hello, &@increase_count)
|
2015-02-05 05:23:39 +08:00
|
|
|
|
|
|
|
# Serializer
|
|
|
|
@plugin.add_to_serializer(:trout, :scales) { 1024 }
|
2023-04-24 19:17:51 +08:00
|
|
|
@plugin.add_to_serializer(:trout, :unconditional_scales, respect_plugin_enabled: false) do
|
|
|
|
2048
|
|
|
|
end
|
|
|
|
@plugin.add_to_serializer(
|
|
|
|
:trout,
|
|
|
|
:conditional_scales,
|
|
|
|
include_condition: -> { !!object.data&.[](:has_scales) },
|
|
|
|
) { 4096 }
|
2019-08-27 16:21:53 +08:00
|
|
|
|
2015-02-05 05:23:39 +08:00
|
|
|
@serializer = TroutSerializer.new(@trout)
|
2019-08-27 16:21:53 +08:00
|
|
|
@child_serializer = TroutJuniorSerializer.new(@trout)
|
2015-02-05 05:23:39 +08:00
|
|
|
end
|
|
|
|
|
2016-09-05 17:03:26 +08:00
|
|
|
after { DiscourseEvent.off(:hello, &@set.first) }
|
2015-02-05 06:33:18 +08:00
|
|
|
|
2015-02-05 05:23:39 +08:00
|
|
|
it "checks enabled/disabled functionality for extensions" do
|
|
|
|
# with an enabled plugin
|
|
|
|
@plugin.enabled = true
|
|
|
|
expect(@trout.status?).to eq("evil")
|
|
|
|
DiscourseEvent.trigger(:hello)
|
|
|
|
expect(@hello_count).to eq(1)
|
|
|
|
expect(@serializer.scales).to eq(1024)
|
|
|
|
expect(@serializer.include_scales?).to eq(true)
|
|
|
|
|
2019-08-27 16:21:53 +08:00
|
|
|
expect(@child_serializer.attributes[:scales]).to eq(1024)
|
|
|
|
|
2015-02-05 05:23:39 +08:00
|
|
|
# When a plugin is disabled
|
|
|
|
@plugin.enabled = false
|
|
|
|
expect(@trout.status?).to eq(nil)
|
|
|
|
DiscourseEvent.trigger(:hello)
|
|
|
|
expect(@hello_count).to eq(1)
|
|
|
|
expect(@serializer.scales).to eq(1024)
|
|
|
|
expect(@serializer.include_scales?).to eq(false)
|
2023-04-24 19:17:51 +08:00
|
|
|
expect(@serializer.include_unconditional_scales?).to eq(true)
|
2019-08-27 16:21:53 +08:00
|
|
|
expect(@serializer.name).to eq("a trout")
|
2015-02-05 05:23:39 +08:00
|
|
|
|
2019-08-27 16:21:53 +08:00
|
|
|
expect(@child_serializer.scales).to eq(1024)
|
|
|
|
expect(@child_serializer.include_scales?).to eq(false)
|
|
|
|
expect(@child_serializer.name).to eq("a trout jr")
|
2015-02-05 05:23:39 +08:00
|
|
|
end
|
2019-12-05 01:26:23 +08:00
|
|
|
|
2023-04-24 19:17:51 +08:00
|
|
|
it "can control the include_* implementation" do
|
|
|
|
@plugin.enabled = true
|
|
|
|
|
|
|
|
expect(@serializer.scales).to eq(1024)
|
|
|
|
expect(@serializer.include_scales?).to eq(true)
|
|
|
|
|
|
|
|
expect(@serializer.unconditional_scales).to eq(2048)
|
|
|
|
expect(@serializer.include_unconditional_scales?).to eq(true)
|
|
|
|
|
|
|
|
expect(@serializer.include_conditional_scales?).to eq(false)
|
|
|
|
@trout.data = { has_scales: true }
|
|
|
|
expect(@serializer.include_conditional_scales?).to eq(true)
|
|
|
|
|
|
|
|
@plugin.enabled = false
|
|
|
|
expect(@serializer.include_scales?).to eq(false)
|
|
|
|
expect(@serializer.include_unconditional_scales?).to eq(true)
|
|
|
|
expect(@serializer.include_conditional_scales?).to eq(false)
|
|
|
|
end
|
|
|
|
|
2019-12-05 01:26:23 +08:00
|
|
|
it "only returns HTML if enabled" do
|
|
|
|
ctx = Trout.new
|
|
|
|
ctx.data = "hello"
|
|
|
|
|
|
|
|
@plugin.register_html_builder("test:html") { |c| "<div>#{c.data}</div>" }
|
|
|
|
@plugin.enabled = false
|
|
|
|
expect(DiscoursePluginRegistry.build_html("test:html", ctx)).to eq("")
|
|
|
|
@plugin.enabled = true
|
|
|
|
expect(DiscoursePluginRegistry.build_html("test:html", ctx)).to eq("<div>hello</div>")
|
|
|
|
end
|
2024-08-01 02:38:10 +08:00
|
|
|
|
|
|
|
it "can act when the plugin is enabled/disabled" do
|
|
|
|
plugin = Plugin::Instance.new
|
|
|
|
plugin.enabled_site_setting(:discourse_sample_plugin_enabled)
|
|
|
|
|
|
|
|
SiteSetting.discourse_sample_plugin_enabled = false
|
|
|
|
expect(plugin.enabled?).to eq(false)
|
|
|
|
|
|
|
|
begin
|
|
|
|
expected_old_value = expected_new_value = nil
|
|
|
|
|
|
|
|
event_handler =
|
|
|
|
plugin.on_enabled_change do |old_value, new_value|
|
|
|
|
expected_old_value = old_value
|
|
|
|
expected_new_value = new_value
|
|
|
|
end
|
|
|
|
|
|
|
|
SiteSetting.discourse_sample_plugin_enabled = true
|
|
|
|
expect(expected_old_value).to eq(false)
|
|
|
|
expect(expected_new_value).to eq(true)
|
|
|
|
|
|
|
|
SiteSetting.discourse_sample_plugin_enabled = false
|
|
|
|
expect(expected_old_value).to eq(true)
|
|
|
|
expect(expected_new_value).to eq(false)
|
|
|
|
|
|
|
|
# ensures only the setting specified in `enabled_site_setting` is tracked
|
|
|
|
expected_old_value = expected_new_value = nil
|
|
|
|
plugin.enabled_site_setting(:discourse_sample_plugin_enabled_alternative)
|
|
|
|
SiteSetting.discourse_sample_plugin_enabled = true
|
|
|
|
expect(expected_old_value).to be_nil
|
|
|
|
expect(expected_new_value).to be_nil
|
|
|
|
ensure
|
|
|
|
# clear the underlying DiscourseEvent
|
|
|
|
DiscourseEvent.off(:site_setting_changed, &event_handler)
|
|
|
|
end
|
|
|
|
end
|
2015-02-05 05:23:39 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-07-28 00:14:14 +08:00
|
|
|
describe "register asset" do
|
2014-12-10 03:20:53 +08:00
|
|
|
it "populates the DiscoursePluginRegistry" do
|
2014-04-07 22:33:35 +08:00
|
|
|
plugin = Plugin::Instance.new nil, "/tmp/test.rb"
|
|
|
|
plugin.register_asset("test.css")
|
|
|
|
plugin.register_asset("test2.css")
|
|
|
|
|
2014-04-10 14:30:22 +08:00
|
|
|
plugin.send :register_assets!
|
|
|
|
|
2019-08-21 00:39:52 +08:00
|
|
|
expect(DiscoursePluginRegistry.mobile_stylesheets[plugin.directory_name]).to be_nil
|
|
|
|
expect(DiscoursePluginRegistry.stylesheets[plugin.directory_name].count).to eq(2)
|
2014-04-07 22:33:35 +08:00
|
|
|
end
|
2018-04-10 14:37:16 +08:00
|
|
|
|
|
|
|
it "remaps vendored_core_pretty_text asset" do
|
|
|
|
plugin = Plugin::Instance.new nil, "/tmp/test.rb"
|
|
|
|
plugin.register_asset("moment.js", :vendored_core_pretty_text)
|
|
|
|
|
|
|
|
plugin.send :register_assets!
|
|
|
|
|
2019-02-13 02:57:52 +08:00
|
|
|
expect(DiscoursePluginRegistry.vendored_core_pretty_text.first).to eq(
|
|
|
|
"vendor/assets/javascripts/moment.js",
|
|
|
|
)
|
2018-04-10 14:37:16 +08:00
|
|
|
end
|
2014-04-07 22:33:35 +08:00
|
|
|
end
|
|
|
|
|
2022-07-28 00:14:14 +08:00
|
|
|
describe "register service worker" do
|
2017-11-23 09:02:01 +08:00
|
|
|
it "populates the DiscoursePluginRegistry" do
|
|
|
|
plugin = Plugin::Instance.new nil, "/tmp/test.rb"
|
|
|
|
plugin.register_service_worker("test.js")
|
|
|
|
plugin.register_service_worker("test2.js")
|
|
|
|
|
|
|
|
plugin.send :register_service_workers!
|
|
|
|
|
|
|
|
expect(DiscoursePluginRegistry.service_workers.count).to eq(2)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-07-27 18:21:10 +08:00
|
|
|
describe "#add_report" do
|
2024-09-12 08:34:38 +08:00
|
|
|
after { Report.remove_report("readers") }
|
|
|
|
|
2018-06-19 21:00:11 +08:00
|
|
|
it "adds a report" do
|
|
|
|
plugin = Plugin::Instance.new nil, "/tmp/test.rb"
|
|
|
|
plugin.add_report("readers") {}
|
|
|
|
|
|
|
|
expect(Report.respond_to?(:report_readers)).to eq(true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-07-28 00:14:14 +08:00
|
|
|
describe "#activate!" do
|
2018-07-23 23:51:57 +08:00
|
|
|
before do
|
2019-05-07 09:00:09 +08:00
|
|
|
# lets piggy back on another boolean setting, so we don't dirty our SiteSetting object
|
|
|
|
SiteSetting.enable_badges = false
|
2018-07-23 23:51:57 +08:00
|
|
|
end
|
|
|
|
|
2013-08-23 14:21:52 +08:00
|
|
|
it "can activate plugins correctly" do
|
2023-06-05 08:06:00 +08:00
|
|
|
plugin = plugin_from_fixtures("my_plugin")
|
2013-08-23 14:21:52 +08:00
|
|
|
junk_file = "#{plugin.auto_generated_path}/junk"
|
|
|
|
|
|
|
|
plugin.ensure_directory(junk_file)
|
|
|
|
File.open("#{plugin.auto_generated_path}/junk", "w") { |f| f.write("junk") }
|
|
|
|
plugin.activate!
|
|
|
|
|
|
|
|
# calls ensure_assets! make sure they are there
|
2015-01-10 00:34:37 +08:00
|
|
|
expect(plugin.assets.count).to eq(1)
|
2022-01-06 01:45:08 +08:00
|
|
|
plugin.assets.each { |a, opts| expect(File.exist?(a)).to eq(true) }
|
2013-08-23 14:21:52 +08:00
|
|
|
|
|
|
|
# ensure it cleans up all crap in autogenerated directory
|
2022-01-06 01:45:08 +08:00
|
|
|
expect(File.exist?(junk_file)).to eq(false)
|
2013-08-23 14:21:52 +08:00
|
|
|
end
|
2014-04-07 22:33:35 +08:00
|
|
|
|
2018-07-23 23:51:57 +08:00
|
|
|
it "registers auth providers correctly" do
|
2023-06-05 08:06:00 +08:00
|
|
|
plugin = plugin_from_fixtures("my_plugin")
|
2018-07-23 23:51:57 +08:00
|
|
|
plugin.activate!
|
2018-12-01 00:58:18 +08:00
|
|
|
expect(DiscoursePluginRegistry.auth_providers.count).to eq(0)
|
2023-10-26 17:54:30 +08:00
|
|
|
plugin.notify_after_initialize
|
2018-07-23 23:51:57 +08:00
|
|
|
expect(DiscoursePluginRegistry.auth_providers.count).to eq(1)
|
2018-12-01 00:58:18 +08:00
|
|
|
auth_provider = DiscoursePluginRegistry.auth_providers.to_a[0]
|
2020-02-08 01:32:35 +08:00
|
|
|
expect(auth_provider.authenticator.name).to eq("facebook")
|
2018-07-23 23:51:57 +08:00
|
|
|
end
|
|
|
|
|
2014-04-07 22:33:35 +08:00
|
|
|
it "finds all the custom assets" do
|
2023-06-05 08:06:00 +08:00
|
|
|
plugin = plugin_from_fixtures("my_plugin")
|
2014-04-10 14:30:22 +08:00
|
|
|
|
2014-04-07 22:33:35 +08:00
|
|
|
plugin.register_asset("test.css")
|
|
|
|
plugin.register_asset("test2.scss")
|
2014-04-10 14:30:22 +08:00
|
|
|
plugin.register_asset("mobile.css", :mobile)
|
|
|
|
plugin.register_asset("desktop.css", :desktop)
|
|
|
|
plugin.register_asset("desktop2.css", :desktop)
|
|
|
|
|
2014-04-07 22:33:35 +08:00
|
|
|
plugin.activate!
|
|
|
|
|
2024-02-01 19:48:31 +08:00
|
|
|
expect(DiscoursePluginRegistry.javascripts.count).to eq(1)
|
2019-08-21 00:39:52 +08:00
|
|
|
expect(DiscoursePluginRegistry.desktop_stylesheets[plugin.directory_name].count).to eq(2)
|
|
|
|
expect(DiscoursePluginRegistry.stylesheets[plugin.directory_name].count).to eq(2)
|
|
|
|
expect(DiscoursePluginRegistry.mobile_stylesheets[plugin.directory_name].count).to eq(1)
|
2014-04-07 22:33:35 +08:00
|
|
|
end
|
2013-08-23 14:21:52 +08:00
|
|
|
end
|
|
|
|
|
2022-07-28 00:14:14 +08:00
|
|
|
describe "serialized_current_user_fields" do
|
2017-11-15 08:55:37 +08:00
|
|
|
before { DiscoursePluginRegistry.serialized_current_user_fields << "has_car" }
|
|
|
|
|
|
|
|
after { DiscoursePluginRegistry.serialized_current_user_fields.delete "has_car" }
|
|
|
|
|
2014-06-11 09:57:22 +08:00
|
|
|
it "correctly serializes custom user fields" do
|
|
|
|
DiscoursePluginRegistry.serialized_current_user_fields << "has_car"
|
|
|
|
user = Fabricate(:user)
|
|
|
|
user.custom_fields["has_car"] = "true"
|
|
|
|
user.save!
|
|
|
|
|
|
|
|
payload = JSON.parse(CurrentUserSerializer.new(user, scope: Guardian.new(user)).to_json)
|
2015-01-10 00:34:37 +08:00
|
|
|
expect(payload["current_user"]["custom_fields"]["has_car"]).to eq("true")
|
2017-11-15 08:55:37 +08:00
|
|
|
|
|
|
|
payload = JSON.parse(UserSerializer.new(user, scope: Guardian.new(user)).to_json)
|
|
|
|
expect(payload["user"]["custom_fields"]["has_car"]).to eq("true")
|
|
|
|
|
|
|
|
UserCustomField.destroy_all
|
|
|
|
user.reload
|
|
|
|
|
|
|
|
payload = JSON.parse(CurrentUserSerializer.new(user, scope: Guardian.new(user)).to_json)
|
|
|
|
expect(payload["current_user"]["custom_fields"]).to eq({})
|
|
|
|
|
|
|
|
payload = JSON.parse(UserSerializer.new(user, scope: Guardian.new(user)).to_json)
|
|
|
|
expect(payload["user"]["custom_fields"]).to eq({})
|
2014-06-11 09:57:22 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-10-25 14:55:53 +08:00
|
|
|
describe ".register_seedfu_fixtures" do
|
|
|
|
it "should add the new path to SeedFu's fixtures path" do
|
|
|
|
plugin = Plugin::Instance.new nil, "/tmp/test.rb"
|
|
|
|
plugin.register_seedfu_fixtures(["some_path"])
|
|
|
|
plugin.register_seedfu_fixtures("some_path2")
|
|
|
|
|
|
|
|
expect(SeedFu.fixture_paths).to include("some_path")
|
|
|
|
expect(SeedFu.fixture_paths).to include("some_path2")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-11-21 10:20:31 +08:00
|
|
|
describe "#add_model_callback" do
|
|
|
|
let(:metadata) do
|
|
|
|
metadata = Plugin::Metadata.new
|
|
|
|
metadata.name = "test"
|
|
|
|
metadata
|
|
|
|
end
|
|
|
|
|
|
|
|
let(:plugin_instance) do
|
|
|
|
plugin = Plugin::Instance.new(nil, "/tmp/test.rb")
|
|
|
|
plugin.metadata = metadata
|
|
|
|
plugin
|
|
|
|
end
|
|
|
|
|
|
|
|
it "should add the right callback" do
|
|
|
|
called = 0
|
|
|
|
|
2017-01-13 04:43:09 +08:00
|
|
|
plugin_instance.add_model_callback(User, :after_create) { called += 1 }
|
2016-11-21 10:20:31 +08:00
|
|
|
|
|
|
|
user = Fabricate(:user)
|
|
|
|
|
|
|
|
expect(called).to eq(1)
|
|
|
|
|
2019-04-29 15:32:25 +08:00
|
|
|
user.update!(username: "some_username")
|
2016-11-21 10:20:31 +08:00
|
|
|
|
|
|
|
expect(called).to eq(1)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "should add the right callback with options" do
|
|
|
|
called = 0
|
|
|
|
|
2017-01-13 04:43:09 +08:00
|
|
|
plugin_instance.add_model_callback(User, :after_commit, on: :create) { called += 1 }
|
2016-11-21 10:20:31 +08:00
|
|
|
|
|
|
|
user = Fabricate(:user)
|
|
|
|
|
|
|
|
expect(called).to eq(1)
|
|
|
|
|
2019-04-29 15:32:25 +08:00
|
|
|
user.update!(username: "some_username")
|
2016-11-21 10:20:31 +08:00
|
|
|
|
|
|
|
expect(called).to eq(1)
|
|
|
|
end
|
|
|
|
end
|
2018-01-25 19:09:18 +08:00
|
|
|
|
2022-07-28 00:14:14 +08:00
|
|
|
describe "locales" do
|
2023-06-05 08:06:00 +08:00
|
|
|
let!(:plugin) { plugin_from_fixtures("custom_locales") }
|
|
|
|
let(:plugin_path) { File.dirname(plugin.path) }
|
2018-01-25 19:09:18 +08:00
|
|
|
let(:plural) do
|
|
|
|
{
|
|
|
|
keys: %i[one few other],
|
|
|
|
rule:
|
|
|
|
lambda do |n|
|
|
|
|
return :one if n == 1
|
|
|
|
return :few if n < 10
|
|
|
|
:other
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
def register_locale(locale, opts)
|
|
|
|
plugin.register_locale(locale, opts)
|
|
|
|
plugin.activate!
|
|
|
|
|
|
|
|
DiscoursePluginRegistry.locales[locale]
|
|
|
|
end
|
|
|
|
|
|
|
|
it "enables the registered locales only on activate" do
|
2019-02-19 22:27:30 +08:00
|
|
|
plugin.register_locale("foo_BAR", name: "Foo", nativeName: "Foo Bar", plural: plural)
|
2020-11-11 09:34:26 +08:00
|
|
|
plugin.register_locale("tup", name: "Tupi", nativeName: "Tupi", fallbackLocale: "pt_BR")
|
2018-01-25 19:09:18 +08:00
|
|
|
expect(DiscoursePluginRegistry.locales.count).to eq(0)
|
|
|
|
|
|
|
|
plugin.activate!
|
2019-02-26 03:40:02 +08:00
|
|
|
|
2018-01-25 19:09:18 +08:00
|
|
|
expect(DiscoursePluginRegistry.locales.count).to eq(2)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "allows finding the locale by string and symbol" do
|
2019-02-19 22:27:30 +08:00
|
|
|
register_locale("foo_BAR", name: "Foo", nativeName: "Foo Bar", plural: plural)
|
2018-01-25 19:09:18 +08:00
|
|
|
|
2019-02-19 22:27:30 +08:00
|
|
|
expect(DiscoursePluginRegistry.locales).to have_key(:foo_BAR)
|
|
|
|
expect(DiscoursePluginRegistry.locales).to have_key("foo_BAR")
|
2018-01-25 19:09:18 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "correctly registers a new locale" do
|
2019-02-19 22:27:30 +08:00
|
|
|
locale = register_locale("foo_BAR", name: "Foo", nativeName: "Foo Bar", plural: plural)
|
2018-01-25 19:09:18 +08:00
|
|
|
|
|
|
|
expect(DiscoursePluginRegistry.locales.count).to eq(1)
|
2019-02-19 22:27:30 +08:00
|
|
|
expect(DiscoursePluginRegistry.locales).to have_key(:foo_BAR)
|
2018-01-25 19:09:18 +08:00
|
|
|
|
|
|
|
expect(locale[:fallbackLocale]).to be_nil
|
2019-02-19 22:27:30 +08:00
|
|
|
expect(locale[:moment_js]).to eq(
|
|
|
|
["foo_BAR", "#{plugin_path}/lib/javascripts/locale/moment_js/foo_BAR.js"],
|
|
|
|
)
|
2019-02-26 03:40:02 +08:00
|
|
|
expect(locale[:moment_js_timezones]).to eq(
|
|
|
|
["foo_BAR", "#{plugin_path}/lib/javascripts/locale/moment_js_timezones/foo_BAR.js"],
|
|
|
|
)
|
2018-01-25 19:09:18 +08:00
|
|
|
expect(locale[:plural]).to eq(plural.with_indifferent_access)
|
|
|
|
|
2019-02-19 22:27:30 +08:00
|
|
|
expect(Rails.configuration.assets.precompile).to include("locales/foo_BAR.js")
|
|
|
|
|
2023-12-07 06:25:00 +08:00
|
|
|
expect(JsLocaleHelper.find_moment_locale(["foo_BAR"])).to eq(locale[:moment_js])
|
|
|
|
expect(JsLocaleHelper.find_moment_locale(["foo_BAR"], timezone_names: true)).to eq(
|
|
|
|
locale[:moment_js_timezones],
|
|
|
|
)
|
2018-01-25 19:09:18 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "correctly registers a new locale using a fallback locale" do
|
2020-11-11 09:34:26 +08:00
|
|
|
locale = register_locale("tup", name: "Tupi", nativeName: "Tupi", fallbackLocale: "pt_BR")
|
2018-01-25 19:09:18 +08:00
|
|
|
|
|
|
|
expect(DiscoursePluginRegistry.locales.count).to eq(1)
|
2020-11-11 09:34:26 +08:00
|
|
|
expect(DiscoursePluginRegistry.locales).to have_key(:tup)
|
2018-01-25 19:09:18 +08:00
|
|
|
|
2020-11-11 09:34:26 +08:00
|
|
|
expect(locale[:fallbackLocale]).to eq("pt_BR")
|
|
|
|
expect(locale[:moment_js]).to eq(
|
|
|
|
["pt-br", "#{Rails.root}/vendor/assets/javascripts/moment-locale/pt-br.js"],
|
|
|
|
)
|
|
|
|
expect(locale[:moment_js_timezones]).to eq(
|
|
|
|
["pt", "#{Rails.root}/vendor/assets/javascripts/moment-timezone-names-locale/pt.js"],
|
|
|
|
)
|
2018-01-25 19:09:18 +08:00
|
|
|
expect(locale[:plural]).to be_nil
|
|
|
|
|
2020-11-11 09:34:26 +08:00
|
|
|
expect(Rails.configuration.assets.precompile).to include("locales/tup.js")
|
2019-02-19 22:27:30 +08:00
|
|
|
|
2023-12-07 06:25:00 +08:00
|
|
|
expect(JsLocaleHelper.find_moment_locale(["tup"])).to eq(locale[:moment_js])
|
2018-01-25 19:09:18 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "correctly registers a new locale when some files exist in core" do
|
|
|
|
locale = register_locale("tlh", name: "Klingon", nativeName: "tlhIngan Hol", plural: plural)
|
|
|
|
|
|
|
|
expect(DiscoursePluginRegistry.locales.count).to eq(1)
|
|
|
|
expect(DiscoursePluginRegistry.locales).to have_key(:tlh)
|
|
|
|
|
|
|
|
expect(locale[:fallbackLocale]).to be_nil
|
2019-02-13 02:57:52 +08:00
|
|
|
expect(locale[:moment_js]).to eq(
|
|
|
|
["tlh", "#{Rails.root}/vendor/assets/javascripts/moment-locale/tlh.js"],
|
|
|
|
)
|
2018-01-25 19:09:18 +08:00
|
|
|
expect(locale[:plural]).to eq(plural.with_indifferent_access)
|
|
|
|
|
|
|
|
expect(Rails.configuration.assets.precompile).to include("locales/tlh.js")
|
2019-02-19 22:27:30 +08:00
|
|
|
|
2023-12-07 06:25:00 +08:00
|
|
|
expect(JsLocaleHelper.find_moment_locale(["tlh"])).to eq(locale[:moment_js])
|
2018-01-25 19:09:18 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "does not register a new locale when the fallback locale does not exist" do
|
|
|
|
register_locale("bar", name: "Bar", nativeName: "Bar", fallbackLocale: "foo")
|
|
|
|
expect(DiscoursePluginRegistry.locales.count).to eq(0)
|
|
|
|
end
|
|
|
|
|
|
|
|
%w[
|
2019-02-19 22:27:30 +08:00
|
|
|
config/locales/client.foo_BAR.yml
|
|
|
|
config/locales/server.foo_BAR.yml
|
|
|
|
lib/javascripts/locale/moment_js/foo_BAR.js
|
|
|
|
assets/locales/foo_BAR.js.erb
|
2018-01-25 19:09:18 +08:00
|
|
|
].each do |path|
|
|
|
|
it "does not register a new locale when #{path} is missing" do
|
|
|
|
path = "#{plugin_path}/#{path}"
|
|
|
|
File.stubs("exist?").returns(false)
|
|
|
|
File.stubs("exist?").with(regexp_matches(/#{Regexp.quote(plugin_path)}.*/)).returns(true)
|
|
|
|
File.stubs("exist?").with(path).returns(false)
|
|
|
|
|
2019-02-19 22:27:30 +08:00
|
|
|
register_locale("foo_BAR", name: "Foo", nativeName: "Foo Bar", plural: plural)
|
2018-01-25 19:09:18 +08:00
|
|
|
expect(DiscoursePluginRegistry.locales.count).to eq(0)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-07-09 23:56:22 +08:00
|
|
|
describe "#register_reviewable_type" do
|
|
|
|
subject(:register_reviewable_type) { plugin_instance.register_reviewable_type(new_type) }
|
2019-01-04 01:03:01 +08:00
|
|
|
|
2024-07-09 23:56:22 +08:00
|
|
|
context "when the provided class inherits from `Reviewable`" do
|
|
|
|
let(:new_type) { Class.new(Reviewable) }
|
2019-01-04 01:03:01 +08:00
|
|
|
|
2024-07-09 23:56:22 +08:00
|
|
|
it "adds the provided class to the existing types" do
|
|
|
|
expect { register_reviewable_type }.to change { Reviewable.types.size }.by(1)
|
|
|
|
expect(Reviewable.types).to include(new_type)
|
|
|
|
end
|
|
|
|
|
|
|
|
context "when the plugin is disabled" do
|
|
|
|
before do
|
|
|
|
register_reviewable_type
|
|
|
|
plugin_instance.stubs(:enabled?).returns(false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "does not return the new type" do
|
|
|
|
expect(Reviewable.types).not_to be_blank
|
|
|
|
expect(Reviewable.types).not_to include(new_type)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context "when the provided class does not inherit from `Reviewable`" do
|
|
|
|
let(:new_type) { Class }
|
|
|
|
|
|
|
|
it "does not add the provided class to the existing types" do
|
|
|
|
expect { register_reviewable_type }.not_to change { Reviewable.types }
|
|
|
|
expect(Reviewable.types).not_to be_blank
|
|
|
|
end
|
2019-04-09 01:42:36 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "#extend_list_method" do
|
2024-07-09 23:56:22 +08:00
|
|
|
subject(:extend_list) do
|
|
|
|
plugin_instance.extend_list_method(UserHistory, :staff_actions, %i[new_action another_action])
|
|
|
|
end
|
|
|
|
|
|
|
|
it "adds the provided values to the provided method on the provided class" do
|
|
|
|
expect { extend_list }.to change { UserHistory.staff_actions.size }.by(2)
|
|
|
|
expect(UserHistory.staff_actions).to include(:new_action, :another_action)
|
|
|
|
end
|
2019-04-09 01:42:36 +08:00
|
|
|
|
2024-07-09 23:56:22 +08:00
|
|
|
context "when the plugin is disabled" do
|
|
|
|
before do
|
|
|
|
extend_list
|
|
|
|
plugin_instance.stubs(:enabled?).returns(false)
|
|
|
|
end
|
2019-04-09 01:42:36 +08:00
|
|
|
|
2024-07-09 23:56:22 +08:00
|
|
|
it "does not return the provided values" do
|
|
|
|
expect(UserHistory.staff_actions).not_to be_blank
|
|
|
|
expect(UserHistory.staff_actions).not_to include(:new_action, :another_action)
|
|
|
|
end
|
2019-01-04 01:03:01 +08:00
|
|
|
end
|
|
|
|
end
|
2020-03-31 02:16:10 +08:00
|
|
|
|
|
|
|
describe "#register_emoji" do
|
|
|
|
before { Plugin::CustomEmoji.clear_cache }
|
|
|
|
|
2020-04-07 01:41:59 +08:00
|
|
|
after { Plugin::CustomEmoji.clear_cache }
|
|
|
|
|
2020-03-31 02:16:10 +08:00
|
|
|
it "allows to register an emoji" do
|
|
|
|
Plugin::Instance.new.register_emoji("foo", "/foo/bar.png")
|
|
|
|
|
|
|
|
custom_emoji = Emoji.custom.first
|
|
|
|
|
|
|
|
expect(custom_emoji.name).to eq("foo")
|
|
|
|
expect(custom_emoji.url).to eq("/foo/bar.png")
|
|
|
|
expect(custom_emoji.group).to eq(Emoji::DEFAULT_GROUP)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "allows to register an emoji with a group" do
|
|
|
|
Plugin::Instance.new.register_emoji("bar", "/baz/bar.png", "baz")
|
|
|
|
|
|
|
|
custom_emoji = Emoji.custom.first
|
|
|
|
|
|
|
|
expect(custom_emoji.name).to eq("bar")
|
|
|
|
expect(custom_emoji.url).to eq("/baz/bar.png")
|
|
|
|
expect(custom_emoji.group).to eq("baz")
|
|
|
|
end
|
2023-07-20 03:09:26 +08:00
|
|
|
|
|
|
|
it "sanitizes emojis' names" do
|
|
|
|
Plugin::Instance.new.register_emoji("?", "/baz/bar.png", "baz")
|
|
|
|
Plugin::Instance.new.register_emoji("?test?!!", "/foo/bar.png", "baz")
|
|
|
|
|
|
|
|
expect(Emoji.custom.first.name).to eq("_")
|
|
|
|
expect(Emoji.custom.second.name).to eq("_test_")
|
|
|
|
end
|
2020-03-31 02:16:10 +08:00
|
|
|
end
|
2020-07-02 22:47:43 +08:00
|
|
|
|
|
|
|
describe "#replace_flags" do
|
2024-05-27 19:57:41 +08:00
|
|
|
after do
|
|
|
|
PostActionType.replace_flag_settings(nil)
|
|
|
|
Flag.reset_flag_settings!
|
|
|
|
end
|
2020-07-07 00:09:56 +08:00
|
|
|
|
2020-07-02 22:47:43 +08:00
|
|
|
let(:original_flags) { PostActionType.flag_settings }
|
|
|
|
|
|
|
|
it "adds a new flag" do
|
|
|
|
highest_flag_id = ReviewableScore.types.values.max
|
|
|
|
flag_name = :new_flag
|
|
|
|
|
2023-06-21 22:00:19 +08:00
|
|
|
plugin_instance.replace_flags(settings: original_flags) do |settings, next_flag_id|
|
2020-07-02 22:47:43 +08:00
|
|
|
settings.add(next_flag_id, flag_name)
|
|
|
|
end
|
|
|
|
|
|
|
|
expect(PostActionType.flag_settings.flag_types.keys).to include(flag_name)
|
|
|
|
expect(PostActionType.flag_settings.flag_types.values.max).to eq(highest_flag_id + 1)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "adds a new score type after adding a new flag" do
|
|
|
|
highest_flag_id = ReviewableScore.types.values.max
|
|
|
|
new_score_type = :new_score_type
|
|
|
|
|
2023-06-21 22:00:19 +08:00
|
|
|
plugin_instance.replace_flags(
|
2020-07-02 22:47:43 +08:00
|
|
|
settings: original_flags,
|
|
|
|
score_type_names: [new_score_type],
|
|
|
|
) { |settings, next_flag_id| settings.add(next_flag_id, :new_flag) }
|
|
|
|
|
|
|
|
expect(PostActionType.flag_settings.flag_types.values.max).to eq(highest_flag_id + 1)
|
|
|
|
expect(ReviewableScore.types.keys).to include(new_score_type)
|
|
|
|
expect(ReviewableScore.types.values.max).to eq(highest_flag_id + 2)
|
|
|
|
end
|
|
|
|
end
|
2021-02-18 01:42:44 +08:00
|
|
|
|
|
|
|
describe "#add_api_key_scope" do
|
2021-02-19 03:05:44 +08:00
|
|
|
after { DiscoursePluginRegistry.reset! }
|
|
|
|
|
2021-02-18 01:42:44 +08:00
|
|
|
it "adds a custom api key scope" do
|
|
|
|
actions = %w[admin/groups#create]
|
2023-06-21 22:00:19 +08:00
|
|
|
plugin_instance.add_api_key_scope(:groups, create: { actions: actions })
|
2021-02-18 01:42:44 +08:00
|
|
|
|
|
|
|
expect(ApiKeyScope.scope_mappings.dig(:groups, :create, :actions)).to contain_exactly(
|
|
|
|
*actions,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
2021-06-23 02:00:04 +08:00
|
|
|
|
|
|
|
describe "#add_directory_column" do
|
|
|
|
let!(:plugin) { Plugin::Instance.new }
|
|
|
|
|
|
|
|
before { DirectoryItem.clear_plugin_queries }
|
|
|
|
|
|
|
|
after { DirectoryColumn.clear_plugin_directory_columns }
|
|
|
|
|
|
|
|
describe "with valid column name" do
|
|
|
|
let(:column_name) { "random_c" }
|
|
|
|
|
|
|
|
before do
|
|
|
|
DB.exec("ALTER TABLE directory_items ADD COLUMN IF NOT EXISTS #{column_name} integer")
|
|
|
|
end
|
|
|
|
|
|
|
|
after do
|
|
|
|
DB.exec("ALTER TABLE directory_items DROP COLUMN IF EXISTS #{column_name}")
|
|
|
|
DiscourseEvent.all_off("before_directory_refresh")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "creates a directory column record when directory items are refreshed" do
|
|
|
|
plugin.add_directory_column(
|
|
|
|
column_name,
|
|
|
|
query: "SELECT COUNT(*) FROM users",
|
|
|
|
icon: "recycle",
|
|
|
|
)
|
|
|
|
expect(
|
|
|
|
DirectoryColumn.find_by(name: column_name, icon: "recycle", enabled: false),
|
|
|
|
).not_to be_present
|
|
|
|
|
|
|
|
DirectoryItem.refresh!
|
|
|
|
expect(
|
|
|
|
DirectoryColumn.find_by(name: column_name, icon: "recycle", enabled: false),
|
|
|
|
).to be_present
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it "errors when the column_name contains invalid characters" do
|
|
|
|
expect {
|
|
|
|
plugin.add_directory_column("Capital", query: "SELECT COUNT(*) FROM users", icon: "recycle")
|
|
|
|
}.to raise_error(RuntimeError)
|
|
|
|
|
|
|
|
expect {
|
|
|
|
plugin.add_directory_column(
|
|
|
|
"has space",
|
|
|
|
query: "SELECT COUNT(*) FROM users",
|
|
|
|
icon: "recycle",
|
|
|
|
)
|
|
|
|
}.to raise_error(RuntimeError)
|
|
|
|
|
|
|
|
expect {
|
|
|
|
plugin.add_directory_column(
|
|
|
|
"has_number_1",
|
|
|
|
query: "SELECT COUNT(*) FROM users",
|
|
|
|
icon: "recycle",
|
|
|
|
)
|
|
|
|
}.to raise_error(RuntimeError)
|
|
|
|
end
|
|
|
|
end
|
2021-07-19 13:54:19 +08:00
|
|
|
|
|
|
|
describe "#register_site_categories_callback" do
|
2023-11-10 06:47:59 +08:00
|
|
|
fab!(:category)
|
2021-07-19 13:54:19 +08:00
|
|
|
|
|
|
|
it "adds a callback to the Site#categories" do
|
|
|
|
instance = Plugin::Instance.new
|
|
|
|
|
2022-12-13 07:49:13 +08:00
|
|
|
site_guardian = Guardian.new
|
|
|
|
|
|
|
|
instance.register_site_categories_callback do |categories, guardian|
|
2021-07-19 13:54:19 +08:00
|
|
|
categories.each { |category| category[:test_field] = "test" }
|
2022-12-13 07:49:13 +08:00
|
|
|
|
|
|
|
expect(guardian).to eq(site_guardian)
|
2021-07-19 13:54:19 +08:00
|
|
|
end
|
|
|
|
|
2022-12-13 07:49:13 +08:00
|
|
|
site = Site.new(site_guardian)
|
2021-07-19 13:54:19 +08:00
|
|
|
|
|
|
|
expect(site.categories.first[:test_field]).to eq("test")
|
|
|
|
ensure
|
|
|
|
Site.clear_cache
|
|
|
|
Site.categories_callbacks.clear
|
|
|
|
end
|
|
|
|
end
|
REFACTOR: Improve support for consolidating notifications. (#14904)
* REFACTOR: Improve support for consolidating notifications.
Before this commit, we didn't have a single way of consolidating notifications. For notifications like group summaries, we manually removed old ones before creating a new one. On the other hand, we used an after_create callback for likes and group membership requests, which caused unnecessary work, as we need to delete the record we created to replace it with a consolidated one.
We now have all the consolidation rules centralized in a single place: the consolidation planner class. Other parts of the app looking to create a consolidable notification can do so by calling Notification#consolidate_or_save!, instead of the default Notification#create! method.
Finally, we added two more rules: one for re-using existing group summaries and another for deleting duplicated dashboard problems PMs notifications when the user is tracking the moderator's inbox. Setting the threshold to one forces the planner to apply this rule every time.
I plan to add plugin support for adding custom rules in another PR to keep this one relatively small.
* DEV: Introduces a plugin API for consolidating notifications.
This commit removes the `Notification#filter_by_consolidation_data` scope since plugins could have to define their criteria. The Plan class now receives two blocks, one to query for an already consolidated notification, which we'll try to update, and another to query for existing ones to consolidate.
It also receives a consolidation window, which accepts an ActiveSupport::Duration object, and filter notifications created since that value.
2021-12-01 00:36:14 +08:00
|
|
|
|
|
|
|
describe "#register_notification_consolidation_plan" do
|
|
|
|
let(:plugin) { Plugin::Instance.new }
|
2023-11-10 06:47:59 +08:00
|
|
|
fab!(:topic)
|
REFACTOR: Improve support for consolidating notifications. (#14904)
* REFACTOR: Improve support for consolidating notifications.
Before this commit, we didn't have a single way of consolidating notifications. For notifications like group summaries, we manually removed old ones before creating a new one. On the other hand, we used an after_create callback for likes and group membership requests, which caused unnecessary work, as we need to delete the record we created to replace it with a consolidated one.
We now have all the consolidation rules centralized in a single place: the consolidation planner class. Other parts of the app looking to create a consolidable notification can do so by calling Notification#consolidate_or_save!, instead of the default Notification#create! method.
Finally, we added two more rules: one for re-using existing group summaries and another for deleting duplicated dashboard problems PMs notifications when the user is tracking the moderator's inbox. Setting the threshold to one forces the planner to apply this rule every time.
I plan to add plugin support for adding custom rules in another PR to keep this one relatively small.
* DEV: Introduces a plugin API for consolidating notifications.
This commit removes the `Notification#filter_by_consolidation_data` scope since plugins could have to define their criteria. The Plan class now receives two blocks, one to query for an already consolidated notification, which we'll try to update, and another to query for existing ones to consolidate.
It also receives a consolidation window, which accepts an ActiveSupport::Duration object, and filter notifications created since that value.
2021-12-01 00:36:14 +08:00
|
|
|
|
|
|
|
after { DiscoursePluginRegistry.reset_register!(:notification_consolidation_plans) }
|
|
|
|
|
|
|
|
it "fails when the received object is not a consolidation plan" do
|
|
|
|
expect { plugin.register_notification_consolidation_plan(Object.new) }.to raise_error(
|
|
|
|
ArgumentError,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "registers a consolidation plan and uses it" do
|
|
|
|
plan =
|
|
|
|
Notifications::ConsolidateNotifications.new(
|
|
|
|
from: Notification.types[:code_review_commit_approved],
|
|
|
|
to: Notification.types[:code_review_commit_approved],
|
|
|
|
threshold: 1,
|
|
|
|
consolidation_window: 1.minute,
|
2023-11-29 13:38:07 +08:00
|
|
|
unconsolidated_query_blk: ->(notifications, _data) do
|
REFACTOR: Improve support for consolidating notifications. (#14904)
* REFACTOR: Improve support for consolidating notifications.
Before this commit, we didn't have a single way of consolidating notifications. For notifications like group summaries, we manually removed old ones before creating a new one. On the other hand, we used an after_create callback for likes and group membership requests, which caused unnecessary work, as we need to delete the record we created to replace it with a consolidated one.
We now have all the consolidation rules centralized in a single place: the consolidation planner class. Other parts of the app looking to create a consolidable notification can do so by calling Notification#consolidate_or_save!, instead of the default Notification#create! method.
Finally, we added two more rules: one for re-using existing group summaries and another for deleting duplicated dashboard problems PMs notifications when the user is tracking the moderator's inbox. Setting the threshold to one forces the planner to apply this rule every time.
I plan to add plugin support for adding custom rules in another PR to keep this one relatively small.
* DEV: Introduces a plugin API for consolidating notifications.
This commit removes the `Notification#filter_by_consolidation_data` scope since plugins could have to define their criteria. The Plan class now receives two blocks, one to query for an already consolidated notification, which we'll try to update, and another to query for existing ones to consolidate.
It also receives a consolidation window, which accepts an ActiveSupport::Duration object, and filter notifications created since that value.
2021-12-01 00:36:14 +08:00
|
|
|
notifications.where("(data::json ->> 'consolidated') IS NULL")
|
2023-11-29 13:38:07 +08:00
|
|
|
end,
|
|
|
|
consolidated_query_blk: ->(notifications, _data) do
|
REFACTOR: Improve support for consolidating notifications. (#14904)
* REFACTOR: Improve support for consolidating notifications.
Before this commit, we didn't have a single way of consolidating notifications. For notifications like group summaries, we manually removed old ones before creating a new one. On the other hand, we used an after_create callback for likes and group membership requests, which caused unnecessary work, as we need to delete the record we created to replace it with a consolidated one.
We now have all the consolidation rules centralized in a single place: the consolidation planner class. Other parts of the app looking to create a consolidable notification can do so by calling Notification#consolidate_or_save!, instead of the default Notification#create! method.
Finally, we added two more rules: one for re-using existing group summaries and another for deleting duplicated dashboard problems PMs notifications when the user is tracking the moderator's inbox. Setting the threshold to one forces the planner to apply this rule every time.
I plan to add plugin support for adding custom rules in another PR to keep this one relatively small.
* DEV: Introduces a plugin API for consolidating notifications.
This commit removes the `Notification#filter_by_consolidation_data` scope since plugins could have to define their criteria. The Plan class now receives two blocks, one to query for an already consolidated notification, which we'll try to update, and another to query for existing ones to consolidate.
It also receives a consolidation window, which accepts an ActiveSupport::Duration object, and filter notifications created since that value.
2021-12-01 00:36:14 +08:00
|
|
|
notifications.where("(data::json ->> 'consolidated') IS NOT NULL")
|
2023-11-29 13:38:07 +08:00
|
|
|
end,
|
REFACTOR: Improve support for consolidating notifications. (#14904)
* REFACTOR: Improve support for consolidating notifications.
Before this commit, we didn't have a single way of consolidating notifications. For notifications like group summaries, we manually removed old ones before creating a new one. On the other hand, we used an after_create callback for likes and group membership requests, which caused unnecessary work, as we need to delete the record we created to replace it with a consolidated one.
We now have all the consolidation rules centralized in a single place: the consolidation planner class. Other parts of the app looking to create a consolidable notification can do so by calling Notification#consolidate_or_save!, instead of the default Notification#create! method.
Finally, we added two more rules: one for re-using existing group summaries and another for deleting duplicated dashboard problems PMs notifications when the user is tracking the moderator's inbox. Setting the threshold to one forces the planner to apply this rule every time.
I plan to add plugin support for adding custom rules in another PR to keep this one relatively small.
* DEV: Introduces a plugin API for consolidating notifications.
This commit removes the `Notification#filter_by_consolidation_data` scope since plugins could have to define their criteria. The Plan class now receives two blocks, one to query for an already consolidated notification, which we'll try to update, and another to query for existing ones to consolidate.
It also receives a consolidation window, which accepts an ActiveSupport::Duration object, and filter notifications created since that value.
2021-12-01 00:36:14 +08:00
|
|
|
).set_mutations(
|
|
|
|
set_data_blk: ->(notification) { notification.data_hash.merge(consolidated: true) },
|
|
|
|
)
|
|
|
|
|
|
|
|
plugin.register_notification_consolidation_plan(plan)
|
|
|
|
|
|
|
|
create_notification!
|
|
|
|
create_notification!
|
|
|
|
|
|
|
|
expect(commit_approved_notifications.count).to eq(1)
|
|
|
|
consolidated_notification = commit_approved_notifications.last
|
|
|
|
expect(consolidated_notification.data_hash[:consolidated]).to eq(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
def commit_approved_notifications
|
|
|
|
Notification.where(
|
|
|
|
user: topic.user,
|
|
|
|
notification_type: Notification.types[:code_review_commit_approved],
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
def create_notification!
|
|
|
|
Notification.consolidate_or_create!(
|
|
|
|
notification_type: Notification.types[:code_review_commit_approved],
|
|
|
|
topic_id: topic.id,
|
|
|
|
user: topic.user,
|
|
|
|
data: {
|
2023-01-09 19:18:21 +08:00
|
|
|
},
|
REFACTOR: Improve support for consolidating notifications. (#14904)
* REFACTOR: Improve support for consolidating notifications.
Before this commit, we didn't have a single way of consolidating notifications. For notifications like group summaries, we manually removed old ones before creating a new one. On the other hand, we used an after_create callback for likes and group membership requests, which caused unnecessary work, as we need to delete the record we created to replace it with a consolidated one.
We now have all the consolidation rules centralized in a single place: the consolidation planner class. Other parts of the app looking to create a consolidable notification can do so by calling Notification#consolidate_or_save!, instead of the default Notification#create! method.
Finally, we added two more rules: one for re-using existing group summaries and another for deleting duplicated dashboard problems PMs notifications when the user is tracking the moderator's inbox. Setting the threshold to one forces the planner to apply this rule every time.
I plan to add plugin support for adding custom rules in another PR to keep this one relatively small.
* DEV: Introduces a plugin API for consolidating notifications.
This commit removes the `Notification#filter_by_consolidation_data` scope since plugins could have to define their criteria. The Plan class now receives two blocks, one to query for an already consolidated notification, which we'll try to update, and another to query for existing ones to consolidate.
It also receives a consolidation window, which accepts an ActiveSupport::Duration object, and filter notifications created since that value.
2021-12-01 00:36:14 +08:00
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
2022-06-22 02:49:47 +08:00
|
|
|
|
|
|
|
describe "#register_email_unsubscriber" do
|
|
|
|
let(:plugin) { Plugin::Instance.new }
|
|
|
|
|
|
|
|
after { DiscoursePluginRegistry.reset_register!(:email_unsubscribers) }
|
|
|
|
|
|
|
|
it "doesn't let you override core unsubscribers" do
|
|
|
|
expect {
|
|
|
|
plugin.register_email_unsubscriber(UnsubscribeKey::ALL_TYPE, Object)
|
|
|
|
}.to raise_error(ArgumentError)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "finds the plugin's custom unsubscriber" do
|
|
|
|
new_unsubscriber_type = "new_type"
|
|
|
|
key = UnsubscribeKey.new(unsubscribe_key_type: new_unsubscriber_type)
|
|
|
|
CustomUnsubscriber = Class.new(EmailControllerHelper::BaseEmailUnsubscriber)
|
|
|
|
|
|
|
|
plugin.register_email_unsubscriber(new_unsubscriber_type, CustomUnsubscriber)
|
|
|
|
|
|
|
|
expect(UnsubscribeKey.get_unsubscribe_strategy_for(key).class).to eq(CustomUnsubscriber)
|
|
|
|
end
|
|
|
|
end
|
FEATURE: Add plugin API to register About stat group (#17442)
This commit introduces a new plugin API to register
a group of stats that will be included in about.json
and also conditionally in the site about UI at /about.
The usage is like this:
```ruby
register_about_stat_group("chat_messages", show_in_ui: true) do
{
last_day: 1,
"7_days" => 10,
"30_days" => 100,
count: 1000,
previous_30_days: 120
}
end
```
In reality the stats will be generated any way the implementer
chooses within the plugin. The `last_day`, `7_days`, `30_days,` and `count`
keys must be present but apart from that additional stats may be added.
Only those core 4 stat keys will be shown in the UI, but everything will be shown
in about.json.
The stat group name is used to prefix the stats in about.json like so:
```json
"chat_messages_last_day": 2322,
"chat_messages_7_days": 2322,
"chat_messages_30_days": 2322,
"chat_messages_count": 2322,
```
The `show_in_ui` option (default false) is used to determine whether the
group of stats is shown on the site About page in the Site Statistics
table. Some stats may be needed purely for reporting purposes and thus
do not need to be shown in the UI to admins/users. An extension to the Site
serializer, `displayed_about_plugin_stat_groups`, has been added so this
can be inspected on the client-side.
2022-07-15 11:16:00 +08:00
|
|
|
|
2023-11-10 04:44:05 +08:00
|
|
|
describe "#register_stat" do
|
FEATURE: Add plugin API to register About stat group (#17442)
This commit introduces a new plugin API to register
a group of stats that will be included in about.json
and also conditionally in the site about UI at /about.
The usage is like this:
```ruby
register_about_stat_group("chat_messages", show_in_ui: true) do
{
last_day: 1,
"7_days" => 10,
"30_days" => 100,
count: 1000,
previous_30_days: 120
}
end
```
In reality the stats will be generated any way the implementer
chooses within the plugin. The `last_day`, `7_days`, `30_days,` and `count`
keys must be present but apart from that additional stats may be added.
Only those core 4 stat keys will be shown in the UI, but everything will be shown
in about.json.
The stat group name is used to prefix the stats in about.json like so:
```json
"chat_messages_last_day": 2322,
"chat_messages_7_days": 2322,
"chat_messages_30_days": 2322,
"chat_messages_count": 2322,
```
The `show_in_ui` option (default false) is used to determine whether the
group of stats is shown on the site About page in the Site Statistics
table. Some stats may be needed purely for reporting purposes and thus
do not need to be shown in the UI to admins/users. An extension to the Site
serializer, `displayed_about_plugin_stat_groups`, has been added so this
can be inspected on the client-side.
2022-07-15 11:16:00 +08:00
|
|
|
let(:plugin) { Plugin::Instance.new }
|
|
|
|
|
2023-03-02 06:10:16 +08:00
|
|
|
after { DiscoursePluginRegistry.reset! }
|
FEATURE: Add plugin API to register About stat group (#17442)
This commit introduces a new plugin API to register
a group of stats that will be included in about.json
and also conditionally in the site about UI at /about.
The usage is like this:
```ruby
register_about_stat_group("chat_messages", show_in_ui: true) do
{
last_day: 1,
"7_days" => 10,
"30_days" => 100,
count: 1000,
previous_30_days: 120
}
end
```
In reality the stats will be generated any way the implementer
chooses within the plugin. The `last_day`, `7_days`, `30_days,` and `count`
keys must be present but apart from that additional stats may be added.
Only those core 4 stat keys will be shown in the UI, but everything will be shown
in about.json.
The stat group name is used to prefix the stats in about.json like so:
```json
"chat_messages_last_day": 2322,
"chat_messages_7_days": 2322,
"chat_messages_30_days": 2322,
"chat_messages_count": 2322,
```
The `show_in_ui` option (default false) is used to determine whether the
group of stats is shown on the site About page in the Site Statistics
table. Some stats may be needed purely for reporting purposes and thus
do not need to be shown in the UI to admins/users. An extension to the Site
serializer, `displayed_about_plugin_stat_groups`, has been added so this
can be inspected on the client-side.
2022-07-15 11:16:00 +08:00
|
|
|
|
|
|
|
it "registers an about stat group correctly" do
|
|
|
|
stats = { :last_day => 1, "7_days" => 10, "30_days" => 100, :count => 1000 }
|
2024-10-29 23:40:11 +08:00
|
|
|
plugin.register_stat("some_group") { stats }
|
2023-11-10 04:44:05 +08:00
|
|
|
expect(Stat.all_stats.with_indifferent_access).to match(
|
FEATURE: Add plugin API to register About stat group (#17442)
This commit introduces a new plugin API to register
a group of stats that will be included in about.json
and also conditionally in the site about UI at /about.
The usage is like this:
```ruby
register_about_stat_group("chat_messages", show_in_ui: true) do
{
last_day: 1,
"7_days" => 10,
"30_days" => 100,
count: 1000,
previous_30_days: 120
}
end
```
In reality the stats will be generated any way the implementer
chooses within the plugin. The `last_day`, `7_days`, `30_days,` and `count`
keys must be present but apart from that additional stats may be added.
Only those core 4 stat keys will be shown in the UI, but everything will be shown
in about.json.
The stat group name is used to prefix the stats in about.json like so:
```json
"chat_messages_last_day": 2322,
"chat_messages_7_days": 2322,
"chat_messages_30_days": 2322,
"chat_messages_count": 2322,
```
The `show_in_ui` option (default false) is used to determine whether the
group of stats is shown on the site About page in the Site Statistics
table. Some stats may be needed purely for reporting purposes and thus
do not need to be shown in the UI to admins/users. An extension to the Site
serializer, `displayed_about_plugin_stat_groups`, has been added so this
can be inspected on the client-side.
2022-07-15 11:16:00 +08:00
|
|
|
hash_including(
|
|
|
|
some_group_last_day: 1,
|
|
|
|
some_group_7_days: 10,
|
|
|
|
some_group_30_days: 100,
|
|
|
|
some_group_count: 1000,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2023-03-02 06:10:16 +08:00
|
|
|
it "does not allow duplicate named stat groups" do
|
|
|
|
stats = { :last_day => 1, "7_days" => 10, "30_days" => 100, :count => 1000 }
|
2023-11-10 04:44:05 +08:00
|
|
|
plugin.register_stat("some_group") { stats }
|
|
|
|
plugin.register_stat("some_group") { stats }
|
|
|
|
expect(DiscoursePluginRegistry.stats.count).to eq(1)
|
2023-03-02 06:10:16 +08:00
|
|
|
end
|
FEATURE: Add plugin API to register About stat group (#17442)
This commit introduces a new plugin API to register
a group of stats that will be included in about.json
and also conditionally in the site about UI at /about.
The usage is like this:
```ruby
register_about_stat_group("chat_messages", show_in_ui: true) do
{
last_day: 1,
"7_days" => 10,
"30_days" => 100,
count: 1000,
previous_30_days: 120
}
end
```
In reality the stats will be generated any way the implementer
chooses within the plugin. The `last_day`, `7_days`, `30_days,` and `count`
keys must be present but apart from that additional stats may be added.
Only those core 4 stat keys will be shown in the UI, but everything will be shown
in about.json.
The stat group name is used to prefix the stats in about.json like so:
```json
"chat_messages_last_day": 2322,
"chat_messages_7_days": 2322,
"chat_messages_30_days": 2322,
"chat_messages_count": 2322,
```
The `show_in_ui` option (default false) is used to determine whether the
group of stats is shown on the site About page in the Site Statistics
table. Some stats may be needed purely for reporting purposes and thus
do not need to be shown in the UI to admins/users. An extension to the Site
serializer, `displayed_about_plugin_stat_groups`, has been added so this
can be inspected on the client-side.
2022-07-15 11:16:00 +08:00
|
|
|
end
|
2022-11-29 00:32:57 +08:00
|
|
|
|
|
|
|
describe "#register_user_destroyer_on_content_deletion_callback" do
|
|
|
|
let(:plugin) { Plugin::Instance.new }
|
|
|
|
|
|
|
|
after { DiscoursePluginRegistry.reset_register!(:user_destroyer_on_content_deletion_callbacks) }
|
|
|
|
|
2023-11-10 06:47:59 +08:00
|
|
|
fab!(:user)
|
2022-11-29 00:32:57 +08:00
|
|
|
|
|
|
|
it "calls the callback when the UserDestroyer runs with the delete_posts opt set to true" do
|
|
|
|
callback_called = false
|
|
|
|
|
|
|
|
cb = Proc.new { callback_called = true }
|
|
|
|
plugin.register_user_destroyer_on_content_deletion_callback(cb)
|
|
|
|
|
|
|
|
UserDestroyer.new(Discourse.system_user).destroy(user, { delete_posts: true })
|
|
|
|
|
|
|
|
expect(callback_called).to eq(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't run the callback when delete_posts opt is not true" do
|
|
|
|
callback_called = false
|
|
|
|
|
|
|
|
cb = Proc.new { callback_called = true }
|
|
|
|
plugin.register_user_destroyer_on_content_deletion_callback(cb)
|
|
|
|
|
|
|
|
UserDestroyer.new(Discourse.system_user).destroy(user, {})
|
|
|
|
|
|
|
|
expect(callback_called).to eq(false)
|
|
|
|
end
|
|
|
|
end
|
2023-03-30 11:39:55 +08:00
|
|
|
|
|
|
|
describe "#register_modifier" do
|
|
|
|
let(:plugin) { Plugin::Instance.new }
|
|
|
|
|
|
|
|
after { DiscoursePluginRegistry.clear_modifiers! }
|
|
|
|
|
|
|
|
it "allows modifier registration" do
|
|
|
|
plugin.register_modifier(:magic_sum_modifier) { |a, b| a + b }
|
|
|
|
|
|
|
|
sum = DiscoursePluginRegistry.apply_modifier(:magic_sum_modifier, 1, 2)
|
|
|
|
expect(sum).to eq(3)
|
|
|
|
end
|
|
|
|
end
|
2013-08-23 14:21:52 +08:00
|
|
|
end
|