mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
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"
/>
430 lines
14 KiB
Ruby
Vendored
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
|