2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-03-03 23:54:20 +08:00
discourse/spec/lib/post_action_creator_spec.rb
Régis Hanol 867a59a1bd
FIX: Exclude notify_moderators PMs from personal message rate limit (#38021)
When a user flags a post with "Something Else", Discourse creates a
private message to moderators containing the flag details. This PM is
created with the flagger as the topic owner, which causes it to count
against the user's `max_personal_messages_per_day` rate limit.

This means users who have exhausted their daily PM quota cannot flag
posts with the "Something Else" reason, even though the flag itself has
its own independent rate limits (`max_flags_per_day` with trust level
multipliers). The error message ("You've reached the maximum messages
allowed per day") is also confusing in a flagging context.

This fix excludes topics with the `notify_moderators` subtype from the
PM per-day rate limit check in `Topic#limit_private_messages_per_day`.
Flag-generated PMs are system-initiated side effects, not user-initiated
messages, and should not count against the personal message quota.

https://meta.discourse.org/t/257002
2026-02-24 14:27:50 +01:00

497 lines
16 KiB
Ruby

# frozen_string_literal: true
RSpec.describe PostActionCreator do
fab!(:admin)
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
fab!(:post)
let(:like_type_id) { PostActionType.types[:like] }
describe "rate limits" do
before { RateLimiter.enable }
it "limits redo/undo" do
PostActionCreator.like(user, post)
PostActionDestroyer.destroy(user, post, :like)
PostActionCreator.like(user, post)
PostActionDestroyer.destroy(user, post, :like)
expect { PostActionCreator.like(user, post) }.to raise_error(RateLimiter::LimitExceeded)
end
it "does not count notify_moderators PM against personal message rate limit" do
SiteSetting.max_personal_messages_per_day = 1
SiteSetting.max_topics_per_day = 0
SiteSetting.max_topics_in_first_day = 0
SiteSetting.rate_limit_create_topic = 0
SiteSetting.rate_limit_create_post = 0
SiteSetting.rate_limit_new_user_create_post = 0
archetype = Archetype.private_message
create_post(user:, archetype:, target_usernames: [admin.username])
expect { create_post(user:, archetype:, target_usernames: [admin.username]) }.to raise_error(
RateLimiter::LimitExceeded,
)
reason = "This is a 'something else' flag."
result = PostActionCreator.notify_moderators(user, post, reason)
expect(result).to be_success
end
end
describe "messaging" do
it "doesn't generate title longer than 255 characters" do
topic =
Fabricate(
:topic,
title:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc sit amet rutrum neque. Pellentesque suscipit vehicula facilisis. Phasellus lacus sapien, aliquam nec convallis sit amet, vestibulum laoreet ante. Curabitur et pellentesque tortor. Donec non.",
)
post = Fabricate(:post, topic: topic)
expect(PostActionCreator.notify_user(user, post, "WAT")).to be_success
end
it "creates a pm to mods if selected" do
result = PostActionCreator.notify_moderators(user, post, "this is my special message")
expect(result).to be_success
post_action = result.post_action
expect(post_action.related_post).to be_present
expect(post_action.related_post.raw).to include("this is my special message")
end
it "sends an pm to user if selected" do
result = PostActionCreator.notify_user(user, post, "another special message")
expect(result).to be_success
post_action = result.post_action
expect(post_action.related_post).to be_present
expect(post_action.related_post.raw).to include("another special message")
end
end
describe "perform" do
it "creates a post action" do
result = PostActionCreator.new(user, post, like_type_id).perform
expect(result.success).to eq(true)
expect(result.post_action).to be_present
expect(result.post_action.user).to eq(user)
expect(result.post_action.post).to eq(post)
expect(result.post_action.post_action_type_id).to eq(like_type_id)
# also test double like
result = PostActionCreator.new(user, post, like_type_id).perform
expect(result.success).not_to eq(true)
expect(result.forbidden).to eq(true)
expect(result.errors.full_messages.join).to eq(I18n.t("action_already_performed"))
end
it "notifies subscribers" do
expect(post.reload.like_count).to eq(0)
messages =
MessageBus.track_publish { PostActionCreator.new(user, post, like_type_id).perform }
message = messages.find { |msg| msg.data[:type] === :liked }.data
expect(message).to be_present
expect(message[:type]).to eq(:liked)
expect(message[:likes_count]).to eq(1)
expect(message[:user_id]).to eq(user.id)
end
it "notifies updated topic stats to subscribers" do
topic = Fabricate(:topic)
post = Fabricate(:post, topic: topic)
expect(post.reload.like_count).to eq(0)
messages =
MessageBus.track_publish("/topic/#{topic.id}") do
PostActionCreator.new(user, post, like_type_id).perform
end
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
expect(stats_message).to be_present
expect(stats_message.data[:like_count]).to eq(1)
end
it "does not create an invalid post action" do
result = PostActionCreator.new(user, nil, like_type_id).perform
expect(result.failed?).to eq(true)
end
it "does not create a double like notification" do
PostActionNotifier.enable
post.user.user_option.update!(
like_notification_frequency: UserOption.like_notification_frequency_type[:always],
)
expect(PostActionCreator.new(user, post, like_type_id).perform.success).to eq(true)
expect(PostActionDestroyer.new(user, post, like_type_id).perform.success).to eq(true)
expect(PostActionCreator.new(user, post, like_type_id).perform.success).to eq(true)
notification = Notification.last
notification_data = JSON.parse(notification.data)
expect(notification_data["display_username"]).to eq(user.username)
expect(notification_data["username2"]).to eq(nil)
end
it "does not create a notification if silent mode is enabled" do
PostActionNotifier.enable
expect(PostActionCreator.new(user, post, like_type_id, silent: true).perform.success).to eq(
true,
)
expect(Notification.where(notification_type: Notification.types[:liked]).exists?).to eq(false)
end
it "triggers the right flag events" do
events = DiscourseEvent.track_events { PostActionCreator.create(user, post, :inappropriate) }
event_names = events.map { |event| event[:event_name] }
expect(event_names).to include(:flag_created)
expect(event_names).not_to include(:like_created)
end
it "triggers the right like events" do
events = DiscourseEvent.track_events { PostActionCreator.create(user, post, :like) }
event_names = events.map { |event| event[:event_name] }
expect(event_names).to include(:like_created)
expect(event_names).not_to include(:flag_created)
end
it "sends the right event arguments" do
events = DiscourseEvent.track_events { PostActionCreator.create(user, post, :like) }
event = events.find { |e| e[:event_name] == :like_created }
expect(event.present?).to eq(true)
expect(event[:params].first).to be_instance_of(PostAction)
expect(event[:params].second).to be_instance_of(PostActionCreator)
end
end
describe "flags" do
it "will create a reviewable if one does not exist" do
result = PostActionCreator.create(user, post, :inappropriate)
expect(result.success?).to eq(true)
reviewable = result.reviewable
expect(reviewable).to be_pending
expect(reviewable.created_by).to eq(user)
expect(reviewable.type).to eq("ReviewableFlaggedPost")
expect(reviewable.target_created_by_id).to eq(post.user_id)
expect(reviewable.reviewable_scores.count).to eq(1)
score = reviewable.reviewable_scores.find_by(user: user)
expect(score).to be_present
expect(score.reviewed_by).to be_blank
expect(score.reviewed_at).to be_blank
end
it "uses the system locale for the message when auto close" do
Topic.any_instance.stubs(:auto_close_threshold_reached?).returns(true)
I18n.with_locale(:fr) { PostActionCreator.create(user, post, :inappropriate) }
post.topic.reload
expect(post.topic.posts.last.raw).to eq(
I18n.t(
"temporarily_closed_due_to_flags",
count: SiteSetting.num_hours_to_close_topic,
locale: :en,
),
)
end
describe "Auto hide spam flagged posts" do
before do
user.trust_level = TrustLevel[3]
post.user.trust_level = TrustLevel[0]
SiteSetting.high_trust_flaggers_auto_hide_posts = true
end
it "hides the post when the flagger is a TL3 user and the poster is a TL0 user" do
PostActionCreator.create(user, post, :spam)
expect(post.hidden?).to eq(true)
end
it "does not hide the post if the setting is disabled" do
SiteSetting.high_trust_flaggers_auto_hide_posts = false
PostActionCreator.create(user, post, :spam)
expect(post.hidden?).to eq(false)
end
it "sets the force_review field" do
result = PostActionCreator.create(user, post, :spam)
reviewable = result.reviewable
expect(reviewable.force_review).to eq(true)
end
end
describe "non-human user being flagged" do
fab!(:system_post) { Fabricate(:post, user: Discourse.system_user) }
it "doesn't create reviewable" do
result = PostActionCreator.create(user, system_post, :inappropriate)
expect(result.success?).to eq(true)
expect(result.reviewable).to be_blank
end
it "applies modifier and can allow reviewable creation for non-human users" do
plugin = Plugin::Instance.new
modifier = :post_action_creator_block_reviewable_for_bot
proc = Proc.new { false }
DiscoursePluginRegistry.register_modifier(plugin, modifier, &proc)
result = PostActionCreator.create(user, system_post, :inappropriate)
expect(result.success?).to eq(true)
expect(result.reviewable).to be_present
ensure
DiscoursePluginRegistry.unregister_modifier(plugin, modifier, &proc)
end
end
context "with existing reviewable" do
let!(:reviewable) do
PostActionCreator.create(
Fabricate(:user, refresh_auto_groups: true),
post,
:inappropriate,
).reviewable
end
it "appends to an existing reviewable if exists" do
result = PostActionCreator.create(user, post, :inappropriate)
expect(result.success?).to eq(true)
expect(result.reviewable).to eq(reviewable)
expect(reviewable.reviewable_scores.count).to eq(2)
score = reviewable.reviewable_scores.find_by(user: user)
expect(score).to be_present
expect(score.reviewed_by).to be_blank
expect(score.reviewed_at).to be_blank
end
describe "When the post was already reviewed by staff" do
before { reviewable.perform(admin, :ignore_and_do_nothing) }
it "fails because the post was recently reviewed" do
freeze_time 10.seconds.from_now
result = PostActionCreator.create(user, post, :inappropriate)
expect(result.success?).to eq(false)
end
it "succeeds with other flag action types" do
freeze_time 10.seconds.from_now
_spam_result = PostActionCreator.create(user, post, :spam)
expect(reviewable.reload.pending?).to eq(true)
end
it "fails when other flag action types are open" do
freeze_time 10.seconds.from_now
_spam_result = PostActionCreator.create(user, post, :spam)
inappropriate_result = PostActionCreator.create(Fabricate(:user), post, :inappropriate)
reviewable.reload
expect(inappropriate_result.success?).to eq(false)
expect(reviewable.pending?).to eq(true)
expect(reviewable.reviewable_scores.select(&:pending?).count).to eq(1)
end
it "successfully flags the post if it was reviewed more than 24 hours ago" do
reviewable.update!(updated_at: 25.hours.ago)
post.last_version_at = 30.hours.ago
result = PostActionCreator.create(user, post, :inappropriate)
expect(result.success?).to eq(true)
expect(result.reviewable).to be_present
end
it "successfully flags the post if it was edited after being reviewed" do
reviewable.update!(updated_at: 10.minutes.ago)
post.last_version_at = 1.minute.ago
result = PostActionCreator.create(user, post, :inappropriate)
expect(result.success?).to eq(true)
expect(result.reviewable).to be_present
end
end
end
end
describe "take_action" do
it "will hide the post" do
PostActionCreator
.new(
Fabricate(:moderator, refresh_auto_groups: true),
post,
PostActionType.types[:spam],
take_action: true,
)
.perform
.reviewable
expect(post.reload).to be_hidden
end
context "when there is another reviewable on the post" do
before do
PostActionCreator.create(Fabricate(:user, refresh_auto_groups: true), post, :inappropriate)
end
it "will agree with the old reviewable" do
reviewable =
PostActionCreator
.new(
Fabricate(:moderator, refresh_auto_groups: true),
post,
PostActionType.types[:spam],
take_action: true,
)
.perform
.reviewable
expect(reviewable.reload).to be_approved
expect(reviewable.reviewable_scores).to all(be_agreed)
end
end
context "when hide_post_sensitivity is low" do
before { SiteSetting.hide_post_sensitivity = Reviewable.sensitivities[:low] }
it "still hides the post without considering the score" do
PostActionCreator
.new(
Fabricate(:moderator, refresh_auto_groups: true),
post,
PostActionType.types[:spam],
take_action: true,
)
.perform
.reviewable
expect(post.reload).to be_hidden
end
end
end
describe "queue_for_review" do
it "fails if the user is not a staff member" do
creator =
PostActionCreator.new(
user,
post,
PostActionType.types[:notify_moderators],
queue_for_review: true,
)
result = creator.perform
expect(result.success?).to eq(false)
end
it "creates a new reviewable and hides the post" do
result = build_creator.perform
expect(result.success?).to eq(true)
score = result.reviewable.reviewable_scores.last
expect(score.reason).to eq("queued_by_staff")
expect(post.reload).to be_hidden
end
it "hides the topic even if it has replies" do
Fabricate(:post, topic: post.topic)
_result = build_creator.perform
expect(post.topic.reload.visible?).to eq(false)
end
def build_creator
PostActionCreator.new(
admin,
post,
PostActionType.types[:notify_moderators],
queue_for_review: true,
)
end
end
describe "With plugin adding post_action_notify_user_handlers" do
let(:message) { "oh that was really bad what you said there" }
let(:plugin) { Plugin::Instance.new }
after { DiscoursePluginRegistry.reset! }
it "evaluates all handlers and creates post if none return false" do
plugin.register_post_action_notify_user_handler(
Proc.new do |user, post, message|
MessageBus.publish("notify_user", { user_id: user.id, message: message })
end,
)
plugin.register_post_action_notify_user_handler(
Proc.new do |user, post, message|
MessageBus.publish("notify_user", { poster_id: post.user_id, message: message })
end,
)
messages =
MessageBus.track_publish("notify_user") do
result =
PostActionCreator.new(
user,
post,
PostActionType.types[:notify_user],
message: message,
flag_topic: false,
).perform
post_action = result.post_action
expect(post_action.related_post).to be_present
end
expect(
messages.find { |m| m.data[:user_id] == user.id && m.data[:message] == message },
).to be_present
expect(
messages.find { |m| m.data[:poster_id] == post.user_id && m.data[:message] == message },
).to be_present
end
it "evaluates all handlers and doesn't create a post one returns false" do
plugin.register_post_action_notify_user_handler(
Proc.new do |user, post, message|
MessageBus.publish("notify_user", { user_id: user.id, message: message })
false
end,
)
messages =
MessageBus.track_publish("notify_user") do
result =
PostActionCreator.new(
user,
post,
PostActionType.types[:notify_user],
message: message,
flag_topic: false,
).perform
post_action = result.post_action
expect(post_action.related_post).not_to be_present
end
expect(
messages.find { |m| m.data[:user_id] == user.id && m.data[:message] == message },
).to be_present
end
end
end