DEV: Move non scheduled problem checks to classes (#26122)

In AdminDashboardData we have a bunch of problem checks implemented as methods on that class. This PR absolves it of the responsibility by promoting each of those checks to a first class ProblemCheck. This way each of them can have their own priority and arbitrary functionality can be isolated in its own class.

Think "extract class" refactoring over and over. Since they were all moved we can also get rid of the @@problem_syms class variable which was basically the old version of the registry now replaced by ProblemCheck.realtime.

In addition AdminDashboardData::Problem value object has been entirely replaced with the new ProblemCheck::Problem (with compatible API).

Lastly, I added some RSpec matchers to simplify testing of problem checks and provide helpful error messages when assertions fail.
This commit is contained in:
Ted Johansson 2024-03-14 10:55:01 +08:00 committed by GitHub
parent 9afb0b29f8
commit ea5c3a3bdc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 1447 additions and 477 deletions

View File

@ -3,33 +3,7 @@
class AdminDashboardData class AdminDashboardData
include StatsCacheable include StatsCacheable
cattr_reader :problem_syms, :problem_blocks, :problem_messages cattr_reader :problem_blocks, :problem_messages
class Problem
VALID_PRIORITIES = %w[low high].freeze
attr_reader :message, :priority, :identifier
def initialize(message, priority: "low", identifier: nil)
@message = message
@priority = VALID_PRIORITIES.include?(priority) ? priority : "low"
@identifier = identifier
end
def to_s
@message
end
def to_h
{ message: message, priority: priority, identifier: identifier }
end
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
# kept for backward compatibility # kept for backward compatibility
GLOBAL_REPORTS ||= [] GLOBAL_REPORTS ||= []
@ -51,18 +25,16 @@ class AdminDashboardData
def problems def problems
problems = [] problems = []
self.class.problem_syms.each do |sym|
message = public_send(sym)
problems << Problem.new(message) if message.present?
end
self.class.problem_blocks.each do |blk| self.class.problem_blocks.each do |blk|
message = instance_exec(&blk) message = instance_exec(&blk)
problems << Problem.new(message) if message.present? problems << ProblemCheck::Problem.new(message) if message.present?
end end
self.class.problem_messages.each do |i18n_key| self.class.problem_messages.each do |i18n_key|
message = self.class.problem_message_check(i18n_key) message = self.class.problem_message_check(i18n_key)
problems << Problem.new(message) if message.present? problems << ProblemCheck::Problem.new(message) if message.present?
end end
problems.concat(ProblemCheck.realtime.flat_map { |c| c.call(@opts).map(&:to_h) })
problems += self.class.load_found_scheduled_check_problems problems += self.class.load_found_scheduled_check_problems
problems.compact! problems.compact!
@ -76,7 +48,6 @@ class AdminDashboardData
end end
def self.add_problem_check(*syms, &blk) def self.add_problem_check(*syms, &blk)
@@problem_syms.push(*syms) if syms
@@problem_blocks << blk if blk @@problem_blocks << blk if blk
end end
@ -109,7 +80,7 @@ class AdminDashboardData
found_problems.filter_map do |problem| found_problems.filter_map do |problem|
begin begin
Problem.from_h(JSON.parse(problem)) ProblemCheck::Problem.from_h(JSON.parse(problem))
rescue JSON::ParserError => err rescue JSON::ParserError => err
Discourse.warn_exception( Discourse.warn_exception(
err, err,
@ -130,7 +101,6 @@ class AdminDashboardData
# tests. It will also fire multiple times in development mode because # tests. It will also fire multiple times in development mode because
# classes are not cached. # classes are not cached.
def self.reset_problem_checks def self.reset_problem_checks
@@problem_syms = []
@@problem_blocks = [] @@problem_blocks = []
@@problem_messages = %w[ @@problem_messages = %w[
@ -139,26 +109,6 @@ class AdminDashboardData
dashboard.poll_pop3_auth_error dashboard.poll_pop3_auth_error
] ]
add_problem_check :rails_env_check,
:host_names_check,
:force_https_check,
:ram_check,
:google_oauth2_config_check,
:facebook_config_check,
:twitter_config_check,
:github_config_check,
:s3_config_check,
:s3_cdn_check,
:image_magick_check,
:failing_emails_check,
:subfolder_ends_in_slash_check,
:email_polling_errored_recently,
:out_of_date_themes,
:unreachable_themes,
:watched_words_check,
:google_analytics_version_check,
:translation_overrides_check
add_problem_check { sidekiq_check || queue_size_check } add_problem_check { sidekiq_check || queue_size_check }
end end
reset_problem_checks reset_problem_checks
@ -226,16 +176,6 @@ class AdminDashboardData
"#{PROBLEM_MESSAGE_PREFIX}#{i18n_key}" "#{PROBLEM_MESSAGE_PREFIX}#{i18n_key}"
end end
def rails_env_check
I18n.t("dashboard.rails_env_warning", env: Rails.env) unless Rails.env.production?
end
def host_names_check
if %w[localhost production.localhost].include?(Discourse.current_hostname)
I18n.t("dashboard.host_names_warning")
end
end
def sidekiq_check def sidekiq_check
last_job_performed_at = Jobs.last_job_performed_at last_job_performed_at = Jobs.last_job_performed_at
if Jobs.queued > 0 && (last_job_performed_at.nil? || last_job_performed_at < 2.minutes.ago) if Jobs.queued > 0 && (last_job_performed_at.nil? || last_job_performed_at < 2.minutes.ago)
@ -247,164 +187,4 @@ class AdminDashboardData
queue_size = Jobs.queued queue_size = Jobs.queued
I18n.t("dashboard.queue_size_warning", queue_size: queue_size) if queue_size >= 100_000 I18n.t("dashboard.queue_size_warning", queue_size: queue_size) if queue_size >= 100_000
end end
def ram_check
I18n.t("dashboard.memory_warning") if MemInfo.new.mem_total && MemInfo.new.mem_total < 950_000
end
def google_oauth2_config_check
if SiteSetting.enable_google_oauth2_logins &&
(
SiteSetting.google_oauth2_client_id.blank? ||
SiteSetting.google_oauth2_client_secret.blank?
)
I18n.t("dashboard.google_oauth2_config_warning", base_path: Discourse.base_path)
end
end
def facebook_config_check
if SiteSetting.enable_facebook_logins &&
(SiteSetting.facebook_app_id.blank? || SiteSetting.facebook_app_secret.blank?)
I18n.t("dashboard.facebook_config_warning", base_path: Discourse.base_path)
end
end
def twitter_config_check
if SiteSetting.enable_twitter_logins &&
(SiteSetting.twitter_consumer_key.blank? || SiteSetting.twitter_consumer_secret.blank?)
I18n.t("dashboard.twitter_config_warning", base_path: Discourse.base_path)
end
end
def github_config_check
if SiteSetting.enable_github_logins &&
(SiteSetting.github_client_id.blank? || SiteSetting.github_client_secret.blank?)
I18n.t("dashboard.github_config_warning", base_path: Discourse.base_path)
end
end
def s3_config_check
# if set via global setting it is validated during the `use_s3?` call
if !GlobalSetting.use_s3?
bad_keys =
(SiteSetting.s3_access_key_id.blank? || SiteSetting.s3_secret_access_key.blank?) &&
!SiteSetting.s3_use_iam_profile
if SiteSetting.enable_s3_uploads && (bad_keys || SiteSetting.s3_upload_bucket.blank?)
return I18n.t("dashboard.s3_config_warning", base_path: Discourse.base_path)
end
if SiteSetting.backup_location == BackupLocationSiteSetting::S3 &&
(bad_keys || SiteSetting.s3_backup_bucket.blank?)
return I18n.t("dashboard.s3_backup_config_warning", base_path: Discourse.base_path)
end
end
nil
end
def s3_cdn_check
if (GlobalSetting.use_s3? || SiteSetting.enable_s3_uploads) &&
SiteSetting.Upload.s3_cdn_url.blank?
I18n.t("dashboard.s3_cdn_warning")
end
end
def translation_overrides_check
if TranslationOverride.exists?(status: %i[outdated invalid_interpolation_keys])
I18n.t("dashboard.outdated_translations_warning", base_path: Discourse.base_path)
end
end
def image_magick_check
if SiteSetting.create_thumbnails && !system("command -v convert >/dev/null;")
I18n.t("dashboard.image_magick_warning")
end
end
def failing_emails_check
num_failed_jobs = Jobs.num_email_retry_jobs
if num_failed_jobs > 0
I18n.t(
"dashboard.failing_emails_warning",
num_failed_jobs: num_failed_jobs,
base_path: Discourse.base_path,
)
end
end
def subfolder_ends_in_slash_check
I18n.t("dashboard.subfolder_ends_in_slash") if Discourse.base_path =~ %r{/\z}
end
def google_analytics_version_check
I18n.t("dashboard.v3_analytics_deprecated") if SiteSetting.ga_version == "v3_analytics"
end
def email_polling_errored_recently
errors = Jobs::PollMailbox.errors_in_past_24_hours
if errors > 0
I18n.t(
"dashboard.email_polling_errored_recently",
count: errors,
base_path: Discourse.base_path,
)
end
end
def missing_mailgun_api_key
return unless SiteSetting.reply_by_email_enabled
return unless ActionMailer::Base.smtp_settings[:address]["smtp.mailgun.org"]
return unless SiteSetting.mailgun_api_key.blank?
I18n.t("dashboard.missing_mailgun_api_key")
end
def force_https_check
return unless @opts[:check_force_https]
unless SiteSetting.force_https
I18n.t("dashboard.force_https_warning", base_path: Discourse.base_path)
end
end
def watched_words_check
WatchedWord.actions.keys.each do |action|
begin
WordWatcher.compiled_regexps_for_action(action, raise_errors: true)
rescue RegexpError => e
translated_action = I18n.t("admin_js.admin.watched_words.actions.#{action}")
I18n.t(
"dashboard.watched_word_regexp_error",
base_path: Discourse.base_path,
action: translated_action,
)
end
end
nil
end
def out_of_date_themes
old_themes = RemoteTheme.out_of_date_themes
return unless old_themes.present?
themes_html_format(old_themes, "dashboard.out_of_date_themes")
end
def unreachable_themes
themes = RemoteTheme.unreachable_themes
return unless themes.present?
themes_html_format(themes, "dashboard.unreachable_themes")
end
private
def themes_html_format(themes, i18n_key)
html =
themes
.map do |name, id|
"<li><a href=\"/admin/customize/themes/#{id}\">#{CGI.escapeHTML(name)}</a></li>"
end
.join("\n")
"#{I18n.t(i18n_key)}<ul>#{html}</ul>"
end
end end

View File

@ -35,6 +35,10 @@ class ProblemCheck
checks.select(&:scheduled?) checks.select(&:scheduled?)
end end
def self.realtime
checks.reject(&:scheduled?)
end
def self.identifier def self.identifier
name.demodulize.underscore.to_sym name.demodulize.underscore.to_sym
end end
@ -45,9 +49,20 @@ class ProblemCheck
end end
delegate :scheduled?, to: :class delegate :scheduled?, to: :class
def self.call def self.realtime?
new.call !scheduled?
end end
delegate :realtime?, to: :class
def self.call(data = {})
new(data).call
end
def initialize(data = {})
@data = OpenStruct.new(data)
end
attr_reader :data
def call def call
raise NotImplementedError raise NotImplementedError
@ -55,10 +70,15 @@ class ProblemCheck
private private
def problem def problem(override_key = nil)
[ [
Problem.new( Problem.new(
I18n.t(translation_key, base_path: Discourse.base_path), message ||
I18n.t(
override_key || translation_key,
base_path: Discourse.base_path,
**translation_data.symbolize_keys,
),
priority: self.config.priority, priority: self.config.priority,
identifier:, identifier:,
), ),
@ -69,8 +89,16 @@ class ProblemCheck
[] []
end end
def message
nil
end
def translation_key def translation_key
# TODO: Infer a default based on class name, then move translations in locale file. # TODO: Infer a default based on class name, then move translations in locale file.
raise NotImplementedError raise NotImplementedError
end end
def translation_data
{}
end
end end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class ProblemCheck::EmailPollingErroredRecently < ProblemCheck
self.priority = "low"
def call
return no_problem if polling_error_count.to_i == 0
problem
end
private
def polling_error_count
@polling_error_count ||= Jobs::PollMailbox.errors_in_past_24_hours
end
def translation_key
"dashboard.email_polling_errored_recently"
end
def translation_data
{ count: polling_error_count }
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class ProblemCheck::FacebookConfig < ProblemCheck
self.priority = "low"
def call
return no_problem if !SiteSetting.enable_facebook_logins
return no_problem if facebook_credentials_present?
problem
end
private
def translation_key
"dashboard.facebook_config_warning"
end
def facebook_credentials_present?
SiteSetting.facebook_app_id.present? && SiteSetting.facebook_app_secret.present?
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class ProblemCheck::FailingEmails < ProblemCheck
self.priority = "low"
def call
return no_problem if failed_job_count.to_i == 0
problem
end
private
def failed_job_count
@failed_job_count ||= Jobs.num_email_retry_jobs
end
def translation_key
"dashboard.failing_emails_warning"
end
def translation_data
{ num_failed_jobs: failed_job_count }
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class ProblemCheck::ForceHttps < ProblemCheck
self.priority = "low"
def call
return no_problem if SiteSetting.force_https
return no_problem if !data.check_force_https
problem
end
private
def translation_key
"dashboard.force_https_warning"
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class ProblemCheck::GithubConfig < ProblemCheck
self.priority = "low"
def call
return no_problem if !SiteSetting.enable_github_logins
return no_problem if github_credentials_present?
problem
end
private
def translation_key
"dashboard.github_config_warning"
end
def github_credentials_present?
SiteSetting.github_client_id.present? && SiteSetting.github_client_secret.present?
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ProblemCheck::GoogleAnalyticsVersion < ProblemCheck
self.priority = "low"
def call
return no_problem if SiteSetting.ga_version != "v3_analytics"
problem
end
private
def translation_key
"dashboard.v3_analytics_deprecated"
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class ProblemCheck::GoogleOauth2Config < ProblemCheck
self.priority = "low"
def call
return no_problem if !SiteSetting.enable_google_oauth2_logins
return no_problem if google_oauth2_credentials_present?
problem
end
private
def translation_key
"dashboard.google_oauth2_config_warning"
end
def google_oauth2_credentials_present?
SiteSetting.google_oauth2_client_id.present? && SiteSetting.google_oauth2_client_secret.present?
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ProblemCheck::HostNames < ProblemCheck
self.priority = "low"
def call
return no_problem if %w[localhost production.localhost].exclude?(Discourse.current_hostname)
problem
end
private
def translation_key
"dashboard.host_names_warning"
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class ProblemCheck::ImageMagick < ProblemCheck
self.priority = "low"
def call
return no_problem if !SiteSetting.create_thumbnails
return no_problem if Kernel.system("command -v convert >/dev/null;")
problem
end
private
def translation_key
"dashboard.image_magick_warning"
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class ProblemCheck::MissingMailgunApiKey < ProblemCheck
self.priority = "low"
def call
return no_problem if !SiteSetting.reply_by_email_enabled
return no_problem if !ActionMailer::Base.smtp_settings.dig(:address, "smtp.mailgun.org")
return no_problem if SiteSetting.mailgun_api_key.present?
problem
end
private
def translation_key
"dashboard.missing_mailgun_api_key"
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class ProblemCheck::OutOfDateThemes < ProblemCheck
self.priority = "low"
def call
return no_problem if out_of_date_themes.empty?
problem
end
private
def out_of_date_themes
@out_of_date_themes ||= RemoteTheme.out_of_date_themes
end
def message
"#{I18n.t("dashboard.out_of_date_themes")}<ul>#{themes_list}</ul>"
end
def themes_list
out_of_date_themes
.map do |name, id|
"<li><a href=\"#{Discourse.base_path}/admin/customize/themes/#{id}\">#{CGI.escapeHTML(name)}</a></li>"
end
.join("\n")
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class ProblemCheck::RailsEnv < ProblemCheck
self.priority = "low"
def call
return no_problem if Rails.env.production?
problem
end
private
def translation_key
"dashboard.rails_env_warning"
end
def translation_data
{ env: Rails.env }
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class ProblemCheck::Ram < ProblemCheck
self.priority = "low"
def call
available_memory = MemInfo.new
return no_problem if available_memory.unknown?
return no_problem if available_memory.mem_total > 950_000
problem
end
private
def translation_key
"dashboard.memory_warning"
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class ProblemCheck::S3BackupConfig < ProblemCheck
self.priority = "low"
def call
return no_problem if GlobalSetting.use_s3?
return no_problem if SiteSetting.backup_location != BackupLocationSiteSetting::S3
return no_problem if !missing_keys? && SiteSetting.s3_backup_bucket.present?
problem
end
private
def missing_keys?
return false if SiteSetting.s3_use_iam_profile
SiteSetting.s3_access_key_id.blank? || SiteSetting.s3_secret_access_key.blank?
end
def translation_key
"dashboard.s3_backup_config_warning"
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class ProblemCheck::S3Cdn < ProblemCheck
self.priority = "low"
def call
return no_problem if !GlobalSetting.use_s3? && !SiteSetting.enable_s3_uploads?
return no_problem if SiteSetting.Upload.s3_cdn_url.present?
problem
end
private
def translation_key
"dashboard.s3_cdn_warning"
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class ProblemCheck::S3UploadConfig < ProblemCheck
self.priority = "low"
def call
return no_problem if GlobalSetting.use_s3?
return no_problem if !SiteSetting.enable_s3_uploads?
return no_problem if !missing_keys? && SiteSetting.s3_upload_bucket.present?
problem
end
private
def missing_keys?
return false if SiteSetting.s3_use_iam_profile
SiteSetting.s3_access_key_id.blank? || SiteSetting.s3_secret_access_key.blank?
end
def translation_key
"dashboard.s3_config_warning"
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ProblemCheck::SubfolderEndsInSlash < ProblemCheck
self.priority = "low"
def call
return no_problem if !Discourse.base_path.end_with?("/")
problem
end
private
def translation_key
"dashboard.subfolder_ends_in_slash"
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class ProblemCheck::TranslationOverrides < ProblemCheck
self.priority = "low"
def call
if !TranslationOverride.exists?(status: %i[outdated invalid_interpolation_keys])
return no_problem
end
problem
end
private
def translation_key
"dashboard.outdated_translations_warning"
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class ProblemCheck::TwitterConfig < ProblemCheck
self.priority = "low"
def call
return no_problem if !SiteSetting.enable_twitter_logins
return no_problem if twitter_credentials_present?
problem
end
private
def translation_key
"dashboard.twitter_config_warning"
end
def twitter_credentials_present?
SiteSetting.twitter_consumer_key.present? && SiteSetting.twitter_consumer_secret.present?
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class ProblemCheck::UnreachableThemes < ProblemCheck
self.priority = "low"
def call
return no_problem if unreachable_themes.empty?
problem
end
private
def unreachable_themes
@unreachable_themes ||= RemoteTheme.unreachable_themes
end
def message
"#{I18n.t("dashboard.unreachable_themes")}<ul>#{themes_list}</ul>"
end
def themes_list
unreachable_themes
.map do |name, id|
"<li><a href=\"/admin/customize/themes/#{id}\">#{CGI.escapeHTML(name)}</a></li>"
end
.join("\n")
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
class ProblemCheck::WatchedWords < ProblemCheck
self.priority = "low"
def call
return no_problem if invalid_regexp_actions.empty?
problem
end
private
def translation_key
"dashboard.watched_word_regexp_error"
end
def translation_data
{ action: invalid_regexp_actions.map { |w| "'#{w}'" }.join(", ") }
end
def invalid_regexp_actions
@invalid_regexp_actions ||=
WatchedWord.actions.keys.filter_map do |action|
WordWatcher.compiled_regexps_for_action(action, raise_errors: true)
nil
rescue RegexpError
I18n.t("admin_js.admin.watched_words.actions.#{action}")
end
end
end

View File

@ -1557,7 +1557,7 @@ en:
force_https_warning: "Your website is using SSL. But `<a href='%{base_path}/admin/site_settings/category/all_results?filter=force_https'>force_https</a>` is not yet enabled in your site settings." force_https_warning: "Your website is using SSL. But `<a href='%{base_path}/admin/site_settings/category/all_results?filter=force_https'>force_https</a>` is not yet enabled in your site settings."
out_of_date_themes: "Updates are available for the following themes:" out_of_date_themes: "Updates are available for the following themes:"
unreachable_themes: "We were unable to check for updates on the following themes:" unreachable_themes: "We were unable to check for updates on the following themes:"
watched_word_regexp_error: "The regular expression for '%{action}' watched words is invalid. Please check your <a href='%{base_path}/admin/customize/watched_words'>Watched Word settings</a>, or disable the 'watched words regular expressions' site setting." watched_word_regexp_error: "The regular expression for %{action} watched words is invalid. Please check your <a href='%{base_path}/admin/customize/watched_words'>Watched Word settings</a>, or disable the 'watched words regular expressions' site setting."
v3_analytics_deprecated: "Your Discourse is currently using Google Analytics 3, which will no longer be supported after July 2023. <a href='https://meta.discourse.org/t/260498'>Upgrade to Google Analytics 4</a> now to continue receiving valuable insights and analytics for your website's performance." v3_analytics_deprecated: "Your Discourse is currently using Google Analytics 3, which will no longer be supported after July 2023. <a href='https://meta.discourse.org/t/260498'>Upgrade to Google Analytics 4</a> now to continue receiving valuable insights and analytics for your website's performance."
category_style_deprecated: "Your Discourse is currently using a deprecated category style which will be removed before the final beta release of Discourse 3.2. Please refer to <a href='https://meta.discourse.org/t/282441'>Moving to a Single Category Style Site Setting</a> for instructions on how to keep your selected category style." category_style_deprecated: "Your Discourse is currently using a deprecated category style which will be removed before the final beta release of Discourse 3.2. Please refer to <a href='https://meta.discourse.org/t/282441'>Moving to a Single Category Style Site Setting</a> for instructions on how to keep your selected category style."
back_from_logster_text: "Back to site" back_from_logster_text: "Back to site"

View File

@ -18,4 +18,8 @@ class MemInfo
nil nil
end end
end end
def unknown?
mem_total.nil?
end
end end

View File

@ -30,27 +30,13 @@ RSpec.describe AdminDashboardData do
problems = AdminDashboardData.fetch_problems problems = AdminDashboardData.fetch_problems
expect(problems.map(&:to_s)).to include("a problem was found") expect(problems.map(&:to_s)).to include("a problem was found")
end 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
end end
describe "adding scheduled checks" do describe "adding scheduled checks" do
it "does not add duplicate problems with the same identifier" do it "does not add duplicate problems with the same identifier" do
prob1 = AdminDashboardData::Problem.new("test problem", identifier: "test") prob1 = ProblemCheck::Problem.new("test problem", identifier: "test")
prob2 = AdminDashboardData::Problem.new("test problem 2", identifier: "test") prob2 = ProblemCheck::Problem.new("test problem 2", identifier: "test")
AdminDashboardData.add_found_scheduled_check_problem(prob1) AdminDashboardData.add_found_scheduled_check_problem(prob1)
AdminDashboardData.add_found_scheduled_check_problem(prob2) AdminDashboardData.add_found_scheduled_check_problem(prob2)
expect(AdminDashboardData.load_found_scheduled_check_problems.map(&:to_s)).to eq( expect(AdminDashboardData.load_found_scheduled_check_problems.map(&:to_s)).to eq(
@ -64,15 +50,15 @@ RSpec.describe AdminDashboardData do
end end
it "clears a specific problem by identifier" do it "clears a specific problem by identifier" do
prob1 = AdminDashboardData::Problem.new("test problem 1", identifier: "test") prob1 = ProblemCheck::Problem.new("test problem 1", identifier: "test")
AdminDashboardData.add_found_scheduled_check_problem(prob1) AdminDashboardData.add_found_scheduled_check_problem(prob1)
AdminDashboardData.clear_found_problem("test") AdminDashboardData.clear_found_problem("test")
expect(AdminDashboardData.load_found_scheduled_check_problems).to eq([]) expect(AdminDashboardData.load_found_scheduled_check_problems).to eq([])
end end
it "defaults to low priority, and uses low priority if an invalid priority is passed" do it "defaults to low priority, and uses low priority if an invalid priority is passed" do
prob1 = AdminDashboardData::Problem.new("test problem 1") prob1 = ProblemCheck::Problem.new("test problem 1")
prob2 = AdminDashboardData::Problem.new("test problem 2", priority: "superbad") prob2 = ProblemCheck::Problem.new("test problem 2", priority: "superbad")
expect(prob1.priority).to eq("low") expect(prob1.priority).to eq("low")
expect(prob2.priority).to eq("low") expect(prob2.priority).to eq("low")
end end
@ -106,44 +92,6 @@ RSpec.describe AdminDashboardData do
end end
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 describe "sidekiq_check" do
subject(:check) { described_class.new.sidekiq_check } subject(:check) { described_class.new.sidekiq_check }
@ -177,175 +125,4 @@ RSpec.describe AdminDashboardData do
expect(check).to_not be_nil expect(check).to_not be_nil
end end
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
end end

View File

@ -4,17 +4,17 @@ RSpec.describe ProblemCheck do
# rubocop:disable RSpec/BeforeAfterAll # rubocop:disable RSpec/BeforeAfterAll
before(:all) do before(:all) do
ScheduledCheck = Class.new(described_class) { self.perform_every = 30.minutes } ScheduledCheck = Class.new(described_class) { self.perform_every = 30.minutes }
UnscheduledCheck = Class.new(described_class) RealtimeCheck = Class.new(described_class)
end end
after(:all) do after(:all) do
Object.send(:remove_const, ScheduledCheck.name) Object.send(:remove_const, ScheduledCheck.name)
Object.send(:remove_const, UnscheduledCheck.name) Object.send(:remove_const, RealtimeCheck.name)
end end
# rubocop:enable RSpec/BeforeAfterAll # rubocop:enable RSpec/BeforeAfterAll
let(:scheduled_check) { ScheduledCheck } let(:scheduled_check) { ScheduledCheck }
let(:unscheduled_check) { UnscheduledCheck } let(:realtime_check) { RealtimeCheck }
describe ".[]" do describe ".[]" do
it { expect(described_class[:scheduled_check]).to eq(scheduled_check) } it { expect(described_class[:scheduled_check]).to eq(scheduled_check) }
@ -26,16 +26,26 @@ RSpec.describe ProblemCheck do
end end
describe ".checks" do describe ".checks" do
it { expect(described_class.checks).to include(scheduled_check, unscheduled_check) } it { expect(described_class.checks).to include(scheduled_check, realtime_check) }
end end
describe ".scheduled" do describe ".scheduled" do
it { expect(described_class.scheduled).to include(scheduled_check) } it { expect(described_class.scheduled).to include(scheduled_check) }
it { expect(described_class.scheduled).not_to include(unscheduled_check) } it { expect(described_class.scheduled).not_to include(realtime_check) }
end
describe ".realtime" do
it { expect(described_class.realtime).to include(realtime_check) }
it { expect(described_class.realtime).not_to include(scheduled_check) }
end end
describe ".scheduled?" do describe ".scheduled?" do
it { expect(scheduled_check).to be_scheduled } it { expect(scheduled_check).to be_scheduled }
it { expect(unscheduled_check).to_not be_scheduled } it { expect(realtime_check).to_not be_scheduled }
end
describe ".realtime?" do
it { expect(realtime_check).to be_realtime }
it { expect(scheduled_check).to_not be_realtime }
end end
end end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::EmailPollingErroredRecently do
subject(:check) { described_class.new }
describe ".call" do
before { Jobs::PollMailbox.stubs(errors_in_past_24_hours: error_count) }
context "when number of failing jobs is 0" do
let(:error_count) { 0 }
it { expect(check).to be_chill_about_it }
end
context "when jobs are failing" do
let(:error_count) { 1 }
it do
expect(check).to(
have_a_problem.with_priority("low").with_message(
"Email polling has generated an error in the past 24 hours. Look at <a href='/logs' target='_blank'>the logs</a> for more details.",
),
)
end
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::FacebookConfig do
subject(:check) { described_class.new }
describe ".call" do
before { SiteSetting.stubs(enable_facebook_logins: enabled) }
context "when Facebook authentication is disabled" do
let(:enabled) { false }
it { expect(check).to be_chill_about_it }
end
context "when Facebook authentication is enabled and configured" do
let(:enabled) { true }
before do
SiteSetting.stubs(facebook_app_id: "foo")
SiteSetting.stubs(facebook_app_secret: "bar")
end
it { expect(check).to be_chill_about_it }
end
context "when Facebook authentication is enabled but missing client ID" do
let(:enabled) { true }
before do
SiteSetting.stubs(facebook_app_id: nil)
SiteSetting.stubs(facebook_app_secret: "bar")
end
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to allow signup and login with Facebook (enable_facebook_logins), but the app id and app secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/configuring-facebook-login-for-discourse/13394" target="_blank">See this guide to learn more</a>.',
)
end
end
context "when Facebook authentication is enabled but missing client secret" do
let(:enabled) { true }
before do
SiteSetting.stubs(facebook_app_id: "foo")
SiteSetting.stubs(facebook_app_secret: nil)
end
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to allow signup and login with Facebook (enable_facebook_logins), but the app id and app secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/configuring-facebook-login-for-discourse/13394" target="_blank">See this guide to learn more</a>.',
)
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::FailingEmails do
subject(:check) { described_class.new }
describe ".call" do
before { Jobs.stubs(num_email_retry_jobs: failing_jobs) }
context "when number of failing jobs is 0" do
let(:failing_jobs) { 0 }
it { expect(check).to be_chill_about_it }
end
context "when jobs are failing" do
let(:failing_jobs) { 1 }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'There are 1 email jobs that failed. Check your app.yml and ensure that the mail server settings are correct. <a href="/sidekiq/retries" target="_blank">See the failed jobs in Sidekiq</a>.',
)
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::ForceHttps do
subject(:check) { described_class.new(data) }
describe ".call" do
before { SiteSetting.stubs(force_https: configured) }
context "when configured to force SSL" do
let(:configured) { true }
let(:data) { { check_force_https: true } }
it { expect(check).to be_chill_about_it }
end
context "when not configured to force SSL" do
let(:configured) { false }
context "when the request is coming over HTTPS" do
let(:data) { { check_force_https: true } }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"Your website is using SSL. But `<a href='/admin/site_settings/category/all_results?filter=force_https'>force_https</a>` is not yet enabled in your site settings.",
)
end
end
context "when the request is coming over HTTP" do
let(:data) { { check_force_https: false } }
it { expect(check).to be_chill_about_it }
end
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::GithubConfig do
subject(:check) { described_class.new }
describe ".call" do
before { SiteSetting.stubs(enable_github_logins: enabled) }
context "when GitHub authentication is disabled" do
let(:enabled) { false }
it { expect(check).to be_chill_about_it }
end
context "when GitHub authentication is enabled and configured" do
let(:enabled) { true }
before do
SiteSetting.stubs(github_client_id: "foo")
SiteSetting.stubs(github_client_secret: "bar")
end
it { expect(check).to be_chill_about_it }
end
context "when GitHub authentication is enabled but missing client ID" do
let(:enabled) { true }
before do
SiteSetting.stubs(github_client_id: nil)
SiteSetting.stubs(github_client_secret: "bar")
end
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to allow signup and login with GitHub (enable_github_logins), but the client id and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/configuring-github-login-for-discourse/13745" target="_blank">See this guide to learn more</a>.',
)
end
end
context "when GitHub authentication is enabled but missing client secret" do
let(:enabled) { true }
before do
SiteSetting.stubs(github_client_id: "foo")
SiteSetting.stubs(github_client_secret: nil)
end
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to allow signup and login with GitHub (enable_github_logins), but the client id and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/configuring-github-login-for-discourse/13745" target="_blank">See this guide to learn more</a>.',
)
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::GoogleAnalyticsVersion do
subject(:check) { described_class.new }
describe ".call" do
before { SiteSetting.stubs(ga_version: version) }
context "when using Google Analytics V3" do
let(:version) { "v3_analytics" }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"Your Discourse is currently using Google Analytics 3, which will no longer be supported after July 2023. <a href='https://meta.discourse.org/t/260498'>Upgrade to Google Analytics 4</a> now to continue receiving valuable insights and analytics for your website's performance.",
)
end
end
context "when using Google Analytics V4" do
let(:version) { "v4_analytics" }
it { expect(check).to be_chill_about_it }
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::GoogleOauth2Config do
subject(:check) { described_class.new }
describe ".call" do
before { SiteSetting.stubs(enable_google_oauth2_logins: enabled) }
context "when Google OAuth is disabled" do
let(:enabled) { false }
it { expect(check).to be_chill_about_it }
end
context "when Google OAuth is enabled and configured" do
let(:enabled) { true }
before do
SiteSetting.stubs(google_oauth2_client_id: "foo")
SiteSetting.stubs(google_oauth2_client_secret: "bar")
end
it { expect(check).to be_chill_about_it }
end
context "when Google OAuth is enabled but missing client ID" do
let(:enabled) { true }
before do
SiteSetting.stubs(google_oauth2_client_id: nil)
SiteSetting.stubs(google_oauth2_client_secret: "bar")
end
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to allow signup and login with Google OAuth2 (enable_google_oauth2_logins), but the client id and client secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/configuring-google-login-for-discourse/15858" target="_blank">See this guide to learn more</a>.',
)
end
end
context "when Google OAuth is enabled but missing client secret" do
let(:enabled) { true }
before do
SiteSetting.stubs(google_oauth2_client_id: "foo")
SiteSetting.stubs(google_oauth2_client_secret: nil)
end
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to allow signup and login with Google OAuth2 (enable_google_oauth2_logins), but the client id and client secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/configuring-google-login-for-discourse/15858" target="_blank">See this guide to learn more</a>.',
)
end
end
end
end

View File

@ -4,6 +4,8 @@ require "net/smtp"
require "net/imap" require "net/imap"
RSpec.describe ProblemCheck::GroupEmailCredentials do RSpec.describe ProblemCheck::GroupEmailCredentials do
subject(:check) { described_class.new }
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) }
@ -12,7 +14,7 @@ RSpec.describe ProblemCheck::GroupEmailCredentials 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.new.call).to eq([]) expect(check).to be_chill_about_it
end end
context "with smtp and imap enabled for the site" do context "with smtp and imap enabled for the site" do
@ -25,7 +27,7 @@ RSpec.describe ProblemCheck::GroupEmailCredentials 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.new.call).to eq([]) expect(check).to be_chill_about_it
end end
it "returns a problem with the group's SMTP settings error" do it "returns a problem with the group's SMTP settings error" do

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::HostNames do
subject(:check) { described_class.new }
describe ".call" do
before { Discourse.stubs(current_hostname: hostname) }
context "when a production host name is configured" do
let(:hostname) { "something.com" }
it { expect(check).to be_chill_about_it }
end
context "when host name is set to localhost" do
let(:hostname) { "localhost" }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"Your config/database.yml file is using the default localhost hostname. Update it to use your site's hostname.",
)
end
end
context "when host name is set to production.localhost" do
let(:hostname) { "production.localhost" }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"Your config/database.yml file is using the default localhost hostname. Update it to use your site's hostname.",
)
end
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::ImageMagick do
subject(:check) { described_class.new }
describe ".call" do
before do
SiteSetting.stubs(create_thumbnails: enabled)
Kernel.stubs(system: installed)
end
context "when thumbnail creation is enabled" do
let(:enabled) { true }
context "when Image Magick is installed" do
let(:installed) { true }
it { expect(check).to be_chill_about_it }
end
context "when Image Magick is not installed" do
let(:installed) { false }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to create thumbnails of large images, but ImageMagick is not installed. Install ImageMagick using your favorite package manager or <a href="https://www.imagemagick.org/script/download.php" target="_blank">download the latest release</a>.',
)
end
end
end
context "when thumbnail creation is disabled" do
let(:enabled) { false }
let(:installed) { false }
it { expect(check).to be_chill_about_it }
end
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::MissingMailgunApiKey do
subject(:check) { described_class.new }
describe ".call" do
before do
SiteSetting.stubs(reply_by_email_enabled: replies_enabled)
ActionMailer::Base.smtp_settings.stubs(dig: mailgun_address)
SiteSetting.stubs(mailgun_api_key: api_key)
end
context "when replies are disabled" do
let(:replies_enabled) { false }
let(:mailgun_address) { anything }
let(:api_key) { anything }
it { expect(check).to be_chill_about_it }
end
context "when not using Mailgun for replies" do
let(:replies_enabled) { false }
let(:mailgun_address) { nil }
let(:api_key) { anything }
it { expect(check).to be_chill_about_it }
end
context "when using Mailgun without an API key" do
let(:replies_enabled) { true }
let(:mailgun_address) { "foo" }
let(:api_key) { nil }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"The server is configured to send emails via Mailgun but you haven't provided an API key used to verify the webhook messages.",
)
end
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::OutOfDateThemes do
subject(:check) { described_class.new }
describe ".call" do
let(:remote) do
RemoteTheme.create!(
remote_url: "https://github.com/org/testtheme",
commits_behind: commits_behind,
)
end
before { Fabricate(:theme, id: 44, remote_theme: remote, name: "Test< Theme") }
context "when theme is out of date" do
let(:commits_behind) { 2 }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'Updates are available for the following themes:<ul><li><a href="/admin/customize/themes/44">Test&lt; Theme</a></li></ul>',
)
end
end
context "when theme is up to date" do
let(:commits_behind) { 0 }
it { expect(check).to be_chill_about_it }
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::RailsEnv do
subject(:check) { described_class.new }
describe ".call" do
before { Rails.stubs(env: ActiveSupport::StringInquirer.new(environment)) }
context "when running in production environment" do
let(:environment) { "production" }
it { expect(check).to be_chill_about_it }
end
context "when running in development environment" do
let(:environment) { "development" }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"Your server is running in development mode.",
)
end
end
context "when running in test environment" do
let(:environment) { "test" }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"Your server is running in test mode.",
)
end
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::Ram do
subject(:check) { described_class.new }
before { MemInfo.any_instance.stubs(mem_total: total_ram) }
context "when total ram is 1 GB" do
let(:total_ram) { 1_025_272 }
it { expect(check).to be_chill_about_it }
end
context "when total ram cannot be determined" do
let(:total_ram) { nil }
it { expect(check).to be_chill_about_it }
end
context "when total ram is less than 1 GB" do
let(:total_ram) { 512_636 }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"Your server is running with less than 1 GB of total memory. At least 1 GB of memory is recommended.",
)
end
end
end

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::S3BackupConfig do
subject(:check) { described_class.new }
describe ".call" do
let(:backup_location) { BackupLocationSiteSetting::S3 }
let(:bucket_name) { "backups" }
before do
GlobalSetting.stubs(use_s3?: globally_enabled)
SiteSetting.stubs(backup_location: backup_location)
SiteSetting.stubs(s3_backup_bucket: bucket_name)
end
context "when S3 uploads are globally enabled" do
let(:globally_enabled) { true }
it "relies on the check in GlobalSettings#use_s3?" do
expect(check).to be_chill_about_it
end
end
context "when S3 backups are disabled" do
let(:globally_enabled) { false }
let(:backup_location) { nil }
it { expect(check).to be_chill_about_it }
end
context "when S3 backups are enabled" do
let(:globally_enabled) { false }
before { SiteSetting.stubs(s3_use_iam_profile: use_iam_profile) }
context "when configured to use IAM profile" do
let(:use_iam_profile) { true }
it { expect(check).to be_chill_about_it }
end
context "when not configured to use IAM profile" do
let(:use_iam_profile) { false }
before do
SiteSetting.stubs(s3_access_key_id: access_key)
SiteSetting.stubs(s3_secret_access_key: secret_access_key)
end
context "when credentials are present" do
let(:access_key) { "foo" }
let(:secret_access_key) { "bar" }
it { expect(check).to be_chill_about_it }
end
context "when credentials are missing" do
let(:access_key) { "foo" }
let(:secret_access_key) { nil }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to upload backups to S3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_backup_bucket. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/how-to-set-up-image-uploads-to-s3/7229" target="_blank">See "How to set up image uploads to S3?" to learn more</a>.',
)
end
end
context "when bucket name is missing" do
let(:access_key) { "foo" }
let(:secret_access_key) { "bar" }
let(:bucket_name) { nil }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to upload backups to S3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_backup_bucket. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/how-to-set-up-image-uploads-to-s3/7229" target="_blank">See "How to set up image uploads to S3?" to learn more</a>.',
)
end
end
end
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::S3Cdn do
subject(:check) { described_class.new }
describe ".call" do
before do
GlobalSetting.stubs(use_s3?: globally_enabled)
SiteSetting.stubs(enable_s3_uploads?: locally_enabled)
SiteSetting::Upload.stubs(s3_cdn_url: cdn_url)
end
context "when S3 uploads are enabled" do
let(:globally_enabled) { false }
let(:locally_enabled) { true }
context "when CDN URL is configured" do
let(:cdn_url) { "https://cdn.codinghorror.com" }
it { expect(check).to be_chill_about_it }
end
context "when CDN URL is not configured" do
let(:cdn_url) { nil }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to upload files to S3, but there is no S3 CDN configured. This can lead to expensive S3 costs and slower site performance. <a href="https://meta.discourse.org/t/-/148916" target="_blank">See "Using Object Storage for Uploads" to learn more</a>.',
)
end
end
end
context "when S3 uploads are disabled" do
let(:globally_enabled) { false }
let(:locally_enabled) { false }
let(:cdn_url) { nil }
it { expect(check).to be_chill_about_it }
end
end
end

View File

@ -0,0 +1,83 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::S3UploadConfig do
subject(:check) { described_class.new }
describe ".call" do
before do
GlobalSetting.stubs(use_s3?: globally_enabled)
SiteSetting.stubs(enable_s3_uploads?: locally_enabled)
end
context "when S3 uploads are globally enabled" do
let(:globally_enabled) { true }
let(:locally_enabled) { false }
it "relies on the check in GlobalSettings#use_s3?" do
expect(check).to be_chill_about_it
end
end
context "when S3 uploads are disabled" do
let(:globally_enabled) { false }
let(:locally_enabled) { false }
it { expect(check).to be_chill_about_it }
end
context "when S3 uploads are locally enabled" do
let(:globally_enabled) { false }
let(:locally_enabled) { true }
before { SiteSetting.stubs(s3_use_iam_profile: use_iam_profile) }
context "when configured to use IAM profile" do
let(:use_iam_profile) { true }
it { expect(check).to be_chill_about_it }
end
context "when not configured to use IAM profile" do
let(:use_iam_profile) { false }
before do
SiteSetting.stubs(s3_access_key_id: access_key)
SiteSetting.stubs(s3_secret_access_key: secret_access_key)
SiteSetting.stubs(s3_upload_bucket: bucket_name)
end
context "when credentials are present" do
let(:access_key) { "foo" }
let(:secret_access_key) { "bar" }
let(:bucket_name) { "baz" }
it { expect(check).to be_chill_about_it }
end
context "when credentials are missing" do
let(:access_key) { "foo" }
let(:secret_access_key) { nil }
let(:bucket_name) { "baz" }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to upload files to S3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_upload_bucket. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/how-to-set-up-image-uploads-to-s3/7229" target="_blank">See "How to set up image uploads to S3?" to learn more</a>.',
)
end
end
context "when bucket name is missing" do
let(:access_key) { "foo" }
let(:secret_access_key) { "bar" }
let(:bucket_name) { nil }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to upload files to S3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_upload_bucket. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/how-to-set-up-image-uploads-to-s3/7229" target="_blank">See "How to set up image uploads to S3?" to learn more</a>.',
)
end
end
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::SubfolderEndsInSlash do
subject(:check) { described_class.new }
describe ".call" do
before { Discourse.stubs(base_path: path) }
context "when path doesn't end in a slash" do
let(:path) { "cats" }
it { expect(check).to be_chill_about_it }
end
context "when path ends in a slash" do
let(:path) { "cats/" }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"Your subfolder setup is incorrect; the DISCOURSE_RELATIVE_URL_ROOT ends in a slash.",
)
end
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::TranslationOverrides do
subject(:check) { described_class.new }
describe ".call" do
before { Fabricate(:translation_override, status: status) }
context "when there are outdated translation overrides" do
let(:status) { "outdated" }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"Some of your translation overrides are out of date. Please check your <a href='/admin/customize/site_texts?outdated=true'>text customizations</a>.",
)
end
end
context "when there are translation overrides with invalid interpolation keys" do
let(:status) { "invalid_interpolation_keys" }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"Some of your translation overrides are out of date. Please check your <a href='/admin/customize/site_texts?outdated=true'>text customizations</a>.",
)
end
end
context "when all translation overrides are fine" do
let(:status) { "up_to_date" }
it { expect(check).to be_chill_about_it }
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::TwitterConfig do
subject(:check) { described_class.new }
describe ".call" do
before { SiteSetting.stubs(enable_twitter_logins: enabled) }
context "when Twitter authentication is disabled" do
let(:enabled) { false }
it { expect(check).to be_chill_about_it }
end
context "when Twitter authentication is enabled and configured" do
let(:enabled) { true }
before do
SiteSetting.stubs(twitter_consumer_key: "foo")
SiteSetting.stubs(twitter_consumer_secret: "bar")
end
it { expect(check).to be_chill_about_it }
end
context "when Twitter authentication is enabled but missing client ID" do
let(:enabled) { true }
before do
SiteSetting.stubs(twitter_consumer_key: nil)
SiteSetting.stubs(twitter_consumer_secret: "bar")
end
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to allow signup and login with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/configuring-twitter-login-for-discourse/13395" target="_blank">See this guide to learn more</a>.',
)
end
end
context "when Twitter authentication is enabled but missing client secret" do
let(:enabled) { true }
before do
SiteSetting.stubs(twitter_consumer_key: "foo")
SiteSetting.stubs(twitter_consumer_secret: nil)
end
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'The server is configured to allow signup and login with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://meta.discourse.org/t/configuring-twitter-login-for-discourse/13395" target="_blank">See this guide to learn more</a>.',
)
end
end
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe ProblemCheck::TwitterLogin do RSpec.describe ProblemCheck::TwitterLogin do
let(:problem_check) { described_class.new } let(:check) { described_class.new }
let(:authenticator) { mock("Auth::TwitterAuthenticator") } let(:authenticator) { mock("Auth::TwitterAuthenticator") }
@ -11,7 +11,7 @@ RSpec.describe ProblemCheck::TwitterLogin do
context "when Twitter authentication isn't enabled" do context "when Twitter authentication isn't enabled" do
before { authenticator.stubs(:enabled?).returns(false) } before { authenticator.stubs(:enabled?).returns(false) }
it { expect(problem_check.call).to be_empty } it { expect(check).to be_chill_about_it }
end end
context "when Twitter authentication appears to work" do context "when Twitter authentication appears to work" do
@ -20,7 +20,7 @@ RSpec.describe ProblemCheck::TwitterLogin do
authenticator.stubs(:healthy?).returns(true) authenticator.stubs(:healthy?).returns(true)
end end
it { expect(problem_check.call).to be_empty } it { expect(check).to be_chill_about_it }
end end
context "when Twitter authentication appears not to work" do context "when Twitter authentication appears not to work" do
@ -31,13 +31,8 @@ RSpec.describe ProblemCheck::TwitterLogin do
end end
it do it do
expect(problem_check.call).to contain_exactly( expect(check).to have_a_problem.with_priority("high").with_message(
have_attributes(
identifier: :twitter_login,
priority: "high",
message:
'Twitter login appears to not be working at the moment. Check the credentials in <a href="foo.bar/admin/site_settings/category/login?filter=twitter">the Site Settings</a>.', 'Twitter login appears to not be working at the moment. Check the credentials in <a href="foo.bar/admin/site_settings/category/login?filter=twitter">the Site Settings</a>.',
),
) )
end end
end end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::UnreachableThemes do
subject(:check) { described_class.new }
describe ".call" do
let(:remote) do
RemoteTheme.create!(
remote_url: "https://github.com/org/testtheme",
last_error_text: last_error,
)
end
before { Fabricate(:theme, id: 50, remote_theme: remote, name: "Test Theme") }
context "when theme is unreachable" do
let(:last_error) { "Can't reach. Too short." }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
'We were unable to check for updates on the following themes:<ul><li><a href="/admin/customize/themes/50">Test Theme</a></li></ul>',
)
end
end
context "when theme is reachable" do
let(:last_error) { nil }
it { expect(check).to be_chill_about_it }
end
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
RSpec.describe ProblemCheck::WatchedWords do
subject(:check) { described_class.new }
context "when all regular expressions are valid" do
before { WordWatcher.stubs(:compiled_regexps_for_action).returns([]) }
it { expect(check).to be_chill_about_it }
end
context "when regular expressions are invalid" do
before { WordWatcher.stubs(:compiled_regexps_for_action).raises(RegexpError.new) }
it do
expect(check).to have_a_problem.with_priority("low").with_message(
"The regular expression for 'Block', 'Censor', 'Require Approval', 'Flag', 'Link', 'Replace', 'Tag', 'Silence' watched words is invalid. Please check your <a href='/admin/customize/watched_words'>Watched Word settings</a>, or disable the 'watched words regular expressions' site setting.",
)
end
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
RSpec::Matchers.define :be_chill_about_it do
match { |service| expect(service.call).to be_empty }
end
RSpec::Matchers.define :have_a_problem do
chain :with_message do |message|
@message = message
end
chain :with_priority do |priority|
@priority = priority
end
match do |service|
@result = service.call
aggregate_failures do
expect(@result).to include(be_a(ProblemCheck::Problem))
expect(@result.first.priority).to(eq(@priority.to_s)) if @priority.present?
expect(@result.first.message).to(eq(@message)) if @message.present?
end
end
failure_message do |service|
if @result.empty?
"Expected check to have a problem, but it was chill about it."
elsif !@result.all?(ProblemCheck::Problem)
"Expected result to contain only instances of `Problem`."
elsif @priority.present? && @result.first.priority != @priority
"Expected problem to have priority `#{@priority}`, but got priority `#{@result.first.priority}`."
elsif @message.present? && @result.first.message != @message
<<~MESSAGE
Expected problem to have message:
> #{@message}
but got message:
> #{@result.first.message}
MESSAGE
end
end
end