0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/spec/services/upcoming_changes/notify_promotion_spec.rb
Martin Brennan 4de06a006b
FIX: Do not notify new sites or plugins enabled of old upcoming changes (#42045)
For upcoming changes, we were having a problem where we
were notifying admins of upcoming changes in brand new sites.
A previous fix 2e5afb46e2f5c0547bc298b2bd1a9c39e813dc17 tried
to remedy this, but since it relies on the `new_site?` method, which
isn't true after 1 hour, the next time the scheduled notifications are
run for an admin they will be sent them even if their site is relatively
new.

This commit attempts to conclusively fix the issue by inserting
upcoming change `admins_notified_automatic_promotion` and
`admins_notified_available_change` events via `BackfillNotifiedEvents`
for changes that exist in particular statuses when the site is first
created. These events will have `event_data: {"backfilled" => true}`
to distinguish them from others.

It also fixes an adjacent issue where an admin was notified of a
plugin's upcoming changes as soon as they enable the plugin,
which isn't useful as the intent of upcoming changes is to notify
of changes for things the admin has already been using.

**Before this change, on brand new site with current upcoming change
state**

<img width="401" height="429" alt="image"
src="https://github.com/user-attachments/assets/a89cd641-59aa-4c48-a57c-9692021df176"
/>

**After this change**

No notification to show :) But when I moved an upcoming change to `beta`
it showed
the notification for the enabled upcoming change correctly:

<img width="378" height="174" alt="image"
src="https://github.com/user-attachments/assets/20e4096f-e83f-4c06-970d-a9b43561b12e"
/>
2026-07-30 09:51:08 +10:00

430 lines
14 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe UpcomingChanges::NotifyPromotion do
describe UpcomingChanges::NotifyPromotion::Contract, type: :model do
it { is_expected.to validate_presence_of(:setting_name) }
it { is_expected.to validate_presence_of(:admin_user_ids) }
end
describe ".call" do
subject(:result) do
described_class.call(
params: {
setting_name:,
admin_user_ids:,
changes_already_notified_about_promotion:,
changes_already_promoted:,
},
guardian: Discourse.system_user.guardian,
)
end
fab!(:admin)
fab!(:admin_2, :admin)
let(:setting_name) { :enable_upload_debug_mode }
let(:admin_user_ids) { [admin.id, admin_2.id] }
let(:changes_already_notified_about_promotion) { [] }
let(:changes_already_promoted) { [] }
let(:setting_status) { :stable }
before do
# No upcoming change notifications are sent for new sites
UpcomingChanges.stubs(:should_notify_admins?).returns(true)
SiteSetting.promote_upcoming_changes_on_status = :stable
mock_upcoming_change_metadata(
enable_upload_debug_mode: {
impact: "other,developers",
status: setting_status,
impact_type: "other",
impact_role: "developers",
},
)
end
context "when data is invalid" do
let(:setting_name) { nil }
it { is_expected.to fail_a_contract }
end
context "when setting is not available" do
let(:setting_name) { :nonexistent_setting }
it { is_expected.to fail_a_policy(:setting_is_available) }
end
context "when setting does not meet or exceed promotion status" do
let(:setting_status) { :beta }
it { is_expected.to fail_a_policy(:meets_or_exceeds_status) }
end
context "when the change is owned by a plugin that is not configurable" do
let(:setting_name) { :enable_experimental_sample_plugin_feature }
before do
SiteSetting::SAMPLE_TEST_PLUGIN.stubs(:configurable?).returns(false)
mock_upcoming_change_metadata(
enable_experimental_sample_plugin_feature: {
impact: "feature,admins",
status: :stable,
},
)
end
it { is_expected.to fail_a_policy(:change_should_be_displayed) }
it "does not fire the upcoming_change_enabled event" do
events = DiscourseEvent.track_events(:upcoming_change_enabled) { result }
expect(events).to be_empty
end
end
context "when the change should not be displayed on this site" do
before do
UpcomingChanges::ConditionalDisplay.stubs(
:should_display_enable_upload_debug_mode?,
).returns(false)
end
it { is_expected.to fail_a_policy(:change_should_be_displayed) }
it "does not notify admins or create an event" do
expect { result }.to not_change {
Notification.where(
notification_type: Notification.types[:upcoming_change_automatically_promoted],
).count
}.and not_change {
UpcomingChangeEvent.where(
event_type: :admins_notified_automatic_promotion,
upcoming_change_name: :enable_upload_debug_mode,
).count
}
end
end
context "when the change has already been promoted" do
let(:changes_already_promoted) { [:enable_upload_debug_mode] }
it { is_expected.to fail_a_policy(:promotion_not_already_handled) }
end
context "when the change has already been notified about, but not yet promoted" do
let(:changes_already_notified_about_promotion) { [:enable_upload_debug_mode] }
it { is_expected.to run_successfully }
it "does not notify admins" do
expect { result }.not_to change {
Notification.where(
notification_type: Notification.types[:upcoming_change_automatically_promoted],
).count
}
end
it "does not record that admins were notified" do
expect { result }.not_to change {
UpcomingChangeEvent.where(
event_type: :admins_notified_automatic_promotion,
upcoming_change_name: :enable_upload_debug_mode,
).count
}
end
it "still promotes the change for real" do
events = DiscourseEvent.track_events { result }
expect(
events.select do |e|
e[:event_name] == :upcoming_change_enabled &&
e[:params].first == :enable_upload_debug_mode
end,
).to be_present
expect(
UpcomingChangeEvent.exists?(
event_type: :automatically_promoted,
upcoming_change_name: :enable_upload_debug_mode,
),
).to eq(true)
end
end
context "when admin has manually opted out" do
before { SiteSetting.enable_upload_debug_mode = false }
it { is_expected.to fail_a_policy(:admin_has_not_manually_toggled) }
end
context "when admin has manually opted in" do
before { SiteSetting.enable_upload_debug_mode = true }
it { is_expected.to fail_a_policy(:admin_has_not_manually_toggled) }
end
context "when the site is new (< 1 hour old)" do
before { UpcomingChanges.stubs(:should_notify_admins?).returns(false) }
it { is_expected.to fail_a_policy(:should_notify_admins) }
end
context "when everything's ok" do
let(:notification) do
Notification.where("data::text LIKE ?", "%enable_upload_debug_mode%").last
end
let(:events) { DiscourseEvent.track_events { result } }
let(:event) do
events.find do |e|
e[:event_name] == :upcoming_change_enabled &&
e[:params].first == :enable_upload_debug_mode
end
end
it { is_expected.to run_successfully }
it "logs the change context in the staff action log" do
expect { result }.to change {
UserHistory.where(
action: UserHistory.actions[:upcoming_change_toggled],
subject: "enable_upload_debug_mode",
).count
}.by(1)
expect(UserHistory.last.context).to eq(
I18n.t(
"staff_action_logs.upcoming_changes.log_promoted",
change_status: UpcomingChanges.change_status(:enable_upload_debug_mode).to_s.titleize,
base_path: Discourse.base_path,
),
)
end
it "notifies admins about the upcoming change" do
expect { result }.to change {
Notification
.where(
notification_type: Notification.types[:upcoming_change_automatically_promoted],
user_id: [admin.id, admin_2.id],
)
.where("data::text LIKE ?", "%enable_upload_debug_mode%")
.count
}.by(2)
data = JSON.parse(notification.data)
expect(data["upcoming_change_names"]).to eq(["enable_upload_debug_mode"])
expect(data["upcoming_change_humanized_names"]).to eq(["Enable upload debug mode"])
expect(data["count"]).to eq(1)
end
it "creates an admins_notified_automatic_promotion event" do
expect { result }.to change {
UpcomingChangeEvent.where(
event_type: :admins_notified_automatic_promotion,
upcoming_change_name: :enable_upload_debug_mode,
).count
}.by(1)
end
it "creates an automatically_promoted event recording the promotion" do
expect { result }.to change {
UpcomingChangeEvent.where(
event_type: :automatically_promoted,
upcoming_change_name: :enable_upload_debug_mode,
).count
}.by(1)
end
it "does not create a duplicate automatically_promoted event when one already exists" do
UpcomingChangeEvent.create!(
event_type: :automatically_promoted,
upcoming_change_name: :enable_upload_debug_mode,
)
expect { result }.not_to change {
UpcomingChangeEvent.where(
event_type: :automatically_promoted,
upcoming_change_name: :enable_upload_debug_mode,
).count
}
end
it "triggers DiscourseEvent for the promoted setting" do
expect(event[:params]).to eq([:enable_upload_debug_mode])
end
context "when there is an existing unread notification" do
before do
Fabricate(
:notification,
user: admin,
notification_type: Notification.types[:upcoming_change_automatically_promoted],
read: false,
data: {
upcoming_change_names: ["other_change"],
upcoming_change_humanized_names: ["Other change"],
count: 1,
}.to_json,
)
end
it "skips sending email when consolidating notifications" do
allow(Notification::Action::BulkCreate).to receive(:call).and_call_original
result
expect(Notification::Action::BulkCreate).to have_received(:call).with(
satisfy { |args| args[:skip_send_email] == true },
)
end
it "consolidates into a single notification per admin" do
result
notifications =
Notification.where(
notification_type: Notification.types[:upcoming_change_automatically_promoted],
user_id: admin.id,
)
expect(notifications.count).to eq(1)
data = JSON.parse(notifications.first.data)
expect(data["upcoming_change_names"]).to contain_exactly(
"other_change",
"enable_upload_debug_mode",
)
expect(data["count"]).to eq(2)
end
end
context "when there is an existing read notification" do
before do
Fabricate(
:notification,
user: admin,
notification_type: Notification.types[:upcoming_change_automatically_promoted],
read: true,
data: {
upcoming_change_names: ["other_change"],
upcoming_change_humanized_names: ["Other change"],
count: 1,
}.to_json,
)
end
it "does not skip sending email when not consolidating notifications" do
allow(Notification::Action::BulkCreate).to receive(:call).and_call_original
result
expect(Notification::Action::BulkCreate).to have_received(:call).with(
satisfy { |args| args[:skip_send_email] == false },
)
end
it "does not consolidate with the read notification" do
result
notifications =
Notification.where(
notification_type: Notification.types[:upcoming_change_automatically_promoted],
user_id: admin.id,
)
expect(notifications.count).to eq(2)
end
end
context "when the same change is already in an unread notification" do
before do
Fabricate(
:notification,
user: admin,
notification_type: Notification.types[:upcoming_change_automatically_promoted],
read: false,
data: {
upcoming_change_names: ["enable_upload_debug_mode"],
upcoming_change_humanized_names: ["Enable upload debug mode"],
count: 1,
}.to_json,
)
end
it "deduplicates the change names" do
result
notifications =
Notification.where(
notification_type: Notification.types[:upcoming_change_automatically_promoted],
user_id: admin.id,
)
expect(notifications.count).to eq(1)
data = JSON.parse(notifications.first.data)
expect(data["upcoming_change_names"]).to eq(["enable_upload_debug_mode"])
expect(data["count"]).to eq(1)
end
end
context "when the notification creation fails" do
before do
Notification::Action::BulkCreate.stubs(:call).raises(ActiveRecord::StatementInvalid.new)
end
it "rolls back the staff action log" do
expect { result }.not_to change {
UserHistory.where(
action: UserHistory.actions[:upcoming_change_toggled],
subject: "enable_upload_debug_mode",
).count
}
end
it "rolls back the upcoming change event" do
expect { result }.not_to change {
UpcomingChangeEvent.where(
event_type: :admins_notified_automatic_promotion,
upcoming_change_name: :enable_upload_debug_mode,
).count
}
end
it "fails the service" do
expect(result).to fail_with_exception
end
end
context "when there is an existing notification with the old data format" do
before do
Fabricate(
:notification,
user: admin,
notification_type: Notification.types[:upcoming_change_automatically_promoted],
read: false,
data: {
upcoming_change_name: "other_change",
upcoming_change_humanized_name: "Other change",
}.to_json,
)
end
it "merges old format into the new array format" do
result
notifications =
Notification.where(
notification_type: Notification.types[:upcoming_change_automatically_promoted],
user_id: admin.id,
)
expect(notifications.count).to eq(1)
data = JSON.parse(notifications.first.data)
expect(data["upcoming_change_names"]).to contain_exactly(
"other_change",
"enable_upload_debug_mode",
)
expect(data["count"]).to eq(2)
end
end
end
end
end