discourse-follow/spec/lib/updater_spec.rb
Régis Hanol 5c0438f695
FIX: Allow users to unfollow suspended/staged/bot users (#176)
The follow button was completely hidden for suspended, staged, and bot
users. This meant that if a user followed someone who was later
suspended, they had no way to unfollow them without an admin
unsuspending and re-suspending the account.

The root cause was that `follow-button.gjs` unconditionally hid the
button based on the target user's state, with no distinction between
the follow and unfollow actions. On top of that, the `can_follow`
serializer field only checked `allow_people_to_follow_me` and not the
other states (suspended, staged, bot, self), so the client had to
duplicate those checks.

This commit introduces a `Guardian#can_follow?` method as the single
source of truth for follow permissions. The serializer delegates to it,
and the client simply shows the button when the user is already followed
OR can be followed. The `Updater` also uses the guardian as a first
check, then determines the specific error reason on failure to provide
meaningful error messages.

Other cleanups:
- Replace manual `raise if !current_user` checks with `requires_login`
- Fix mismatched i18n keys (`cannot_follow_*` → `user_cannot_follow_*`)
- Fix typo in locale ("suspend" → "suspended")

https://meta.discourse.org/t/304256
2026-03-31 11:36:41 +02:00

189 lines
8 KiB
Ruby

# frozen_string_literal: true
describe ::Follow::Updater do
before { SiteSetting.discourse_follow_enabled = true }
def new_updater(follower, target)
::Follow::Updater.new(follower, target)
end
fab!(:user1, :user)
fab!(:user2, :user)
fab!(:user3, :user)
it "does not allow following a bot user" do
expect { new_updater(user1, Discourse.system_user).watch_follow }.to raise_error(
Discourse::InvalidAccess,
) { |error| expect(error.custom_message).to eq("follow.user_cannot_follow_bot") }
end
it "does not allow following a staged user" do
user2.update!(staged: true)
expect { new_updater(user1, user2).watch_follow }.to raise_error(
Discourse::InvalidAccess,
) { |error| expect(error.custom_message).to eq("follow.user_cannot_follow_staged") }
end
it "does not allow following a suspended user" do
user2.update!(suspended_till: 10.hours.from_now)
expect { new_updater(user1, user2).watch_follow }.to raise_error(
Discourse::InvalidAccess,
) { |error| expect(error.custom_message).to eq("follow.user_cannot_follow_suspended") }
end
it "does not allow a user to follow themself" do
expect { new_updater(user1, user1).watch_follow }.to raise_error(
Discourse::InvalidAccess,
) { |error| expect(error.custom_message).to eq("follow.user_cannot_follow_themself") }
end
it "does not allow following a user who has disabled follows" do
user2.custom_fields["allow_people_to_follow_me"] = false
user2.save!
expect { new_updater(user1, user2).watch_follow }.to raise_error(
Discourse::InvalidAccess,
) do |error|
expect(error.custom_message).to eq("follow.user_does_not_allow_follow")
expect(error.custom_message_params).to eq({ username: user2.username })
end
end
it "does not allow following a user who has hidden their profile" do
user2.user_option.update!(hide_profile: true)
expect { new_updater(user1, user2).watch_follow }.to raise_error(
Discourse::InvalidAccess,
) do |error|
expect(error.custom_message).to eq("follow.user_does_not_allow_follow")
expect(error.custom_message_params).to eq({ username: user2.username })
end
end
it "works" do
new_updater(user1, user2).watch_follow
expect(user1.following.pluck(:id)).to contain_exactly(user2.id)
expect(user2.followers.pluck(:id)).to contain_exactly(user1.id)
relation = user1.following_relations.find_by(user_id: user2.id)
expect(relation.level).to eq(Follow::Notification.levels[:watching])
end
it "allows unfollowing a suspended user" do
new_updater(user1, user2).watch_follow
user2.update!(suspended_till: 10.hours.from_now)
new_updater(user1, user2).unfollow
expect(user1.reload.following.pluck(:id)).to eq([])
end
it "allows unfollowing a staged user" do
new_updater(user1, user2).watch_follow
user2.update!(staged: true)
new_updater(user1, user2).unfollow
expect(user1.reload.following.pluck(:id)).to eq([])
end
it "sends a notification to the followed user if they opt-in this type " \
"of notifications and follower allows this notification" do
user1.custom_fields[:notify_followed_user_when_followed] = true
user1.save!
user2.custom_fields[:notify_me_when_followed] = true
user2.save!
user2.notifications.destroy_all
new_updater(user1, user2).watch_follow
notification =
user2.reload.notifications.find_by(notification_type: Notification.types[:following])
expect(notification).to be_present
data = JSON.parse(notification.data)
expect(data["display_username"]).to eq(user1.username)
expect(user1.following.pluck(:id)).to contain_exactly(user2.id)
expect(user2.followers.pluck(:id)).to contain_exactly(user1.id)
relation = user1.following_relations.find_by(user_id: user2.id)
expect(relation.level).to eq(Follow::Notification.levels[:watching])
end
it "does not send a notification to the followed user if they opt-out of " \
"this type of notifications" do
user1.custom_fields[:notify_followed_user_when_followed] = true
user1.save!
user2.custom_fields[:notify_me_when_followed] = false
user2.save!
new_updater(user1, user2).watch_follow
expect(user2.reload.notifications.count).to eq(0)
expect(user1.following.pluck(:id)).to contain_exactly(user2.id)
expect(user2.followers.pluck(:id)).to contain_exactly(user1.id)
relation = user1.following_relations.find_by(user_id: user2.id)
expect(relation.level).to eq(Follow::Notification.levels[:watching])
end
it "does not send a notification to the followed user if the follower " \
"does not allow this notification to be sent out" do
user1.custom_fields[:notify_followed_user_when_followed] = false
user1.save!
user2.custom_fields[:notify_me_when_followed] = true
user2.save!
new_updater(user1, user2).watch_follow
expect(user2.reload.notifications.count).to eq(0)
expect(user1.following.pluck(:id)).to contain_exactly(user2.id)
expect(user2.followers.pluck(:id)).to contain_exactly(user1.id)
relation = user1.following_relations.find_by(user_id: user2.id)
expect(relation.level).to eq(Follow::Notification.levels[:watching])
end
it "does not send a notification to the followed user if the " \
"follow_notifications_enabled site setting is off" do
SiteSetting.follow_notifications_enabled = false
user1.custom_fields[:notify_followed_user_when_followed] = true
user1.save!
user2.custom_fields[:notify_me_when_followed] = true
user2.save!
new_updater(user1, user2).watch_follow
expect(user2.reload.notifications.count).to eq(0)
expect(user1.following.pluck(:id)).to contain_exactly(user2.id)
expect(user2.followers.pluck(:id)).to contain_exactly(user1.id)
relation = user1.following_relations.find_by(user_id: user2.id)
expect(relation.level).to eq(Follow::Notification.levels[:watching])
end
it "does not send a notification to the followed user if there is " \
"a follow notification from the same follower within the last 24 hours" do
user1.custom_fields[:notify_followed_user_when_followed] = true
user1.save!
user2.custom_fields[:notify_me_when_followed] = true
user2.save!
expect do new_updater(user1, user2).watch_follow end.to change {
user2.reload.notifications.where(notification_type: Notification.types[:following]).count
}.by(1)
expect(user1.following.pluck(:id)).to contain_exactly(user2.id)
expect(user2.followers.pluck(:id)).to contain_exactly(user1.id)
relation = user1.following_relations.find_by(user_id: user2.id)
expect(relation.level).to eq(Follow::Notification.levels[:watching])
freeze_time 6.hours.from_now
new_updater(user1, user2).unfollow
expect(user1.reload.following.pluck(:id)).to eq([])
expect(user2.reload.followers.pluck(:id)).to eq([])
expect(
user2.notifications.where(notification_type: Notification.types[:following]).count,
).to eq(1) # unchanged
# follow again
expect do new_updater(user1, user2).watch_follow end.not_to change {
user2.reload.notifications.where(notification_type: Notification.types[:following]).count
}
expect(user1.following.pluck(:id)).to contain_exactly(user2.id)
expect(user2.followers.pluck(:id)).to contain_exactly(user1.id)
relation = user1.following_relations.find_by(user_id: user2.id)
expect(relation.level).to eq(Follow::Notification.levels[:watching])
# following another user will result in a notification
user3.custom_fields[:notify_me_when_followed] = true
user3.save!
expect do new_updater(user1, user3).watch_follow end.to change {
user3.reload.notifications.where(notification_type: Notification.types[:following]).count
}.by(1)
expect(user1.following.pluck(:id)).to contain_exactly(user2.id, user3.id)
expect(user3.followers.pluck(:id)).to contain_exactly(user1.id)
relation = user1.following_relations.find_by(user_id: user3.id)
expect(relation.level).to eq(Follow::Notification.levels[:watching])
end
end