diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index 439541a6749..6842391deb5 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -3,33 +3,7 @@ class AdminDashboardData include StatsCacheable - cattr_reader :problem_syms, :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 + cattr_reader :problem_blocks, :problem_messages # kept for backward compatibility GLOBAL_REPORTS ||= [] @@ -51,18 +25,16 @@ class AdminDashboardData def 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| message = instance_exec(&blk) - problems << Problem.new(message) if message.present? + problems << ProblemCheck::Problem.new(message) if message.present? end self.class.problem_messages.each do |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 + problems.concat(ProblemCheck.realtime.flat_map { |c| c.call(@opts).map(&:to_h) }) + problems += self.class.load_found_scheduled_check_problems problems.compact! @@ -76,7 +48,6 @@ class AdminDashboardData end def self.add_problem_check(*syms, &blk) - @@problem_syms.push(*syms) if syms @@problem_blocks << blk if blk end @@ -109,7 +80,7 @@ class AdminDashboardData found_problems.filter_map do |problem| begin - Problem.from_h(JSON.parse(problem)) + ProblemCheck::Problem.from_h(JSON.parse(problem)) rescue JSON::ParserError => err Discourse.warn_exception( err, @@ -130,7 +101,6 @@ class AdminDashboardData # tests. It will also fire multiple times in development mode because # classes are not cached. def self.reset_problem_checks - @@problem_syms = [] @@problem_blocks = [] @@problem_messages = %w[ @@ -139,26 +109,6 @@ class AdminDashboardData 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 } end reset_problem_checks @@ -226,16 +176,6 @@ class AdminDashboardData "#{PROBLEM_MESSAGE_PREFIX}#{i18n_key}" 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 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) @@ -247,164 +187,4 @@ class AdminDashboardData queue_size = Jobs.queued I18n.t("dashboard.queue_size_warning", queue_size: queue_size) if queue_size >= 100_000 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| - "
  • #{CGI.escapeHTML(name)}
  • " - end - .join("\n") - - "#{I18n.t(i18n_key)}" - end end diff --git a/app/models/problem_check.rb b/app/models/problem_check.rb index fd053564ea4..8bc0ea67ff9 100644 --- a/app/models/problem_check.rb +++ b/app/models/problem_check.rb @@ -35,6 +35,10 @@ class ProblemCheck checks.select(&:scheduled?) end + def self.realtime + checks.reject(&:scheduled?) + end + def self.identifier name.demodulize.underscore.to_sym end @@ -45,9 +49,20 @@ class ProblemCheck end delegate :scheduled?, to: :class - def self.call - new.call + def self.realtime? + !scheduled? 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 raise NotImplementedError @@ -55,10 +70,15 @@ class ProblemCheck private - def problem + def problem(override_key = nil) [ 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, identifier:, ), @@ -69,8 +89,16 @@ class ProblemCheck [] end + def message + nil + end + def translation_key # TODO: Infer a default based on class name, then move translations in locale file. raise NotImplementedError end + + def translation_data + {} + end end diff --git a/app/services/problem_check/email_polling_errored_recently.rb b/app/services/problem_check/email_polling_errored_recently.rb new file mode 100644 index 00000000000..2f742b5e5f9 --- /dev/null +++ b/app/services/problem_check/email_polling_errored_recently.rb @@ -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 diff --git a/app/services/problem_check/facebook_config.rb b/app/services/problem_check/facebook_config.rb new file mode 100644 index 00000000000..682297bf07b --- /dev/null +++ b/app/services/problem_check/facebook_config.rb @@ -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 diff --git a/app/services/problem_check/failing_emails.rb b/app/services/problem_check/failing_emails.rb new file mode 100644 index 00000000000..4eabd27d682 --- /dev/null +++ b/app/services/problem_check/failing_emails.rb @@ -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 diff --git a/app/services/problem_check/force_https.rb b/app/services/problem_check/force_https.rb new file mode 100644 index 00000000000..bbc25387d29 --- /dev/null +++ b/app/services/problem_check/force_https.rb @@ -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 diff --git a/app/services/problem_check/github_config.rb b/app/services/problem_check/github_config.rb new file mode 100644 index 00000000000..4f2b4b16784 --- /dev/null +++ b/app/services/problem_check/github_config.rb @@ -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 diff --git a/app/services/problem_check/google_analytics_version.rb b/app/services/problem_check/google_analytics_version.rb new file mode 100644 index 00000000000..2c47cfa25a2 --- /dev/null +++ b/app/services/problem_check/google_analytics_version.rb @@ -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 diff --git a/app/services/problem_check/google_oauth2_config.rb b/app/services/problem_check/google_oauth2_config.rb new file mode 100644 index 00000000000..5cea8531300 --- /dev/null +++ b/app/services/problem_check/google_oauth2_config.rb @@ -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 diff --git a/app/services/problem_check/host_names.rb b/app/services/problem_check/host_names.rb new file mode 100644 index 00000000000..28520181f4f --- /dev/null +++ b/app/services/problem_check/host_names.rb @@ -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 diff --git a/app/services/problem_check/image_magick.rb b/app/services/problem_check/image_magick.rb new file mode 100644 index 00000000000..56131b31adb --- /dev/null +++ b/app/services/problem_check/image_magick.rb @@ -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 diff --git a/app/services/problem_check/missing_mailgun_api_key.rb b/app/services/problem_check/missing_mailgun_api_key.rb new file mode 100644 index 00000000000..d21eb10a15b --- /dev/null +++ b/app/services/problem_check/missing_mailgun_api_key.rb @@ -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 diff --git a/app/services/problem_check/out_of_date_themes.rb b/app/services/problem_check/out_of_date_themes.rb new file mode 100644 index 00000000000..119aa83140e --- /dev/null +++ b/app/services/problem_check/out_of_date_themes.rb @@ -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")}" + end + + def themes_list + out_of_date_themes + .map do |name, id| + "
  • #{CGI.escapeHTML(name)}
  • " + end + .join("\n") + end +end diff --git a/app/services/problem_check/rails_env.rb b/app/services/problem_check/rails_env.rb new file mode 100644 index 00000000000..6f02b6db399 --- /dev/null +++ b/app/services/problem_check/rails_env.rb @@ -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 diff --git a/app/services/problem_check/ram.rb b/app/services/problem_check/ram.rb new file mode 100644 index 00000000000..aebf27bcb22 --- /dev/null +++ b/app/services/problem_check/ram.rb @@ -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 diff --git a/app/services/problem_check/s3_backup_config.rb b/app/services/problem_check/s3_backup_config.rb new file mode 100644 index 00000000000..5b2373020df --- /dev/null +++ b/app/services/problem_check/s3_backup_config.rb @@ -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 diff --git a/app/services/problem_check/s3_cdn.rb b/app/services/problem_check/s3_cdn.rb new file mode 100644 index 00000000000..40fa79df4fb --- /dev/null +++ b/app/services/problem_check/s3_cdn.rb @@ -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 diff --git a/app/services/problem_check/s3_upload_config.rb b/app/services/problem_check/s3_upload_config.rb new file mode 100644 index 00000000000..0580436b891 --- /dev/null +++ b/app/services/problem_check/s3_upload_config.rb @@ -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 diff --git a/app/services/problem_check/subfolder_ends_in_slash.rb b/app/services/problem_check/subfolder_ends_in_slash.rb new file mode 100644 index 00000000000..8da3d618f1e --- /dev/null +++ b/app/services/problem_check/subfolder_ends_in_slash.rb @@ -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 diff --git a/app/services/problem_check/translation_overrides.rb b/app/services/problem_check/translation_overrides.rb new file mode 100644 index 00000000000..d7b22d80da7 --- /dev/null +++ b/app/services/problem_check/translation_overrides.rb @@ -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 diff --git a/app/services/problem_check/twitter_config.rb b/app/services/problem_check/twitter_config.rb new file mode 100644 index 00000000000..4566f3460ad --- /dev/null +++ b/app/services/problem_check/twitter_config.rb @@ -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 diff --git a/app/services/problem_check/unreachable_themes.rb b/app/services/problem_check/unreachable_themes.rb new file mode 100644 index 00000000000..d5a3a7a279a --- /dev/null +++ b/app/services/problem_check/unreachable_themes.rb @@ -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")}" + end + + def themes_list + unreachable_themes + .map do |name, id| + "
  • #{CGI.escapeHTML(name)}
  • " + end + .join("\n") + end +end diff --git a/app/services/problem_check/watched_words.rb b/app/services/problem_check/watched_words.rb new file mode 100644 index 00000000000..49aed44ad66 --- /dev/null +++ b/app/services/problem_check/watched_words.rb @@ -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 diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 779926f1b2c..415ec416a0e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1557,7 +1557,7 @@ en: force_https_warning: "Your website is using SSL. But `force_https` is not yet enabled in your site settings." out_of_date_themes: "Updates are available for 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 Watched Word settings, 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 Watched Word settings, 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. Upgrade to Google Analytics 4 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 Moving to a Single Category Style Site Setting for instructions on how to keep your selected category style." back_from_logster_text: "Back to site" diff --git a/lib/mem_info.rb b/lib/mem_info.rb index c8ea0d0d6c3..d68a8dff93b 100644 --- a/lib/mem_info.rb +++ b/lib/mem_info.rb @@ -18,4 +18,8 @@ class MemInfo nil end end + + def unknown? + mem_total.nil? + end end diff --git a/spec/models/admin_dashboard_data_spec.rb b/spec/models/admin_dashboard_data_spec.rb index fd5d675a9ea..22f8fb96bd1 100644 --- a/spec/models/admin_dashboard_data_spec.rb +++ b/spec/models/admin_dashboard_data_spec.rb @@ -30,27 +30,13 @@ RSpec.describe AdminDashboardData do 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") + prob1 = ProblemCheck::Problem.new("test problem", identifier: "test") + prob2 = ProblemCheck::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( @@ -64,15 +50,15 @@ RSpec.describe AdminDashboardData do end 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.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") + prob1 = ProblemCheck::Problem.new("test problem 1") + prob2 = ProblemCheck::Problem.new("test problem 2", priority: "superbad") expect(prob1.priority).to eq("low") expect(prob2.priority).to eq("low") end @@ -106,44 +92,6 @@ RSpec.describe AdminDashboardData do 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 } @@ -177,175 +125,4 @@ RSpec.describe AdminDashboardData do 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") + - "", - ) - - 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") + - "", - ) - - 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 diff --git a/spec/models/problem_check_spec.rb b/spec/models/problem_check_spec.rb index b608d09cdc2..d7703fa88d1 100644 --- a/spec/models/problem_check_spec.rb +++ b/spec/models/problem_check_spec.rb @@ -4,17 +4,17 @@ 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) + RealtimeCheck = Class.new(described_class) end after(:all) do Object.send(:remove_const, ScheduledCheck.name) - Object.send(:remove_const, UnscheduledCheck.name) + Object.send(:remove_const, RealtimeCheck.name) end # rubocop:enable RSpec/BeforeAfterAll let(:scheduled_check) { ScheduledCheck } - let(:unscheduled_check) { UnscheduledCheck } + let(:realtime_check) { RealtimeCheck } describe ".[]" do it { expect(described_class[:scheduled_check]).to eq(scheduled_check) } @@ -26,16 +26,26 @@ RSpec.describe ProblemCheck do end 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 describe ".scheduled" do 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 describe ".scheduled?" do 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 diff --git a/spec/services/problem_check/email_polling_errored_recently_spec.rb b/spec/services/problem_check/email_polling_errored_recently_spec.rb new file mode 100644 index 00000000000..9383e56336e --- /dev/null +++ b/spec/services/problem_check/email_polling_errored_recently_spec.rb @@ -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 the logs for more details.", + ), + ) + end + end + end +end diff --git a/spec/services/problem_check/facebook_config_spec.rb b/spec/services/problem_check/facebook_config_spec.rb new file mode 100644 index 00000000000..36823dbbf83 --- /dev/null +++ b/spec/services/problem_check/facebook_config_spec.rb @@ -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 the Site Settings and update the settings. See this guide to learn more.', + ) + 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 the Site Settings and update the settings. See this guide to learn more.', + ) + end + end + end +end diff --git a/spec/services/problem_check/failing_emails_spec.rb b/spec/services/problem_check/failing_emails_spec.rb new file mode 100644 index 00000000000..b32479a7f06 --- /dev/null +++ b/spec/services/problem_check/failing_emails_spec.rb @@ -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. See the failed jobs in Sidekiq.', + ) + end + end + end +end diff --git a/spec/services/problem_check/force_https_spec.rb b/spec/services/problem_check/force_https_spec.rb new file mode 100644 index 00000000000..47ecd7e27e7 --- /dev/null +++ b/spec/services/problem_check/force_https_spec.rb @@ -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 `force_https` 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 diff --git a/spec/services/problem_check/github_config_spec.rb b/spec/services/problem_check/github_config_spec.rb new file mode 100644 index 00000000000..1fa171ad75a --- /dev/null +++ b/spec/services/problem_check/github_config_spec.rb @@ -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 the Site Settings and update the settings. See this guide to learn more.', + ) + 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 the Site Settings and update the settings. See this guide to learn more.', + ) + end + end + end +end diff --git a/spec/services/problem_check/google_analytics_version_spec.rb b/spec/services/problem_check/google_analytics_version_spec.rb new file mode 100644 index 00000000000..0f39596da77 --- /dev/null +++ b/spec/services/problem_check/google_analytics_version_spec.rb @@ -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. Upgrade to Google Analytics 4 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 diff --git a/spec/services/problem_check/google_oauth2_config_spec.rb b/spec/services/problem_check/google_oauth2_config_spec.rb new file mode 100644 index 00000000000..3f489947bd7 --- /dev/null +++ b/spec/services/problem_check/google_oauth2_config_spec.rb @@ -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 the Site Settings and update the settings. See this guide to learn more.', + ) + 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 the Site Settings and update the settings. See this guide to learn more.', + ) + end + end + end +end diff --git a/spec/services/problem_check/group_email_credentials_spec.rb b/spec/services/problem_check/group_email_credentials_spec.rb index 3d3a74d9cfa..5eed6ea23c1 100644 --- a/spec/services/problem_check/group_email_credentials_spec.rb +++ b/spec/services/problem_check/group_email_credentials_spec.rb @@ -4,6 +4,8 @@ require "net/smtp" require "net/imap" RSpec.describe ProblemCheck::GroupEmailCredentials do + subject(:check) { described_class.new } + fab!(:group1) { Fabricate(:group) } fab!(:group2) { Fabricate(:smtp_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 expect_no_validate_any SiteSetting.enable_smtp = false - expect(described_class.new.call).to eq([]) + expect(check).to be_chill_about_it end context "with smtp and imap enabled for the site" do @@ -25,7 +27,7 @@ RSpec.describe ProblemCheck::GroupEmailCredentials do expect_no_validate_any group2.update!(smtp_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 it "returns a problem with the group's SMTP settings error" do diff --git a/spec/services/problem_check/host_names_spec.rb b/spec/services/problem_check/host_names_spec.rb new file mode 100644 index 00000000000..12cd4561378 --- /dev/null +++ b/spec/services/problem_check/host_names_spec.rb @@ -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 diff --git a/spec/services/problem_check/image_magick_spec.rb b/spec/services/problem_check/image_magick_spec.rb new file mode 100644 index 00000000000..3815e808648 --- /dev/null +++ b/spec/services/problem_check/image_magick_spec.rb @@ -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 download the latest release.', + ) + 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 diff --git a/spec/services/problem_check/missing_mailgun_api_key_spec.rb b/spec/services/problem_check/missing_mailgun_api_key_spec.rb new file mode 100644 index 00000000000..1b2ea77ba84 --- /dev/null +++ b/spec/services/problem_check/missing_mailgun_api_key_spec.rb @@ -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 diff --git a/spec/services/problem_check/out_of_date_themes_spec.rb b/spec/services/problem_check/out_of_date_themes_spec.rb new file mode 100644 index 00000000000..4e91a045134 --- /dev/null +++ b/spec/services/problem_check/out_of_date_themes_spec.rb @@ -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:', + ) + 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 diff --git a/spec/services/problem_check/rails_env_spec.rb b/spec/services/problem_check/rails_env_spec.rb new file mode 100644 index 00000000000..dfa09a26a24 --- /dev/null +++ b/spec/services/problem_check/rails_env_spec.rb @@ -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 diff --git a/spec/services/problem_check/ram_spec.rb b/spec/services/problem_check/ram_spec.rb new file mode 100644 index 00000000000..70074263197 --- /dev/null +++ b/spec/services/problem_check/ram_spec.rb @@ -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 diff --git a/spec/services/problem_check/s3_backup_config_spec.rb b/spec/services/problem_check/s3_backup_config_spec.rb new file mode 100644 index 00000000000..2d536ed657e --- /dev/null +++ b/spec/services/problem_check/s3_backup_config_spec.rb @@ -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 the Site Settings and update the settings. See "How to set up image uploads to S3?" to learn more.', + ) + 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 the Site Settings and update the settings. See "How to set up image uploads to S3?" to learn more.', + ) + end + end + end + end + end +end diff --git a/spec/services/problem_check/s3_cdn_spec.rb b/spec/services/problem_check/s3_cdn_spec.rb new file mode 100644 index 00000000000..83b5a163527 --- /dev/null +++ b/spec/services/problem_check/s3_cdn_spec.rb @@ -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. See "Using Object Storage for Uploads" to learn more.', + ) + 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 diff --git a/spec/services/problem_check/s3_upload_config_spec.rb b/spec/services/problem_check/s3_upload_config_spec.rb new file mode 100644 index 00000000000..656e4d8eb7a --- /dev/null +++ b/spec/services/problem_check/s3_upload_config_spec.rb @@ -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 the Site Settings and update the settings. See "How to set up image uploads to S3?" to learn more.', + ) + 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 the Site Settings and update the settings. See "How to set up image uploads to S3?" to learn more.', + ) + end + end + end + end + end +end diff --git a/spec/services/problem_check/subfolder_ends_in_slash_spec.rb b/spec/services/problem_check/subfolder_ends_in_slash_spec.rb new file mode 100644 index 00000000000..55f336586fc --- /dev/null +++ b/spec/services/problem_check/subfolder_ends_in_slash_spec.rb @@ -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 diff --git a/spec/services/problem_check/translation_overrides_spec.rb b/spec/services/problem_check/translation_overrides_spec.rb new file mode 100644 index 00000000000..88f3dadc907 --- /dev/null +++ b/spec/services/problem_check/translation_overrides_spec.rb @@ -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 text customizations.", + ) + 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 text customizations.", + ) + 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 diff --git a/spec/services/problem_check/twitter_config_spec.rb b/spec/services/problem_check/twitter_config_spec.rb new file mode 100644 index 00000000000..9a01d92a66a --- /dev/null +++ b/spec/services/problem_check/twitter_config_spec.rb @@ -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 the Site Settings and update the settings. See this guide to learn more.', + ) + 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 the Site Settings and update the settings. See this guide to learn more.', + ) + end + end + end +end diff --git a/spec/services/problem_check/twitter_login_spec.rb b/spec/services/problem_check/twitter_login_spec.rb index 0a94b6af620..40408803be2 100644 --- a/spec/services/problem_check/twitter_login_spec.rb +++ b/spec/services/problem_check/twitter_login_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe ProblemCheck::TwitterLogin do - let(:problem_check) { described_class.new } + let(:check) { described_class.new } let(:authenticator) { mock("Auth::TwitterAuthenticator") } @@ -11,7 +11,7 @@ RSpec.describe ProblemCheck::TwitterLogin do context "when Twitter authentication isn't enabled" do before { authenticator.stubs(:enabled?).returns(false) } - it { expect(problem_check.call).to be_empty } + it { expect(check).to be_chill_about_it } end context "when Twitter authentication appears to work" do @@ -20,7 +20,7 @@ RSpec.describe ProblemCheck::TwitterLogin do authenticator.stubs(:healthy?).returns(true) end - it { expect(problem_check.call).to be_empty } + it { expect(check).to be_chill_about_it } end context "when Twitter authentication appears not to work" do @@ -31,13 +31,8 @@ RSpec.describe ProblemCheck::TwitterLogin do end it do - expect(problem_check.call).to contain_exactly( - have_attributes( - identifier: :twitter_login, - priority: "high", - message: - 'Twitter login appears to not be working at the moment. Check the credentials in the Site Settings.', - ), + expect(check).to have_a_problem.with_priority("high").with_message( + 'Twitter login appears to not be working at the moment. Check the credentials in the Site Settings.', ) end end diff --git a/spec/services/problem_check/unreachable_themes_spec.rb b/spec/services/problem_check/unreachable_themes_spec.rb new file mode 100644 index 00000000000..089661aab3a --- /dev/null +++ b/spec/services/problem_check/unreachable_themes_spec.rb @@ -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:', + ) + end + end + + context "when theme is reachable" do + let(:last_error) { nil } + + it { expect(check).to be_chill_about_it } + end + end +end diff --git a/spec/services/problem_check/watched_words_spec.rb b/spec/services/problem_check/watched_words_spec.rb new file mode 100644 index 00000000000..32eb65f33c3 --- /dev/null +++ b/spec/services/problem_check/watched_words_spec.rb @@ -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 Watched Word settings, or disable the 'watched words regular expressions' site setting.", + ) + end + end +end diff --git a/spec/support/problem_check_matcher.rb b/spec/support/problem_check_matcher.rb new file mode 100644 index 00000000000..623bc1cb389 --- /dev/null +++ b/spec/support/problem_check_matcher.rb @@ -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