2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-03-04 01:15:08 +08:00
discourse/spec/models/problem_check_tracker_spec.rb
Ted Johansson 8ca5fb706a
DEV: Remove admin notices when destroying problem check trackers (#35976)
When a problem check is run with an invalid target, which can happen if
a target is dynamically removed from the list, we destroy the problem
check tracker.

This makes it so that we also clean up any associated admin notices when
this happens.

It also marks `target` as required. This is because we now use
`__NULL__` to indicate "no target". This allows us to clean up the Ruby
implementation a bit. (Context: this is required for uniqueness checks
to work, since PostgreSQL considers `NULL` values to be distinct.)
2025-11-13 10:34:28 +08:00

312 lines
8.7 KiB
Ruby

# frozen_string_literal: true
RSpec.describe ProblemCheckTracker do
describe "validations" do
let(:record) { described_class.new(identifier: "twitter_login") }
it { expect(record).to validate_presence_of(:identifier) }
it { expect(record).to validate_uniqueness_of(:identifier).scoped_to(:target) }
it { expect(record).to validate_numericality_of(:blips).is_greater_than_or_equal_to(0) }
it { expect(record).to validate_presence_of(:target) }
end
describe "callbacks" do
describe "before_destroy (silence the alarm)" do
let(:tracker) do
ProblemCheckTracker.create!(identifier: "twitter_login", target: ProblemCheck::NO_TARGET)
end
before { tracker.problem! }
it "removes any associated admin notices" do
expect { tracker.destroy }.to change { AdminNotice.count }.by(-1)
end
end
end
describe ".[]" do
before { Fabricate(:problem_check_tracker, identifier: "twitter_login") }
context "when the problem check tracker already exists" do
it { expect(described_class[:twitter_login]).not_to be_new_record }
end
context "when the problem check tracker doesn't exist yet" do
it { expect(described_class[:facebook_login]).to be_previously_new_record }
end
end
describe "#check" do
before do
Fabricate(:problem_check_tracker, identifier: "twitter_login")
Fabricate(:problem_check_tracker, identifier: "missing_check")
end
context "when the tracker has a corresponding check" do
it { expect(described_class[:twitter_login].check.new).to be_a(ProblemCheck) }
end
context "when the checking logic of the tracker has been removed or renamed" do
it do
expect { described_class[:missing_check].check }.to change { described_class.count }.by(-1)
end
end
end
describe "#ready_to_run?" do
let(:problem_tracker) { described_class.new(next_run_at:) }
context "when the next run timestamp is not set" do
let(:next_run_at) { nil }
it { expect(problem_tracker).to be_ready_to_run }
end
context "when the next run timestamp is in the past" do
let(:next_run_at) { 5.minutes.ago }
it { expect(problem_tracker).to be_ready_to_run }
end
context "when the next run timestamp is in the future" do
let(:next_run_at) { 5.minutes.from_now }
it { expect(problem_tracker).not_to be_ready_to_run }
end
end
describe "#failing?" do
before { freeze_time }
let(:problem_tracker) { described_class.new(last_problem_at:, last_run_at:, last_success_at:) }
context "when the last run passed" do
let(:last_run_at) { 1.minute.ago }
let(:last_success_at) { 1.minute.ago }
let(:last_problem_at) { 11.minutes.ago }
it { expect(problem_tracker).not_to be_failing }
end
context "when the last run had a problem" do
let(:last_run_at) { 1.minute.ago }
let(:last_success_at) { 11.minutes.ago }
let(:last_problem_at) { 1.minute.ago }
it { expect(problem_tracker).to be_failing }
end
end
describe "#passing?" do
before { freeze_time }
let(:problem_tracker) { described_class.new(last_problem_at:, last_run_at:, last_success_at:) }
context "when the last run passed" do
let(:last_run_at) { 1.minute.ago }
let(:last_success_at) { 1.minute.ago }
let(:last_problem_at) { 11.minutes.ago }
it { expect(problem_tracker).to be_passing }
end
context "when the last run had a problem" do
let(:last_run_at) { 1.minute.ago }
let(:last_success_at) { 11.minutes.ago }
let(:last_problem_at) { 1.minute.ago }
it { expect(problem_tracker).not_to be_passing }
end
end
describe "#problem!" do
let(:problem_tracker) do
Fabricate(
:problem_check_tracker,
identifier: "twitter_login",
target: "foo",
**original_attributes,
)
end
let(:original_attributes) do
{
blips:,
last_problem_at: 1.week.ago,
last_success_at: 24.hours.ago,
last_run_at: 24.hours.ago,
next_run_at: nil,
}
end
let(:blips) { 0 }
let(:updated_attributes) { { blips: 1 } }
it do
freeze_time
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.to change {
problem_tracker.attributes
}.to(hash_including(updated_attributes))
end
context "when the maximum number of blips have been surpassed" do
let(:blips) { 1 }
it "sounds the alarm" do
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.to change {
AdminNotice.problem.count
}.by(1)
end
end
context "when the details of the problem change but the problem remains" do
let(:blips) { 1 }
it "updates the notice" do
original_details = {
themes_list:
"<ul><li><a href=\"/admin/customize/themes/13\">discourse-blank-theme</a></li> <li><a href=\"/admin/customize/themes/31\">Simple Theme</a></li></ul>",
base_path: "",
}
expect do problem_tracker.problem!(details: original_details) end.to change {
AdminNotice.problem.count
}.by(1)
admin_notice = AdminNotice.problem.find_by(identifier: "twitter_login")
expect(
admin_notice.details.merge(target: problem_tracker.target).with_indifferent_access,
).to eq(original_details.merge(target: problem_tracker.target).with_indifferent_access)
new_details = {
themes_list: "<ul><li><a href=\"/admin/customize/themes/31\">Simple Theme</a></li></ul>",
base_path: "",
}
expect do problem_tracker.problem!(details: new_details) end.not_to change {
AdminNotice.problem.count
}
admin_notice.reload
expect(
admin_notice.details.merge(target: problem_tracker.target).with_indifferent_access,
).to eq(new_details.merge(target: problem_tracker.target).with_indifferent_access)
end
end
context "when there's an alarm sounding for multi-target trackers" do
let(:blips) { 1 }
before do
Fabricate(
:admin_notice,
subject: "problem",
identifier: "twitter_login",
details: {
target: target,
},
)
end
context "when the alarm is for a different target" do
let(:target) { "bar" }
it "sounds the alarm" do
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.to change {
AdminNotice.problem.count
}.by(1)
end
end
context "when the alarm is for a the same target" do
let(:target) { "foo" }
it "does not duplicate the alarm" do
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.not_to change {
AdminNotice.problem.count
}
end
end
end
context "when there are still blips to go" do
let(:blips) { 0 }
before { ProblemCheck::TwitterLogin.stubs(:max_blips).returns(1) }
it "does not sound the alarm" do
expect { problem_tracker.problem!(next_run_at: 24.hours.from_now) }.not_to change {
AdminNotice.problem.count
}
end
end
end
describe "#no_problem!" do
let(:next_run_at) { 24.hours.from_now.round(6) }
let(:problem_tracker) do
Fabricate(:problem_check_tracker, identifier: "twitter_login", **original_attributes)
end
let(:original_attributes) do
{
blips: 0,
last_problem_at: 1.week.ago,
last_success_at: Time.current,
last_run_at: 24.hours.ago,
next_run_at: nil,
}
end
let(:updated_attributes) { { blips: 0, next_run_at: } }
it do
freeze_time
expect { problem_tracker.no_problem!(next_run_at:) }.to change {
problem_tracker.attributes
}.to(hash_including(updated_attributes))
end
context "when there's an alarm sounding" do
before { problem_tracker.problem! }
it "silences the alarm" do
expect { problem_tracker.no_problem!(next_run_at: 24.hours.from_now) }.to change {
AdminNotice.problem.count
}.by(-1)
end
end
end
describe "#reset" do
let(:problem_tracker) do
Fabricate(:problem_check_tracker, identifier: "twitter_login", **original_attributes)
end
let(:original_attributes) do
{
blips: 0,
last_problem_at: 1.week.ago,
last_success_at: Time.current,
last_run_at: 24.hours.ago,
next_run_at: nil,
}
end
let(:updated_attributes) { { blips: 0 } }
it do
freeze_time
expect { problem_tracker.reset(next_run_at: 24.hours.from_now) }.to change {
problem_tracker.attributes
}.to(hash_including(updated_attributes))
end
end
end