0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/spec/services/upcoming_changes/promote_spec.rb
Martin Brennan fb9bb31983
FEATURE: Notify admins of upcoming changes and log events (#37003)
This commit adds several pieces of functionality to help keep admins
in the loop about upcoming changes.

First of all, there is a new initializer on boot that will notify admins
about
newly available upcoming changes, as well as log removed changes and
status movement of existing changes.

* When there is a new upcoming change, we only notify admins about
   it when the status is the `promote_upcoming_changes_on_status` - 1,
   e.g. if `promote_upcoming_changes_on_status` is `beta` then we only
   tell admin about the change once it has reached `alpha`. This means
   we may log the `added` event in one deploy, but only actually notify
   admins in a subsequent deploy.
* We log removed upcoming changes so we can automatically delete old
   site setting data in a future job as needed.

We also now  notify  admins when  upcoming changes are automatically
promoted to enabled based on the site's
`promote_upcoming_changes_on_status`:

<img width="378" height="600" alt="image"
src="https://github.com/user-attachments/assets/4200fbee-9990-4bbc-a378-85946e631e77"
/>

In addition, we now show an indicator in the admin sidebar
if there are new upcoming changes that have been added since
they last visited the upcoming change config page. This data
is stored in a user custom field, because Redis is ephemeral,
and storing in the User table is overkill because 99% of users
are not staff:

<img width="248" height="112" alt="image"
src="https://github.com/user-attachments/assets/4c3d3cf7-ac39-45f8-a2c8-a049cb85b8e9"
/>

Finally, this commit moves both the Track and Promote initializer
logic behind a `DistributedMutex`, we don't want multiple processes
running the same logic here, it needs to be only once.

---------

Co-authored-by: Loïc Guitaut <loic@discourse.org>
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
2026-01-21 12:45:54 +10:00

158 lines
4.9 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe UpcomingChanges::Promote do
describe UpcomingChanges::Promote::Contract, type: :model do
subject(:contract) { described_class.new }
it { is_expected.to validate_presence_of(:setting_name) }
it { is_expected.to validate_presence_of(:promotion_status_threshold) }
it do
is_expected.to validate_inclusion_of(:promotion_status_threshold).in_array(
UpcomingChanges.statuses.keys,
)
end
end
describe ".call" do
subject(:result) { described_class.call(params:, **dependencies) }
fab!(:admin)
let(:current_change_status) { :beta }
let(:dependencies) { { guardian: } }
let(:guardian) { admin.guardian }
let(:params) { { setting_name: :enable_upload_debug_mode, promotion_status_threshold: } }
before do
mock_upcoming_change_metadata(
{
enable_upload_debug_mode: {
impact: "other,developers",
status: current_change_status,
impact_type: "other",
impact_role: "developers",
},
},
)
end
context "when contract is invalid" do
let(:params) { {} }
it { is_expected.to fail_a_contract }
end
context "when the upcoming change does not meet the status promotion criteria" do
let(:promotion_status_threshold) { :stable }
it { is_expected.to fail_a_policy(:meets_promotion_criteria) }
end
context "when the underlying setting for the upcoming change already exists in the DB (admin has modified it)" do
let(:promotion_status_threshold) { :beta }
before do
SiteSetting.enable_upload_debug_mode = false
SiteSetting.create!(
name: "enable_upload_debug_mode",
value: false,
data_type: SiteSetting.types[:bool],
)
end
after { SiteSetting.find_by(name: "enable_upload_debug_mode").destroy! }
context "when the current_change_status is not permanent" do
let(:current_change_status) { :beta }
it { is_expected.to fail_a_policy(:setting_not_modified) }
end
context "when the current_change_status is permanent" do
let(:current_change_status) { :permanent }
it "enables the upcoming change setting" do
expect { result }.to change { SiteSetting.enable_upload_debug_mode }.to be_truthy
end
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
end
end
context "when the upcoming change is already enabled" do
let(:promotion_status_threshold) { :beta }
before { SiteSetting.enable_upload_debug_mode = true }
it { is_expected.to fail_a_policy(:setting_not_already_enabled) }
end
context "when everything is ok" do
fab!(:admin_2, :admin)
let(:promotion_status_threshold) { :beta }
it { is_expected.to run_successfully }
it "enables the upcoming change setting" do
expect { result }.to change { SiteSetting.enable_upload_debug_mode }.to be_truthy
end
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],
).count
}.by(2)
expect(Notification.last.data).to eq(
{
upcoming_change_name: :enable_upload_debug_mode,
upcoming_change_humanized_name: "Enable upload debug mode",
}.to_json,
)
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
end
end
end