mirror of
https://github.com/discourse/discourse.git
synced 2026-08-08 17:53:55 +08:00
Previously, suspect-user reviewables ("user needs approval" flags) and
queued-post reviewables could only be resolved by deleting the user or
rejecting the post outright — a reviewer mistake on an automated false
positive was irreversible, and sites with no-deletion policies had to
resolve the flag and then manually suspend the user from their admin
page. Requested in [meta t/408891](https://meta.discourse.org/t/408891)
and [meta t/225660](https://meta.discourse.org/t/225660).
This change adds guardian-gated **Silence user** / **Suspend user**
resolutions to both queues via a shared `build_penalty_actions` helper,
using the same penalize-modal flow the flagged-post and
review-every-post queues already use (staff log linked back to the
reviewable). Along the way it:
- hides penalty actions that are already active everywhere (previously a
resolve-then-409 dead end), and fixes the penalize modal's unawaited
`before()` race so the penalty is only applied once the reviewable
action succeeded;
- narrows the rejected-user **scrub** affordance to records whose
identity snapshot is the last remaining copy (user deleted, or
renamed/anonymized after a failed deletion), admin-gated to match the
endpoint;
- shows an active penalty (localized end date and reason) on the
reviewable user card, so it's clear why a penalty option is absent;
- preloads `anonymous_user_master` on the queue since the new
`silenced?` gates would otherwise lazy-load it per row;
- rewrites the action descriptions in one consistent voice.
<img width="460" height="313" alt="2026-07-31 @ 07 46 57"
src="https://github.com/user-attachments/assets/87d5bf49-97e0-47a4-939e-16da3f55c6e1"
/>
97 lines
3.9 KiB
Ruby
Vendored
97 lines
3.9 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe ReviewableUserSerializer do
|
|
let(:user) { Fabricate(:user) }
|
|
let(:admin) { Fabricate(:admin) }
|
|
let(:moderator) { Fabricate(:moderator) }
|
|
let(:reviewable) { Reviewable.find_by(target: user) }
|
|
|
|
before do
|
|
SiteSetting.must_approve_users = true
|
|
Jobs::CreateUserReviewable.new.execute(user_id: user.id)
|
|
end
|
|
|
|
it "includes the user fields for review" do
|
|
json = ReviewableUserSerializer.new(reviewable, scope: Guardian.new(admin), root: nil).as_json
|
|
expect(json[:user_id]).to eq(reviewable.target_id)
|
|
expect(json[:payload]["username"]).to eq(user.username)
|
|
expect(json[:payload]["email"]).to eq(user.email)
|
|
expect(json[:payload]["name"]).to eq(user.name)
|
|
expect(json[:topic_url]).to be_blank
|
|
end
|
|
|
|
it "excludes the email user field for moderators" do
|
|
json =
|
|
ReviewableUserSerializer.new(reviewable, scope: Guardian.new(moderator), root: nil).as_json
|
|
expect(json[:user_id]).to eq(reviewable.target_id)
|
|
expect(json[:payload]["username"]).to eq(user.username)
|
|
expect(json[:payload]["email"]).to eq(nil)
|
|
expect(json[:payload]["name"]).to eq(user.name)
|
|
expect(json[:topic_url]).to be_blank
|
|
end
|
|
|
|
it "includes the email user field for moderators if enabled" do
|
|
SiteSetting.moderators_view_emails = true
|
|
|
|
json =
|
|
ReviewableUserSerializer.new(reviewable, scope: Guardian.new(moderator), root: nil).as_json
|
|
expect(json[:user_id]).to eq(reviewable.target_id)
|
|
expect(json[:payload]["username"]).to eq(user.username)
|
|
expect(json[:payload]["email"]).to eq(user.email)
|
|
expect(json[:payload]["name"]).to eq(user.name)
|
|
expect(json[:topic_url]).to be_blank
|
|
end
|
|
|
|
it "includes the scrubbed fields for scrubbed reviewables" do
|
|
reviewable.scrub("reason", Guardian.new(admin))
|
|
|
|
json = ReviewableUserSerializer.new(reviewable, scope: Guardian.new(admin), root: nil).as_json
|
|
expect(json[:user_id]).to eq(reviewable.target_id)
|
|
expect(json[:payload]["username"]).to eq(nil)
|
|
expect(json[:payload]["email"]).to eq(nil)
|
|
expect(json[:payload]["name"]).to eq(nil)
|
|
expect(json[:payload]["scrubbed_by"]).to eq(admin.username)
|
|
expect(json[:payload]["scrubbed_reason"]).to eq("reason")
|
|
expect(json[:payload]["scrubbed_at"]).to be_present
|
|
expect(json[:topic_url]).to be_blank
|
|
end
|
|
|
|
describe "target_user" do
|
|
it "returns nil when there is no target" do
|
|
reviewable = ReviewableUser.new
|
|
json = ReviewableUserSerializer.new(reviewable, scope: Guardian.new(admin), root: nil).as_json
|
|
expect(json[:target_user]).to be_nil
|
|
end
|
|
|
|
it "returns FlaggedUserSerializer when there is a target" do
|
|
json = ReviewableUserSerializer.new(reviewable, scope: Guardian.new(admin), root: nil).as_json
|
|
expect(json[:target_user]).to be_present
|
|
expect(json[:target_user][:id]).to eq(user.id)
|
|
expect(json[:target_user][:username]).to eq(user.username)
|
|
end
|
|
|
|
it "exposes an active penalty with its reason" do
|
|
user.update!(silenced_till: 1.month.from_now)
|
|
UserHistory.create!(
|
|
action: UserHistory.actions[:silence_user],
|
|
acting_user_id: admin.id,
|
|
target_user_id: user.id,
|
|
details: "Promotional links in bio",
|
|
)
|
|
|
|
json = ReviewableUserSerializer.new(reviewable, scope: Guardian.new(admin), root: nil).as_json
|
|
expect(json[:target_user][:silenced_till]).to be_present
|
|
expect(json[:target_user][:silence_reason]).to eq("Promotional links in bio")
|
|
expect(json[:target_user]).not_to have_key(:suspended_till)
|
|
expect(json[:target_user]).not_to have_key(:suspend_reason)
|
|
end
|
|
|
|
it "doesn't expose expired penalties" do
|
|
user.update!(suspended_till: 1.day.ago, suspended_at: 1.month.ago)
|
|
|
|
json = ReviewableUserSerializer.new(reviewable, scope: Guardian.new(admin), root: nil).as_json
|
|
expect(json[:target_user]).not_to have_key(:suspended_till)
|
|
expect(json[:target_user]).not_to have_key(:suspend_reason)
|
|
end
|
|
end
|
|
end
|