DEV: Introduce a problem checks API (#25783)

Previously, problem checks were all added as either class methods or blocks in AdminDashboardData. Another set of class methods were used to add and run problem checks.

As of this PR, problem checks are promoted to first-class citizens. Each problem check receives their own class. This class of course contains the implementation for running the check, but also configuration items like retry strategies (for scheduled checks.)

In addition, the parent class ProblemCheck also serves as a registry for checks. For example we can get a list of all existing check classes through ProblemCheck.checks, or just the ones running on a schedule through ProblemCheck.scheduled.

After this refactor, the task of adding a new check is significantly simplified. You add a class that inherits ProblemCheck, you implement it, add a test, and you're good to go.
This commit is contained in:
Ted Johansson 2024-02-23 11:20:32 +08:00 committed by GitHub
parent 1ad8e85b37
commit a72dc2f420
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 370 additions and 304 deletions

View File

@ -13,17 +13,23 @@ module Jobs
retry_count = args[:retry_count].to_i retry_count = args[:retry_count].to_i
identifier = args[:check_identifier].to_sym identifier = args[:check_identifier].to_sym
check = AdminDashboardData.problem_scheduled_check_klasses[identifier] check = ::ProblemCheck[identifier]
AdminDashboardData.execute_scheduled_check(identifier) do |problems| problems = check.call
raise RetrySignal if retry_count < check.max_retries raise RetrySignal if problems.present? && retry_count < check.max_retries
end
problems.each { |problem| AdminDashboardData.add_found_scheduled_check_problem(problem) }
rescue RetrySignal rescue RetrySignal
Jobs.enqueue_in( Jobs.enqueue_in(
check.retry_wait, check.retry_after,
:problem_check, :problem_check,
args.merge(retry_count: retry_count + 1).stringify_keys, args.merge(retry_count: retry_count + 1).stringify_keys,
) )
rescue StandardError => err
Discourse.warn_exception(
err,
message: "A scheduled admin dashboard problem check (#{identifier}) errored.",
)
end end
end end
end end

View File

@ -2,8 +2,8 @@
module Jobs module Jobs
# This job runs all of the scheduled problem checks for the admin dashboard # This job runs all of the scheduled problem checks for the admin dashboard
# on a regular basis. To add a problem check for this scheduled job run # on a regular basis. To add a problem check, add a new class that inherits
# call AdminDashboardData.add_scheduled_problem_check # the `ProblemCheck` base class.
class ProblemChecks < ::Jobs::Scheduled class ProblemChecks < ::Jobs::Scheduled
sidekiq_options retry: false sidekiq_options retry: false
@ -13,7 +13,10 @@ module Jobs
# This way if the problems have been solved in the meantime, then they will # This way if the problems have been solved in the meantime, then they will
# not be re-added by the relevant checker, and will be cleared. # not be re-added by the relevant checker, and will be cleared.
AdminDashboardData.clear_found_scheduled_check_problems AdminDashboardData.clear_found_scheduled_check_problems
AdminDashboardData.execute_scheduled_checks
::ProblemCheck.scheduled.each do |check|
Jobs.enqueue(:problem_check, check_identifier: check.identifier.to_s)
end
end end
end end
end end

View File

@ -3,11 +3,7 @@
class AdminDashboardData class AdminDashboardData
include StatsCacheable include StatsCacheable
cattr_reader :problem_syms, cattr_reader :problem_syms, :problem_blocks, :problem_messages
:problem_blocks,
:problem_messages,
:problem_scheduled_check_blocks,
:problem_scheduled_check_klasses
class Problem class Problem
VALID_PRIORITIES = %w[low high].freeze VALID_PRIORITIES = %w[low high].freeze
@ -84,11 +80,6 @@ class AdminDashboardData
@@problem_blocks << blk if blk @@problem_blocks << blk if blk
end end
def self.add_scheduled_problem_check(check_identifier, klass = nil, &blk)
@@problem_scheduled_check_klasses[check_identifier] = klass
@@problem_scheduled_check_blocks[check_identifier] = blk
end
def self.add_found_scheduled_check_problem(problem) def self.add_found_scheduled_check_problem(problem)
problems = load_found_scheduled_check_problems problems = load_found_scheduled_check_problems
if problem.identifier.present? if problem.identifier.present?
@ -129,57 +120,6 @@ class AdminDashboardData
end end
end end
def self.register_default_scheduled_problem_checks
add_scheduled_problem_check(:group_smtp_credentials, GroupEmailCredentialsCheck) do
problems = GroupEmailCredentialsCheck.run
problems.map do |p|
problem_message =
I18n.t(
"dashboard.group_email_credentials_warning",
{
base_path: Discourse.base_path,
group_name: p[:group_name],
group_full_name: p[:group_full_name],
error: p[:message],
},
)
Problem.new(
problem_message,
priority: "high",
identifier: "group_#{p[:group_id]}_email_credentials",
)
end
end
end
def self.execute_scheduled_checks
problem_scheduled_check_blocks.keys.each do |check_identifier|
Jobs.enqueue(:problem_check, check_identifier: check_identifier.to_s)
end
end
def self.execute_scheduled_check(identifier)
check = problem_scheduled_check_blocks[identifier]
problems = instance_exec(&check)
yield(problems) if block_given? && problems.present?
Array
.wrap(problems)
.compact
.each do |problem|
next if !problem.is_a?(Problem)
add_found_scheduled_check_problem(problem)
end
rescue StandardError => err
Discourse.warn_exception(
err,
message: "A scheduled admin dashboard problem check (#{identifier}) errored.",
)
end
## ##
# We call this method in the class definition below # We call this method in the class definition below
# so all of the problem checks in this class are registered on # so all of the problem checks in this class are registered on
@ -192,8 +132,6 @@ class AdminDashboardData
def self.reset_problem_checks def self.reset_problem_checks
@@problem_syms = [] @@problem_syms = []
@@problem_blocks = [] @@problem_blocks = []
@@problem_scheduled_check_blocks = {}
@@problem_scheduled_check_klasses = {}
@@problem_messages = %w[ @@problem_messages = %w[
dashboard.bad_favicon_url dashboard.bad_favicon_url
@ -222,8 +160,6 @@ class AdminDashboardData
:translation_overrides_check, :translation_overrides_check,
:ember_version_check :ember_version_check
register_default_scheduled_problem_checks
add_problem_check { sidekiq_check || queue_size_check } add_problem_check { sidekiq_check || queue_size_check }
end end
reset_problem_checks reset_problem_checks

View File

@ -1,65 +0,0 @@
# frozen_string_literal: true
##
# If group SMTP or IMAP has been configured, we want to make sure the
# credentials are always valid otherwise emails will not be sending out
# from group inboxes. This check is run as part of scheduled AdminDashboardData
# problem checks, and if any credentials have issues they will show up on
# the admin dashboard as a high priority issue.
class GroupEmailCredentialsCheck
def self.max_retries = 2
def self.retry_wait = 30.seconds
def self.run
errors = []
if SiteSetting.enable_smtp
Group.with_smtp_configured.find_each do |group|
errors << try_validate(group) do
EmailSettingsValidator.validate_smtp(
host: group.smtp_server,
port: group.smtp_port,
username: group.email_username,
password: group.email_password,
)
end
end
end
if SiteSetting.enable_imap
Group.with_imap_configured.find_each do |group|
errors << try_validate(group) do
EmailSettingsValidator.validate_imap(
host: group.smtp_server,
port: group.smtp_port,
username: group.email_username,
password: group.email_password,
)
end
end
end
errors.compact
end
def self.try_validate(group, &blk)
begin
blk.call
nil
rescue *EmailSettingsExceptionHandler::EXPECTED_EXCEPTIONS => err
{
group_id: group.id,
group_name: group.name,
group_full_name: group.full_name,
message: EmailSettingsExceptionHandler.friendly_exception_message(err, group.smtp_server),
}
rescue => err
Discourse.warn_exception(
err,
message:
"Unexpected error when checking SMTP credentials for group #{group.id} (#{group.name}).",
)
nil
end
end
end

53
lib/problem_check.rb Normal file
View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
class ProblemCheck
include ActiveSupport::Configurable
# Determines if the check should be performed at a regular interval, and if
# so how often. If left blank, the check will be performed every time the
# admin dashboard is loaded, or the data is otherwise requested.
#
config_accessor :perform_every, default: nil, instance_writer: false
# How many times the check should retry before registering a problem. Only
# works for scheduled checks.
#
config_accessor :max_retries, default: 2, instance_writer: false
# The retry delay after a failed check. Only works for scheduled checks with
# more than one retry configured.
#
config_accessor :retry_after, default: 30.seconds, instance_writer: false
def self.[](key)
key = key.to_sym
checks.find { |c| c.identifier == key }
end
def self.checks
descendants
end
def self.scheduled
checks.select(&:scheduled?)
end
def self.identifier
name.demodulize.underscore.to_sym
end
delegate :identifier, to: :class
def self.scheduled?
perform_every.present?
end
delegate :scheduled?, to: :class
def self.call
new.call
end
def call
raise NotImplementedError
end
end

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
##
# If group SMTP or IMAP has been configured, we want to make sure the
# credentials are always valid otherwise emails will not be sending out
# from group inboxes. This check is run as part of scheduled admin
# problem checks, and if any credentials have issues they will show up on
# the admin dashboard as a high priority issue.
class ProblemCheck::GroupEmailCredentials < ProblemCheck
self.perform_every = 30.minutes
def call
[*smtp_errors, *imap_errors]
end
private
def smtp_errors
return [] if !SiteSetting.enable_smtp
Group.with_smtp_configured.find_each.filter_map do |group|
try_validate(group) do
EmailSettingsValidator.validate_smtp(
host: group.smtp_server,
port: group.smtp_port,
username: group.email_username,
password: group.email_password,
)
end
end
end
def imap_errors
return [] if !SiteSetting.enable_imap
Group.with_imap_configured.find_each.filter_map do |group|
try_validate(group) do
EmailSettingsValidator.validate_imap(
host: group.imap_server,
port: group.imap_port,
username: group.email_username,
password: group.email_password,
)
end
end
end
def try_validate(group, &blk)
begin
blk.call
nil
rescue *EmailSettingsExceptionHandler::EXPECTED_EXCEPTIONS => err
message =
I18n.t(
"dashboard.group_email_credentials_warning",
{
base_path: Discourse.base_path,
group_name: group.name,
group_full_name: group.full_name,
error: EmailSettingsExceptionHandler.friendly_exception_message(err, group.smtp_server),
},
)
Problem.new(message, priority: "high", identifier: "group_#{group.id}_email_credentials")
rescue => err
Discourse.warn_exception(
err,
message:
"Unexpected error when checking SMTP credentials for group #{group.id} (#{group.name}).",
)
nil
end
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class ProblemCheck::Problem
PRIORITIES = %w[low high].freeze
attr_reader :message, :priority, :identifier
def initialize(message, priority: "low", identifier: nil)
@message = message
@priority = PRIORITIES.include?(priority) ? priority : "low"
@identifier = identifier
end
def to_s
@message
end
def to_h
{ message: message, priority: priority, identifier: identifier }
end
alias_method :attributes, :to_h
def self.from_h(h)
h = h.with_indifferent_access
return if h[:message].blank?
new(h[:message], priority: h[:priority], identifier: h[:identifier])
end
end

View File

@ -3,98 +3,128 @@
RSpec.describe Jobs::ProblemCheck do RSpec.describe Jobs::ProblemCheck do
after do after do
Discourse.redis.flushdb Discourse.redis.flushdb
AdminDashboardData.reset_problem_checks
::ProblemCheck.send(:remove_const, "TestCheck")
end end
class TestCheck context "when there are problems" do
def self.max_retries = 0 before do
def self.retry_wait = 30.seconds ::ProblemCheck::TestCheck =
end Class.new(::ProblemCheck) do
self.max_retries = 0
it "runs the scheduled problem check that has been added and adds the messages to the load_found_scheduled_check_problems array" do def call
AdminDashboardData.add_scheduled_problem_check(:test_identifier, TestCheck) do
AdminDashboardData::Problem.new("big problem")
end
described_class.new.execute(check_identifier: :test_identifier)
problems = AdminDashboardData.load_found_scheduled_check_problems
expect(problems.count).to eq(1)
expect(problems.first).to be_a(AdminDashboardData::Problem)
expect(problems.first.to_s).to eq("big problem")
end
it "can handle the problem check returning multiple problems" do
AdminDashboardData.add_scheduled_problem_check(:test_identifier, TestCheck) do
[ [
AdminDashboardData::Problem.new("big problem"), ::ProblemCheck::Problem.new("Big problem"),
AdminDashboardData::Problem.new( ::ProblemCheck::Problem.new(
"yuge problem", "Yuge problem",
priority: "high", priority: "high",
identifier: "config_is_a_mess", identifier: "config_is_a_mess",
), ),
] ]
end end
end
described_class.new.execute(check_identifier: :test_identifier)
problems = AdminDashboardData.load_found_scheduled_check_problems
expect(problems.map(&:to_s)).to match_array(["big problem", "yuge problem"])
end end
it "does not add the same problem twice if the identifier already exists" do it "adds the messages to the Redis problems array" do
AdminDashboardData.add_scheduled_problem_check(:test_identifier, TestCheck) do described_class.new.execute(check_identifier: :test_check)
problems = AdminDashboardData.load_found_scheduled_check_problems
expect(problems.map(&:to_s)).to contain_exactly("Big problem", "Yuge problem")
end
end
context "with multiple problems with the same identifier" do
before do
::ProblemCheck::TestCheck =
Class.new(::ProblemCheck) do
self.max_retries = 0
def call
[ [
AdminDashboardData::Problem.new( ::ProblemCheck::Problem.new(
"yuge problem", "Yuge problem",
priority: "high", priority: "high",
identifier: "config_is_a_mess", identifier: "config_is_a_mess",
), ),
AdminDashboardData::Problem.new( ::ProblemCheck::Problem.new(
"nasty problem", "Nasty problem",
priority: "high", priority: "high",
identifier: "config_is_a_mess", identifier: "config_is_a_mess",
), ),
] ]
end end
end
end
it "does not add the same problem twice" do
described_class.new.execute(check_identifier: :test_check)
described_class.new.execute(check_identifier: :test_identifier)
problems = AdminDashboardData.load_found_scheduled_check_problems problems = AdminDashboardData.load_found_scheduled_check_problems
expect(problems.map(&:to_s)).to match_array(["yuge problem"])
expect(problems.map(&:to_s)).to match_array(["Yuge problem"])
end
end end
it "schedules a retry if there are attempts remaining" do context "when there are retries remaining" do
AdminDashboardData.add_scheduled_problem_check(:test_identifier, TestCheck) do before do
AdminDashboardData::Problem.new("big problem") ::ProblemCheck::TestCheck =
Class.new(::ProblemCheck) do
self.max_retries = 2
def call
[::ProblemCheck::Problem.new("Yuge problem")]
end
end
end end
TestCheck.stubs(:max_retries).returns(1) it "schedules a retry" do
expect_enqueued_with( expect_enqueued_with(
job: :problem_check, job: :problem_check,
args: { args: {
check_identifier: :test_identifier, check_identifier: :test_check,
retry_count: 1, retry_count: 1,
}, },
) { described_class.new.execute(check_identifier: :test_identifier) } ) { described_class.new.execute(check_identifier: :test_check) }
end
end end
it "does not schedule a retry if there are no more attempts remaining" do context "when there are no retries remaining" do
AdminDashboardData.add_scheduled_problem_check(:test_identifier, TestCheck) do before do
AdminDashboardData::Problem.new("big problem") ::ProblemCheck::TestCheck =
Class.new(::ProblemCheck) do
self.max_retries = 1
def call
[::ProblemCheck::Problem.new("Yuge problem")]
end
end
end end
TestCheck.stubs(:max_retries).returns(1) it "does not schedule a retry" do
expect_not_enqueued_with(job: :problem_check) do expect_not_enqueued_with(job: :problem_check) do
described_class.new.execute(check_identifier: :test_identifier, retry_count: 1) described_class.new.execute(check_identifier: :test_identifier, retry_count: 1)
end end
end end
it "handles errors from a troublesome check" do
AdminDashboardData.add_scheduled_problem_check(:test_identifier, TestCheck) do
raise StandardError.new("something went wrong")
end end
described_class.new.execute(check_identifier: :test_identifier) context "when the check unexpectedly errors out" do
expect(AdminDashboardData.load_found_scheduled_check_problems.count).to eq(0) before do
::ProblemCheck::TestCheck =
Class.new(::ProblemCheck) do
self.max_retries = 1
def call
raise StandardError.new("Something went wrong")
end
end
end
it "does not add a problem to the Redis array" do
described_class.new.execute(check_identifier: :test_check)
expect(AdminDashboardData.load_found_scheduled_check_problems).to be_empty
end
end end
end end

View File

@ -1,31 +1,37 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe Jobs::ProblemChecks do RSpec.describe Jobs::ProblemChecks do
before { Jobs.run_immediately! } before do
::ProblemCheck::ScheduledCheck =
Class.new(ProblemCheck) do
self.perform_every = 30.minutes
def call = []
end
::ProblemCheck::NonScheduledCheck = Class.new(ProblemCheck) { def call = [] }
end
after do after do
Discourse.redis.flushdb Discourse.redis.flushdb
AdminDashboardData.reset_problem_checks AdminDashboardData.reset_problem_checks
ProblemCheck.send(:remove_const, "ScheduledCheck")
ProblemCheck.send(:remove_const, "NonScheduledCheck")
end end
class TestCheck it "schedules the individual scheduled checks" do
def self.max_retries = 0 expect_enqueued_with(job: :problem_check, args: { check_identifier: "scheduled_check" }) do
def self.retry_wait = 0.seconds described_class.new.execute([])
end
it "starts with a blank slate every time the checks are run to avoid duplicate problems and to clear no longer firing problems" do
problem_should_fire = true
AdminDashboardData.reset_problem_checks
AdminDashboardData.add_scheduled_problem_check(:test_identifier, TestCheck) do
if problem_should_fire
problem_should_fire = false
AdminDashboardData::Problem.new("yuge problem", priority: "high")
end end
end end
described_class.new.execute(nil) it "does not schedule non-scheduled checks" do
expect(AdminDashboardData.load_found_scheduled_check_problems.count).to eq(1) expect_not_enqueued_with(
described_class.new.execute(nil) job: :problem_check,
expect(AdminDashboardData.load_found_scheduled_check_problems.count).to eq(0) args: {
check_identifier: "non_scheduled_check",
},
) { described_class.new.execute([]) }
end end
end end

View File

@ -3,16 +3,16 @@
require "net/smtp" require "net/smtp"
require "net/imap" require "net/imap"
RSpec.describe GroupEmailCredentialsCheck do RSpec.describe ProblemCheck::GroupEmailCredentials do
fab!(:group1) { Fabricate(:group) } fab!(:group1) { Fabricate(:group) }
fab!(:group2) { Fabricate(:smtp_group) } fab!(:group2) { Fabricate(:smtp_group) }
fab!(:group3) { Fabricate(:imap_group) } fab!(:group3) { Fabricate(:imap_group) }
describe "#run" do describe "#call" do
it "does nothing if SMTP is disabled for the site" do it "does nothing if SMTP is disabled for the site" do
expect_no_validate_any expect_no_validate_any
SiteSetting.enable_smtp = false SiteSetting.enable_smtp = false
expect(described_class.run).to eq([]) expect(described_class.new.call).to eq([])
end end
context "with smtp and imap enabled for the site" do context "with smtp and imap enabled for the site" do
@ -25,10 +25,10 @@ RSpec.describe GroupEmailCredentialsCheck do
expect_no_validate_any expect_no_validate_any
group2.update!(smtp_enabled: false) group2.update!(smtp_enabled: false)
group3.update!(smtp_enabled: false, imap_enabled: false) group3.update!(smtp_enabled: false, imap_enabled: false)
expect(described_class.run).to eq([]) expect(described_class.new.call).to eq([])
end end
it "returns an error message and the group ID if the group's SMTP settings error" do it "returns a problem with the group's SMTP settings error" do
EmailSettingsValidator EmailSettingsValidator
.expects(:validate_smtp) .expects(:validate_smtp)
.raises(Net::SMTPAuthenticationError.new("bad credentials")) .raises(Net::SMTPAuthenticationError.new("bad credentials"))
@ -37,15 +37,13 @@ RSpec.describe GroupEmailCredentialsCheck do
.at_least_once .at_least_once
EmailSettingsValidator.stubs(:validate_imap).returns(true) EmailSettingsValidator.stubs(:validate_imap).returns(true)
expect(described_class.run).to eq( expect(described_class.new.call).to contain_exactly(
[ have_attributes(
{ identifier: "group_#{group2.id}_email_credentials",
group_full_name: group2.full_name, priority: "high",
group_name: group2.name, message:
group_id: group2.id, "There was an issue with the email credentials for the group <a href=\"/g/#{group2.name}/manage/email\"></a>. No emails will be sent from the group inbox until this problem is addressed. There was an issue with the SMTP credentials provided, check the username and password and try again.",
message: I18n.t("email_settings.smtp_authentication_error"), ),
},
],
) )
end end
@ -56,15 +54,13 @@ RSpec.describe GroupEmailCredentialsCheck do
.raises(Net::IMAP::NoResponseError.new(stub(data: stub(text: "Invalid credentials")))) .raises(Net::IMAP::NoResponseError.new(stub(data: stub(text: "Invalid credentials"))))
.once .once
expect(described_class.run).to eq( expect(described_class.new.call).to contain_exactly(
[ have_attributes(
{ identifier: "group_#{group3.id}_email_credentials",
group_full_name: group3.full_name, priority: "high",
group_name: group3.name, message:
group_id: group3.id, "There was an issue with the email credentials for the group <a href=\"/g/#{group3.name}/manage/email\"></a>. No emails will be sent from the group inbox until this problem is addressed. There was an issue with the IMAP credentials provided, check the username and password and try again.",
message: I18n.t("email_settings.imap_authentication_error"), ),
},
],
) )
end end
@ -73,7 +69,7 @@ RSpec.describe GroupEmailCredentialsCheck do
EmailSettingsValidator.stubs(:validate_smtp).returns(true) EmailSettingsValidator.stubs(:validate_smtp).returns(true)
EmailSettingsValidator.expects(:validate_imap).never EmailSettingsValidator.expects(:validate_imap).never
expect(described_class.run).to eq([]) expect(described_class.new.call).to eq([])
end end
end end
end end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck do
# rubocop:disable RSpec/BeforeAfterAll
before(:all) do
ScheduledCheck = Class.new(described_class) { self.perform_every = 30.minutes }
UnscheduledCheck = Class.new(described_class)
end
after(:all) do
Object.send(:remove_const, ScheduledCheck.name)
Object.send(:remove_const, UnscheduledCheck.name)
end
# rubocop:enable RSpec/BeforeAfterAll
let(:scheduled_check) { ScheduledCheck }
let(:unscheduled_check) { UnscheduledCheck }
describe ".[]" do
it { expect(described_class[:scheduled_check]).to eq(scheduled_check) }
it { expect(described_class[:foo]).to eq(nil) }
end
describe ".identifier" do
it { expect(scheduled_check.identifier).to eq(:scheduled_check) }
end
describe ".checks" do
it { expect(described_class.checks).to contain_exactly(scheduled_check, unscheduled_check) }
end
describe ".scheduled" do
it { expect(described_class.scheduled).to contain_exactly(scheduled_check) }
end
describe ".scheduled?" do
it { expect(scheduled_check).to be_scheduled }
it { expect(unscheduled_check).to_not be_scheduled }
end
end

View File

@ -82,49 +82,6 @@ RSpec.describe AdminDashboardData do
include_examples "stats cacheable" include_examples "stats cacheable"
end 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 describe "#problem_message_check" do
let(:key) { AdminDashboardData.problem_messages.first } let(:key) { AdminDashboardData.problem_messages.first }