discourse/spec/models/admin_dashboard_data_spec.rb
Ted Johansson 47e58357b6
DEV: Parallel scheduled admin checks (#24190)
This PR does some preparatory refactoring of scheduled admin checks in order for us to be able to do custom retry strategies for some of them.

Instead of running all checks in sequence inside a single, scheduled job, the scheduled job spawns one new job per check.

In order to be concurrency-safe, we need to change the existing Redis data structure from a string (of serialized JSON) to a list of strings (of serialized JSON).
2023-11-03 09:05:29 +08:00

417 lines
14 KiB
Ruby

# frozen_string_literal: true
RSpec.describe AdminDashboardData do
after do
AdminDashboardData.reset_problem_checks
Discourse.redis.flushdb
end
describe "#fetch_problems" do
describe "adding problem messages" do
it "adds the message and returns it when the problems are fetched" do
AdminDashboardData.add_problem_message("dashboard.bad_favicon_url")
problems = AdminDashboardData.fetch_problems.map(&:to_s)
expect(problems).to include(
I18n.t("dashboard.bad_favicon_url", { base_path: Discourse.base_path }),
)
end
it "does not allow adding of arbitrary problem messages, they must exist in AdminDashboardData.problem_messages" do
AdminDashboardData.add_problem_message("errors.messages.invalid")
problems = AdminDashboardData.fetch_problems.map(&:to_s)
expect(problems).not_to include(I18n.t("errors.messages.invalid"))
end
end
describe "adding new checks" do
it "calls the passed block" do
AdminDashboardData.add_problem_check { "a problem was found" }
problems = AdminDashboardData.fetch_problems
expect(problems.map(&:to_s)).to include("a problem was found")
end
it "calls the passed method" do
klass =
Class.new(AdminDashboardData) do
def my_test_method
"a problem was found"
end
end
klass.add_problem_check :my_test_method
problems = klass.fetch_problems
expect(problems.map(&:to_s)).to include("a problem was found")
end
end
end
describe "adding scheduled checks" do
it "does not add duplicate problems with the same identifier" do
prob1 = AdminDashboardData::Problem.new("test problem", identifier: "test")
prob2 = AdminDashboardData::Problem.new("test problem 2", identifier: "test")
AdminDashboardData.add_found_scheduled_check_problem(prob1)
AdminDashboardData.add_found_scheduled_check_problem(prob2)
expect(AdminDashboardData.load_found_scheduled_check_problems.map(&:to_s)).to eq(
["test problem"],
)
end
it "does not error when loading malformed problems saved in redis" do
Discourse.redis.rpush(AdminDashboardData::SCHEDULED_PROBLEM_STORAGE_KEY, "{ 'badjson")
expect(AdminDashboardData.load_found_scheduled_check_problems).to eq([])
end
it "clears a specific problem by identifier" do
prob1 = AdminDashboardData::Problem.new("test problem 1", identifier: "test")
AdminDashboardData.add_found_scheduled_check_problem(prob1)
AdminDashboardData.clear_found_problem("test")
expect(AdminDashboardData.load_found_scheduled_check_problems).to eq([])
end
it "defaults to low priority, and uses low priority if an invalid priority is passed" do
prob1 = AdminDashboardData::Problem.new("test problem 1")
prob2 = AdminDashboardData::Problem.new("test problem 2", priority: "superbad")
expect(prob1.priority).to eq("low")
expect(prob2.priority).to eq("low")
end
end
describe "stats cache" do
include_examples "stats cacheable"
end
describe ".execute_scheduled_checks" do
let(:blk) { -> {} }
before { AdminDashboardData.add_scheduled_problem_check(:foo, &blk) }
after { AdminDashboardData.reset_problem_checks }
it do
expect_enqueued_with(job: :problem_check, args: { check_identifier: :foo }) do
AdminDashboardData.execute_scheduled_checks
end
end
end
describe ".execute_scheduled_check" do
context "when problems are found" do
let(:blk) { -> { self::Problem.new("Problem") } }
before do
AdminDashboardData.add_scheduled_problem_check(:foo, &blk)
AdminDashboardData.expects(:add_found_scheduled_check_problem).once
end
after { AdminDashboardData.reset_problem_checks }
it do
expect(described_class.execute_scheduled_check(:foo)).to all(be_a(described_class::Problem))
end
end
context "when check errors out" do
let(:blk) { -> { raise StandardError } }
before do
AdminDashboardData.add_scheduled_problem_check(:foo, &blk)
Discourse.expects(:warn_exception).once
end
after { AdminDashboardData.reset_problem_checks }
it { expect(described_class.execute_scheduled_check(:foo)).to eq(nil) }
end
end
describe "#problem_message_check" do
let(:key) { AdminDashboardData.problem_messages.first }
after { described_class.clear_problem_message(key) }
it "returns nil if message has not been added" do
expect(described_class.problem_message_check(key)).to be_nil
end
it "returns a message if it was added" do
described_class.add_problem_message(key)
expect(described_class.problem_message_check(key)).to eq(
I18n.t(key, base_path: Discourse.base_path),
)
end
it "returns a message if it was added with an expiry" do
described_class.add_problem_message(key, 300)
expect(described_class.problem_message_check(key)).to eq(
I18n.t(key, base_path: Discourse.base_path),
)
end
end
describe "rails_env_check" do
subject(:check) { described_class.new.rails_env_check }
it "returns nil when running in production mode" do
Rails.stubs(env: ActiveSupport::StringInquirer.new("production"))
expect(check).to be_nil
end
it "returns a string when running in development mode" do
Rails.stubs(env: ActiveSupport::StringInquirer.new("development"))
expect(check).to_not be_nil
end
it "returns a string when running in test mode" do
Rails.stubs(env: ActiveSupport::StringInquirer.new("test"))
expect(check).to_not be_nil
end
end
describe "host_names_check" do
subject(:check) { described_class.new.host_names_check }
it "returns nil when host_names is set" do
Discourse.stubs(:current_hostname).returns("something.com")
expect(check).to be_nil
end
it "returns a string when host_name is localhost" do
Discourse.stubs(:current_hostname).returns("localhost")
expect(check).to_not be_nil
end
it "returns a string when host_name is production.localhost" do
Discourse.stubs(:current_hostname).returns("production.localhost")
expect(check).to_not be_nil
end
end
describe "sidekiq_check" do
subject(:check) { described_class.new.sidekiq_check }
it "returns nil when sidekiq processed a job recently" do
Jobs.stubs(:last_job_performed_at).returns(1.minute.ago)
Jobs.stubs(:queued).returns(0)
expect(check).to be_nil
end
it "returns nil when last job processed was a long time ago, but no jobs are queued" do
Jobs.stubs(:last_job_performed_at).returns(7.days.ago)
Jobs.stubs(:queued).returns(0)
expect(check).to be_nil
end
it "returns nil when no jobs have ever been processed, but no jobs are queued" do
Jobs.stubs(:last_job_performed_at).returns(nil)
Jobs.stubs(:queued).returns(0)
expect(check).to be_nil
end
it "returns a string when no jobs were processed recently and some jobs are queued" do
Jobs.stubs(:last_job_performed_at).returns(20.minutes.ago)
Jobs.stubs(:queued).returns(1)
expect(check).to_not be_nil
end
it "returns a string when no jobs have ever been processed, and some jobs are queued" do
Jobs.stubs(:last_job_performed_at).returns(nil)
Jobs.stubs(:queued).returns(1)
expect(check).to_not be_nil
end
end
describe "ram_check" do
subject(:check) { described_class.new.ram_check }
it "returns nil when total ram is 1 GB" do
MemInfo.any_instance.stubs(:mem_total).returns(1_025_272)
expect(check).to be_nil
end
it "returns nil when total ram cannot be determined" do
MemInfo.any_instance.stubs(:mem_total).returns(nil)
expect(check).to be_nil
end
it "returns a string when total ram is less than 1 GB" do
MemInfo.any_instance.stubs(:mem_total).returns(512_636)
expect(check).to_not be_nil
end
end
describe "auth_config_checks" do
shared_examples "problem detection for login providers" do
context "when disabled" do
it "returns nil" do
SiteSetting.set(enable_setting, false)
expect(check).to be_nil
end
end
context "when enabled" do
before { SiteSetting.set(enable_setting, true) }
it "returns nil when key and secret are set" do
SiteSetting.set(key, "12313213")
SiteSetting.set(secret, "12312313123")
expect(check).to be_nil
end
it "returns a string when key is not set" do
SiteSetting.set(key, "")
SiteSetting.set(secret, "12312313123")
expect(check).to_not be_nil
end
it "returns a string when secret is not set" do
SiteSetting.set(key, "123123")
SiteSetting.set(secret, "")
expect(check).to_not be_nil
end
it "returns a string when key and secret are not set" do
SiteSetting.set(key, "")
SiteSetting.set(secret, "")
expect(check).to_not be_nil
end
end
end
describe "facebook" do
subject(:check) { described_class.new.facebook_config_check }
let(:enable_setting) { :enable_facebook_logins }
let(:key) { :facebook_app_id }
let(:secret) { :facebook_app_secret }
include_examples "problem detection for login providers"
end
describe "twitter" do
subject(:check) { described_class.new.twitter_config_check }
let(:enable_setting) { :enable_twitter_logins }
let(:key) { :twitter_consumer_key }
let(:secret) { :twitter_consumer_secret }
include_examples "problem detection for login providers"
end
describe "github" do
subject(:check) { described_class.new.github_config_check }
let(:enable_setting) { :enable_github_logins }
let(:key) { :github_client_id }
let(:secret) { :github_client_secret }
include_examples "problem detection for login providers"
end
end
describe "force_https_check" do
subject(:check) { described_class.new(check_force_https: true).force_https_check }
it "returns nil if force_https site setting enabled" do
SiteSetting.force_https = true
expect(check).to be_nil
end
it "returns nil if force_https site setting not enabled" do
SiteSetting.force_https = false
expect(check).to eq(I18n.t("dashboard.force_https_warning", base_path: Discourse.base_path))
end
end
describe "ignore force_https_check" do
subject(:check) { described_class.new(check_force_https: false).force_https_check }
it "returns nil" do
SiteSetting.force_https = true
expect(check).to be_nil
SiteSetting.force_https = false
expect(check).to be_nil
end
end
describe "#out_of_date_themes" do
let(:remote) { RemoteTheme.create!(remote_url: "https://github.com/org/testtheme") }
let!(:theme) { Fabricate(:theme, remote_theme: remote, name: "Test< Theme") }
it "outputs correctly formatted html" do
remote.update!(local_version: "old version", remote_version: "new version", commits_behind: 2)
dashboard_data = described_class.new
expect(dashboard_data.out_of_date_themes).to eq(
I18n.t("dashboard.out_of_date_themes") +
"<ul><li><a href=\"/admin/customize/themes/#{theme.id}\">Test&lt; Theme</a></li></ul>",
)
remote.update!(local_version: "new version", commits_behind: 0)
expect(dashboard_data.out_of_date_themes).to eq(nil)
end
end
describe "#unreachable_themes" do
let(:remote) do
RemoteTheme.create!(
remote_url: "https://github.com/org/testtheme",
last_error_text: "can't reach repo :'(",
)
end
let!(:theme) { Fabricate(:theme, remote_theme: remote, name: "Test< Theme") }
it "outputs correctly formatted html" do
dashboard_data = described_class.new
expect(dashboard_data.unreachable_themes).to eq(
I18n.t("dashboard.unreachable_themes") +
"<ul><li><a href=\"/admin/customize/themes/#{theme.id}\">Test&lt; Theme</a></li></ul>",
)
remote.update!(last_error_text: nil)
expect(dashboard_data.out_of_date_themes).to eq(nil)
end
end
describe "#translation_overrides_check" do
subject(:dashboard_data) { described_class.new }
context "when there are outdated translations" do
before { Fabricate(:translation_override, translation_key: "foo.bar", status: "outdated") }
it "outputs the correct message" do
expect(dashboard_data.translation_overrides_check).to eq(
I18n.t("dashboard.outdated_translations_warning", base_path: Discourse.base_path),
)
end
end
context "when there are no outdated translations" do
before { Fabricate(:translation_override, status: "up_to_date") }
it "outputs nothing" do
expect(dashboard_data.translation_overrides_check).to eq(nil)
end
end
end
describe "#deprecated_category_style_check" do
subject(:dashboard_data) { described_class.new }
context "with a non-default category style" do
before { SiteSetting.set(:category_style, "box") }
it "outputs the correct message" do
expect(dashboard_data.deprecated_category_style_check).to eq(
I18n.t("dashboard.category_style_deprecated"),
)
end
end
context "with the default category style" do
before { SiteSetting.set(:category_style, "bullet") }
it "outputs nothing" do
expect(dashboard_data.deprecated_category_style_check).to eq(nil)
end
end
end
end