0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/spec/services/user/bulk_suspend_spec.rb
Régis Hanol a1ebd19813
FEATURE: Bulk suspend users and filter the admin users list by activation (#41227)
- Adds an activation filter (All / Activated / Not activated) to the
Admin > Users > New tab so unverified accounts can be isolated in the
UI.
- Adds a bulk suspend action next to the existing bulk delete, plus a
"Select all" / "Clear all" control so a whole page of accounts can be
actioned at once.
- Shows the suspend reason on the Suspended tab, mirroring the silenced
tab.

Already-suspended users are excluded from bulk suspension, and the
penalty-reason columns are populated from a single batched query instead
of one query per row.

**SCREENSHOTS**

<img width="1400" height="1200"
alt="desktop-horizon-light-admin-users-activation-filter"
src="https://github.com/user-attachments/assets/31165d10-42cb-479b-897c-fafeb3283526"
/>
<img width="1400" height="1200"
alt="desktop-horizon-light-admin-users-bulk-select"
src="https://github.com/user-attachments/assets/037e8149-2ac6-40de-aaae-0d0e443ec143"
/>
<img width="1400" height="1200"
alt="desktop-horizon-light-admin-users-suspended"
src="https://github.com/user-attachments/assets/e6dbefed-4aaf-49bb-a9bd-52c7253e14b6"
/>
<img width="1400" height="1200"
alt="desktop-horizon-light-admin-users-bulk-suspend"
src="https://github.com/user-attachments/assets/5bdf85ef-4e30-4d8d-b521-d8639c1a2b97"
/>
2026-06-29 11:26:34 +02:00

77 lines
2.4 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe User::BulkSuspend do
describe described_class::Contract, type: :model do
it { is_expected.to validate_length_of(:user_ids).as_array.is_at_most(100).is_at_least(1) }
it { is_expected.to validate_presence_of(:reason) }
it { is_expected.to validate_length_of(:reason).is_at_most(300) }
it { is_expected.to validate_presence_of(:suspend_until) }
end
describe ".call" do
subject(:result) { described_class.call(params:, **dependencies) }
fab!(:admin)
fab!(:users) { Fabricate.times(5, :user) }
let(:params) { { user_ids:, reason: "spam wave", suspend_until: 1.year.from_now } }
let(:dependencies) { { guardian: } }
let(:guardian) { admin.guardian }
let(:user_ids) { users.map(&:id) }
context "when invalid data is provided" do
let(:user_ids) { nil }
it { is_expected.to fail_a_contract }
end
context "when no reason is provided" do
let(:params) { { user_ids:, suspend_until: 1.year.from_now } }
it { is_expected.to fail_a_contract }
end
context "when provided users does not exist" do
let(:user_ids) { 0 }
it { is_expected.to fail_to_find_a_model(:users) }
end
context "when at least one user cannot be suspended" do
before { users << Fabricate(:admin) }
it { is_expected.to fail_a_policy(:can_suspend_users) }
end
context "when at least one user is already suspended" do
before { users.first.update!(suspended_till: 1.day.from_now, suspended_at: Time.zone.now) }
it { is_expected.to fail_a_policy(:can_suspend_users) }
end
context "when everything's ok" do
before { allow(MessageBus).to receive(:publish) }
it "suspends each user" do
result
expect(User.where(id: user_ids).where.not(suspended_till: nil).count).to eq(users.size)
end
it "logs the suspension with its reason" do
result
histories =
UserHistory.where(action: UserHistory.actions[:suspend_user], target_user_id: user_ids)
expect(histories.count).to eq(users.size)
expect(histories.pluck(:details).uniq).to eq(["spam wave"])
end
it "publishes suspension progress" do
result
expect(MessageBus).to have_received(:publish)
.with("/bulk-user-suspend", a_kind_of(Hash), user_ids: [admin.id])
.exactly(user_ids.size)
.times
end
end
end
end