0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 17:53:55 +08:00
discourse/spec/lib/post_destroyer_spec.rb
Régis Hanol c47c3eeba6
FIX: Enforce skip_review_media_groups when an edit adds media
The check only ran in `NewPostManager`, so it covered new posts and never
edits. A user could publish plain text, then edit an image in, and the post
would never reach the review queue. Inside `editing_grace_period` that edit
also creates no revision, so it left no trace in the UI either.

`PostRevisor` now runs the same check after a successful save, and queues a
`ReviewablePost` only when the edit introduces media the post did not already
have. Media is compared by its sources rather than its markup, so rescaling an
image or editing its alt text is not mistaken for new media, while replacing an
approved image with a different one is.

Undeleting your own post rewrites the original raw back in, which looks exactly
like adding media, so `PostDestroyer` now marks that revision as a recovery.

The `contains_media` reason also pointed moderators at a setting name that does
not exist, leaving them with a dead link from the review queue.
2026-08-04 17:01:44 +02:00

1579 lines
53 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe PostDestroyer do
before { UserActionManager.enable }
fab!(:moderator) { Fabricate(:moderator, refresh_auto_groups: true) }
fab!(:admin)
fab!(:coding_horror) { Fabricate(:coding_horror, refresh_auto_groups: true) }
let(:post) { create_post }
describe "destroy_old_hidden_posts" do
it "destroys posts that have been hidden for 30 days" do
now = Time.now
freeze_time(now - 60.days)
topic = post.topic
reply1 = create_post(topic: topic)
freeze_time(now - 40.days)
reply2 = create_post(topic: topic)
reply2.hide!(PostActionType.types[:off_topic])
freeze_time(now - 20.days)
reply3 = create_post(topic: topic)
reply3.hide!(PostActionType.types[:off_topic])
freeze_time(now - 10.days)
reply4 = create_post(topic: topic)
freeze_time(now)
PostDestroyer.destroy_old_hidden_posts
reply1.reload
reply2.reload
reply3.reload
reply4.reload
expect(reply1.deleted_at).to eq(nil)
expect(reply2.deleted_at).not_to eq(nil)
expect(reply3.deleted_at).to eq(nil)
expect(reply4.deleted_at).to eq(nil)
end
end
describe "destroy_old_stubs" do
it "destroys stubs for deleted by user topics" do
SiteSetting.delete_removed_posts_after = 24
PostDestroyer.new(post.user, post).destroy
post.update_column(:updated_at, 2.days.ago)
PostDestroyer.destroy_stubs
expect(post.reload.deleted_at).not_to eq(nil)
end
it "destroys stubs for deleted by user posts" do
SiteSetting.delete_removed_posts_after = 24
topic = post.topic
reply1 = create_post(topic: topic)
reply2 = create_post(topic: topic)
reply3 = create_post(topic: topic)
PostDestroyer.new(reply1.user, reply1).destroy
PostDestroyer.new(reply2.user, reply2).destroy
reply2.update_column(:updated_at, 2.days.ago)
PostDestroyer.destroy_stubs
reply1.reload
reply2.reload
reply3.reload
expect(reply1.deleted_at).to eq(nil)
expect(reply2.deleted_at).not_to eq(nil)
expect(reply3.deleted_at).to eq(nil)
# if topic is deleted we should still be able to destroy stubs
topic.trash!
reply1.update_column(:updated_at, 2.days.ago)
PostDestroyer.destroy_stubs
reply1.reload
expect(reply1.deleted_at).to eq(nil)
# flag the post, it should not nuke the stub anymore
topic.recover!
reviewable = PostActionCreator.spam(coding_horror, reply1).reviewable
PostDestroyer.destroy_stubs
reply1.reload
expect(reply1.deleted_at).to eq(nil)
# ignore the flag, we should be able to delete the stub
reviewable.perform(Discourse.system_user, :ignore_and_do_nothing)
PostDestroyer.destroy_stubs
reply1.reload
expect(reply1.deleted_at).to_not eq(nil)
end
it "uses the delete_removed_posts_after site setting" do
topic = post.topic
reply1 = create_post(topic: topic)
reply2 = create_post(topic: topic)
PostDestroyer.new(reply1.user, reply1).destroy
PostDestroyer.new(reply2.user, reply2).destroy
SiteSetting.delete_removed_posts_after = 1
reply2.update_column(:updated_at, 70.minutes.ago)
PostDestroyer.destroy_stubs
reply1.reload
reply2.reload
expect(reply1.deleted_at).to eq(nil)
expect(reply2.deleted_at).not_to eq(nil)
SiteSetting.delete_removed_posts_after = 72
reply1.update_column(:updated_at, 2.days.ago)
PostDestroyer.destroy_stubs
expect(reply1.reload.deleted_at).to eq(nil)
SiteSetting.delete_removed_posts_after = 47
PostDestroyer.destroy_stubs
expect(reply1.reload.deleted_at).not_to eq(nil)
end
it "deletes posts immediately if delete_removed_posts_after is 0" do
topic = post.topic
reply1 = create_post(topic: topic)
SiteSetting.delete_removed_posts_after = 0
PostDestroyer.new(reply1.user, reply1).destroy
expect(reply1.reload.deleted_at).not_to eq(nil)
end
end
describe "recovery and user actions" do
it "recreates user actions" do
reply = create_post(topic: post.topic)
author = reply.user
post_action =
author.user_actions.where(action_type: UserAction::REPLY, target_post_id: reply.id).first
expect(post_action).to be_present
PostDestroyer.new(moderator, reply).destroy
# User Action is removed
post_action =
author.user_actions.where(action_type: UserAction::REPLY, target_post_id: reply.id).first
expect(post_action).to be_blank
PostDestroyer.new(moderator, reply).recover
# On recovery, the user action is recreated
post_action =
author.user_actions.where(action_type: UserAction::REPLY, target_post_id: reply.id).first
expect(post_action).to be_present
end
it "creates a staff log entry when recovering the first post" do
first_post = create_post
PostDestroyer.new(moderator, first_post).destroy
expect { PostDestroyer.new(moderator, first_post).recover }.to change {
UserHistory.where(action: UserHistory.actions[:recover_topic]).count
}.by(1)
end
it "creates a staff log entry when recovering a reply" do
reply = create_post(topic: post.topic)
PostDestroyer.new(moderator, reply).destroy
expect { PostDestroyer.new(moderator, reply).recover }.to change {
UserHistory.where(action: UserHistory.actions[:recover_post]).count
}.by(1)
end
it "works with topics and posts with no user" do
post = Fabricate(:post)
UserDestroyer.new(Discourse.system_user).destroy(post.user, delete_posts: true)
expect { PostDestroyer.new(admin, post.reload).recover }.to change { post.reload.user_id }.to(
Discourse.system_user.id,
).and change { post.topic.user_id }.to(Discourse.system_user.id)
end
it "bypassed validation when updating users" do
post = create_post
# ensure user would fail validations
UserEmail.where(user_id: post.user_id).delete_all
PostDestroyer.new(admin, post.reload).destroy
PostDestroyer.new(admin, post.reload, force_destroy: true).destroy
expect(Post.with_deleted.find_by(id: post.id)).to eq(nil)
end
describe "post_count recovery" do
before do
post
@user = post.user
@reply = create_post(topic: post.topic, user: @user)
expect(@user.user_stat.post_count).to eq(1)
end
it "Recovers the post correctly" do
PostDestroyer.new(admin, post).destroy
post.reload
PostDestroyer.new(admin, post).recover
recovered_topic = post.reload.topic
expect(recovered_topic.deleted_at).to be_nil
expect(recovered_topic.deleted_by_id).to be_nil
end
context "with recover" do
it "doesn't raise an error when the raw doesn't change" do
PostRevisor.new(@reply).revise!(
@user,
{ edit_reason: "made a change" },
force_new_version: true,
)
PostDestroyer.new(@user, @reply.reload).recover
end
it "won't recover a non user-deleted post" do
PostRevisor.new(@reply).revise!(
admin,
{ raw: "this is a change to the post" },
force_new_version: true,
)
PostDestroyer.new(@user, @reply.reload).recover
expect(@reply.reload.raw).to eq("this is a change to the post")
end
it "should increment the user's post count" do
PostDestroyer.new(@user, @reply).destroy
expect(@user.user_stat.topic_count).to eq(1)
expect(@user.user_stat.post_count).to eq(1)
PostDestroyer.new(@user, @reply.reload).recover
expect(@user.user_stat.topic_count).to eq(1)
expect(@user.reload.user_stat.post_count).to eq(1)
expect(
UserAction.where(
target_topic_id: post.topic_id,
action_type: UserAction::NEW_TOPIC,
).count,
).to eq(1)
expect(
UserAction.where(target_topic_id: post.topic_id, action_type: UserAction::REPLY).count,
).to eq(1)
end
it "runs the SyncTopicUserBookmarked for the topic that the post is in so topic_users.bookmarked is correct" do
PostDestroyer.new(@user, @reply).destroy
expect_enqueued_with(
job: :sync_topic_user_bookmarked,
args: {
topic_id: @reply.topic_id,
},
) { PostDestroyer.new(@user, @reply.reload).recover }
end
end
context "when recovered by admin" do
it "should set user_deleted to false" do
PostDestroyer.new(@user, @reply).destroy
expect(@reply.reload.user_deleted).to eq(true)
PostDestroyer.new(admin, @reply).recover
expect(@reply.reload.user_deleted).to eq(false)
end
it "should increment the user's post count" do
PostDestroyer.new(moderator, @reply).destroy
expect(@user.reload.user_stat.topic_count).to eq(1)
expect(@user.user_stat.post_count).to eq(0)
PostDestroyer.new(admin, @reply).recover
expect(@user.reload.user_stat.topic_count).to eq(1)
expect(@user.user_stat.post_count).to eq(1)
PostDestroyer.new(moderator, post).destroy
expect(@user.reload.user_stat.topic_count).to eq(0)
expect(@user.user_stat.post_count).to eq(0)
PostDestroyer.new(admin, post).recover
expect(@user.reload.user_stat.topic_count).to eq(1)
expect(@user.user_stat.post_count).to eq(1)
expect(
UserAction.where(
target_topic_id: post.topic_id,
action_type: UserAction::NEW_TOPIC,
).count,
).to eq(1)
expect(
UserAction.where(target_topic_id: post.topic_id, action_type: UserAction::REPLY).count,
).to eq(1)
end
context "when recovered by user with access to moderate topic category" do
fab!(:review_user, :user)
before do
SiteSetting.enable_category_group_moderation = true
review_group = Fabricate(:group)
review_category = Fabricate(:category)
Fabricate(:category_moderation_group, category: review_category, group: review_group)
@reply.topic.update!(category: review_category)
review_group.users << review_user
end
context "when the post has a Reviewable record" do
before do
ReviewableFlaggedPost.needs_review!(target: @reply, created_by: Fabricate(:user))
end
def changes_deleted_at_to_nil
PostDestroyer.new(Discourse.system_user, @reply, context: "Automated testing").destroy
@reply.reload
expect(@reply.user_deleted).to eq(false)
expect(@reply.deleted_at).not_to eq(nil)
PostDestroyer.new(review_user, @reply).recover
@reply.reload
expect(@reply.deleted_at).to eq(nil)
end
it "changes deleted_at to nil" do
changes_deleted_at_to_nil
end
context "when the topic is deleted" do
before { @reply.topic.trash! }
it "changes deleted_at to nil" do
changes_deleted_at_to_nil
end
end
end
end
end
end
end
describe "recovery and post actions" do
fab!(:codinghorror) { coding_horror }
let!(:like) { PostActionCreator.like(codinghorror, post).post_action }
let!(:another_like) { PostActionCreator.like(moderator, post).post_action }
it "restores public post actions" do
PostDestroyer.new(moderator, post).destroy
expect(PostAction.exists?(id: like.id)).to eq(false)
PostDestroyer.new(moderator, post).recover
expect(PostAction.exists?(id: like.id)).to eq(true)
end
it "does not recover previously-deleted actions" do
PostActionDestroyer.destroy(codinghorror, post, :like)
expect(PostAction.exists?(id: like.id)).to eq(false)
PostDestroyer.new(moderator, post).destroy
PostDestroyer.new(moderator, post).recover
expect(PostAction.exists?(id: another_like.id)).to eq(true)
expect(PostAction.exists?(id: like.id)).to eq(false)
end
it "updates post like count" do
PostDestroyer.new(moderator, post).destroy
PostDestroyer.new(moderator, post).recover
post.reload
expect(post.like_count).to eq(2)
expect(post.custom_fields["deleted_public_actions"]).to be_nil
end
it "restores PostReply when the reply is recovered" do
reply =
create_post(topic: post.topic, user: codinghorror, reply_to_post_number: post.post_number)
expect(post.post_replies.count).to eq(1)
PostDestroyer.new(moderator, reply).destroy
expect(post.post_replies.count).to eq(0)
PostDestroyer.new(moderator, reply.reload).recover
expect(post.post_replies.reload.count).to eq(1)
end
it "restores reply_count when the reply is recovered" do
reply =
create_post(topic: post.topic, user: codinghorror, reply_to_post_number: post.post_number)
expect(post.reload.reply_count).to eq(1)
PostDestroyer.new(moderator, reply).destroy
expect(post.reload.reply_count).to eq(0)
PostDestroyer.new(moderator, reply.reload).recover
expect(post.reload.reply_count).to eq(1)
end
it "restores PostReply for quoted posts when recovered" do
reply =
create_post(
topic: post.topic,
user: codinghorror,
raw:
"[quote=\"#{post.user.username}, post:#{post.post_number}, topic:#{post.topic_id}\"]\nquoted\n[/quote]\nmy reply",
)
expect(post.post_replies.count).to eq(1)
expect(reply.reply_to_post_number).to be_nil
PostDestroyer.new(moderator, reply).destroy
expect(post.post_replies.count).to eq(0)
PostDestroyer.new(moderator, reply.reload).recover
expect(post.post_replies.reload.count).to eq(1)
end
end
describe "basic destroying" do
it "as the creator of the post, doesn't delete the post" do
post2 = create_post
user_stat = post2.user.user_stat
called = 0
topic_destroyed = ->(topic, user) do
expect(topic).to eq(post2.topic)
expect(user).to eq(post2.user)
called += 1
end
DiscourseEvent.on(:topic_destroyed, &topic_destroyed)
@orig = post2.cooked
# Guardian.new(post2.user).can_delete_post?(post2) == false
PostDestroyer.new(post2.user, post2).destroy
post2.reload
expect(post2.deleted_at).to be_blank
expect(post2.deleted_by).to be_blank
expect(post2.user_deleted).to eq(true)
expect(post2.raw).to eq(I18n.t("js.topic.deleted_by_author_simple"))
expect(post2.version).to eq(2)
expect(called).to eq(1)
expect(user_stat.reload.post_count).to eq(0)
expect(user_stat.reload.topic_count).to eq(1)
called = 0
topic_recovered = ->(topic, user) do
expect(topic).to eq(post2.topic)
expect(user).to eq(post2.user)
called += 1
end
DiscourseEvent.on(:topic_recovered, &topic_recovered)
# lets try to recover
PostDestroyer.new(post2.user, post2).recover
post2.reload
expect(post2.version).to eq(3)
expect(post2.user_deleted).to eq(false)
expect(post2.cooked).to eq(@orig)
expect(called).to eq(1)
expect(user_stat.reload.post_count).to eq(0)
expect(user_stat.reload.topic_count).to eq(1)
ensure
DiscourseEvent.off(:topic_destroyed, &topic_destroyed)
DiscourseEvent.off(:topic_recovered, &topic_recovered)
end
it "maintains history when a user destroys a hidden post" do
post.hide!(PostActionType.types[:inappropriate])
PostDestroyer.new(post.user, post).destroy
expect(post.revisions[0].modifications["raw"]).to be_present
end
it "resolves reviewable when author deletes their flagged post and undeletes" do
reply = create_post(topic: post.topic)
result = PostActionCreator.spam(coding_horror, reply)
reviewable = result.reviewable
expect(reviewable).to be_pending
PostDestroyer.new(reply.user, reply).destroy
expect(reply.reload.user_deleted).to eq(true)
expect(reviewable.reload).to be_ignored
expect(reviewable.reviewable_scores.first.reviewed_by_id).to eq(Discourse.system_user.id)
expect(reviewable.reviewable_scores.first.reviewed_at).to be_present
history = reviewable.reviewable_histories.last
expect(history.reviewable_history_type).to eq("transitioned")
expect(history.created_by).to eq(Discourse.system_user)
PostDestroyer.new(reply.user, reply).recover
expect(reply.reload.user_deleted).to eq(false)
expect(reviewable.reload).to be_pending
recovery_note = reviewable.reviewable_notes.last
expect(recovery_note.user).to eq(Discourse.system_user)
expect(recovery_note.content).to eq(I18n.t("reviewables.post_restored_by_author"))
history = reviewable.reviewable_histories.last
expect(history.reviewable_history_type).to eq("transitioned")
expect(history.created_by).to eq(Discourse.system_user)
end
it "does not queue the restored post for media review" do
SiteSetting.skip_review_media_groups = Group::AUTO_GROUPS[:trust_level_3]
reply = create_post(topic: post.topic, raw: "look: ![image](upload://sherlock.jpeg)")
PostDestroyer.new(reply.user, reply).destroy
expect { PostDestroyer.new(reply.user, reply.reload).recover }.not_to change(
ReviewablePost,
:count,
)
expect(reply.reload.raw).to include("upload://sherlock.jpeg")
end
it "does not restore reviewable when manually ignored by moderator" do
reply = create_post(topic: post.topic)
result = PostActionCreator.spam(coding_horror, reply)
reviewable = result.reviewable
expect(reviewable).to be_present
expect(reviewable).to be_pending
reviewable.perform(moderator, :ignore_and_do_nothing)
expect(reviewable.reload).to be_ignored
expect(reviewable.reviewable_scores.first.reviewed_by_id).to eq(moderator.id)
PostDestroyer.new(reply.user, reply).destroy
expect(reply.reload.user_deleted).to eq(true)
PostDestroyer.new(reply.user, reply).recover
expect(reply.reload.user_deleted).to eq(false)
expect(reviewable.reload).to be_ignored
end
it "resolves reviewable when author deletes their post via perform_delete (delete_removed_posts_after = 0)" do
SiteSetting.delete_removed_posts_after = 0
reply = create_post(topic: post.topic)
reviewable =
ReviewablePost.needs_review!(
target: reply,
created_by: Discourse.system_user,
reviewable_by_moderator: true,
)
expect(reviewable).to be_pending
PostDestroyer.new(reply.user, reply).destroy
expect(reply.reload.deleted_at).to be_present
expect(reviewable.reload).to be_ignored
end
it "does not auto-ignore reviewable when author was silenced for the post" do
reply = create_post(topic: post.topic)
reviewable = PostActionCreator.spam(coding_horror, reply).reviewable
UserSilencer.silence(reply.user, moderator, post_id: reply.id)
PostDestroyer.new(reply.user, reply).destroy
expect(reply.reload.user_deleted).to eq(true)
expect(reviewable.reload).to be_pending
expect(reviewable.reviewable_notes.last.content).to eq(
I18n.t("reviewables.post_deleted_by_author_after_penalty"),
)
PostDestroyer.new(reply.user, reply).recover
expect(reply.reload.user_deleted).to eq(false)
expect(reviewable.reload).to be_pending
end
it "does not auto-ignore reviewable when author was suspended for the post" do
reply = create_post(topic: post.topic)
reviewable = PostActionCreator.spam(coding_horror, reply).reviewable
UserSuspender.new(
reply.user,
suspended_till: 5.days.from_now,
reason: "spam",
by_user: moderator,
post_id: reply.id,
).suspend
PostDestroyer.new(reply.user, reply).destroy
expect(reply.reload.user_deleted).to eq(true)
expect(reviewable.reload).to be_pending
PostDestroyer.new(reply.user, reply).recover
expect(reply.reload.user_deleted).to eq(false)
expect(reviewable.reload).to be_pending
end
it "auto-ignores reviewable when author was silenced but is no longer silenced" do
reply = create_post(topic: post.topic)
reviewable = PostActionCreator.spam(coding_horror, reply).reviewable
UserSilencer.silence(reply.user, moderator, post_id: reply.id)
UserSilencer.unsilence(reply.user, moderator)
PostDestroyer.new(reply.user, reply).destroy
expect(reply.reload.user_deleted).to eq(true)
expect(reviewable.reload).to be_ignored
end
it "auto-ignores reviewable when author was suspended but is no longer suspended" do
reply = create_post(topic: post.topic)
reviewable = PostActionCreator.spam(coding_horror, reply).reviewable
UserSuspender.new(
reply.user,
suspended_till: 5.days.from_now,
reason: "spam",
by_user: moderator,
post_id: reply.id,
).suspend
reply.user.update!(suspended_till: nil, suspended_at: nil)
PostDestroyer.new(reply.user, reply).destroy
expect(reply.reload.user_deleted).to eq(true)
expect(reviewable.reload).to be_ignored
end
it "when topic is destroyed, it updates user_stats correctly" do
SiteSetting.min_topic_title_length = 5
post.topic.update_column(:title, "xyz")
user1 = post.user
user2 = Fabricate(:user)
reply = create_post(topic_id: post.topic_id, user: user2)
reply2 = create_post(topic_id: post.topic_id, user: user1)
expect(user1.user_stat.topic_count).to eq(1)
expect(user1.user_stat.post_count).to eq(1)
expect(user2.user_stat.topic_count).to eq(0)
expect(user2.user_stat.post_count).to eq(1)
PostDestroyer.new(admin, post).destroy
user1.reload
user2.reload
expect(user1.user_stat.topic_count).to eq(0)
expect(user1.user_stat.post_count).to eq(0)
expect(user2.user_stat.topic_count).to eq(0)
expect(user2.user_stat.post_count).to eq(0)
end
it "does not update post_count or topic_count to a negative number" do
user1 = post.user
reply2 = create_post(topic_id: post.topic_id, user: user1)
expect(user1.user_stat.topic_count).to eq(1)
expect(user1.user_stat.post_count).to eq(1)
user1.user_stat.update!(topic_count: 0)
user1.user_stat.update!(post_count: 0)
PostDestroyer.new(admin, post).destroy
user1.reload
expect(user1.user_stat.topic_count).to eq(0)
expect(user1.user_stat.post_count).to eq(0)
end
it "deletes the published page associated with the topic" do
slug = "my-published-page"
publish_result = PublishedPage.publish!(admin, post.topic, slug)
pp = publish_result.last
expect(publish_result.first).to eq(true)
PostDestroyer.new(admin, post).destroy
expect(PublishedPage.find_by(id: pp.id)).to be_nil
end
it "accepts a delete_removed_posts_after option" do
SiteSetting.delete_removed_posts_after = 0
post.update!(post_number: 2)
PostDestroyer.new(post.user, post, delete_removed_posts_after: 1).destroy
post.reload
expect(post.deleted_at).to eq(nil)
expect(post.user_deleted).to eq(true)
expect(post.raw).to eq(I18n.t("js.post.deleted_by_author_simple"))
end
it "runs the SyncTopicUserBookmarked for the topic that the post is in so topic_users.bookmarked is correct" do
post2 = create_post
PostDestroyer.new(post2.user, post2).destroy
expect_job_enqueued(job: :sync_topic_user_bookmarked, args: { topic_id: post2.topic_id })
end
it "skips post revise validations when post is marked for deletion by the author" do
SiteSetting.min_first_post_length = 100
post =
create_post(
raw: "this is a long post what passes the min_first_post_length validation " * 3,
)
PostDestroyer.new(post.user, post).destroy
post.reload
expect(post.errors).to be_blank
expect(post.revisions.count).to eq(1)
expect(post.raw).to eq(I18n.t("js.topic.deleted_by_author_simple"))
expect(post.user_deleted).to eq(true)
expect(post.topic.closed).to eq(true)
end
context "as a moderator" do
it "deletes the post" do
author = post.user
reply = create_post(topic_id: post.topic_id, user: author)
post_count = author.post_count
history_count = UserHistory.count
PostDestroyer.new(moderator, reply).destroy
expect(reply.deleted_at).to be_present
expect(reply.deleted_by).to eq(moderator)
author.reload
expect(author.post_count).to eq(post_count - 1)
expect(UserHistory.count).to eq(history_count + 1)
end
it "links the staff action log to the reviewable when passed via opts" do
reply = create_post(topic_id: post.topic_id, user: post.user)
reviewable = Fabricate(:reviewable_flagged_post, target: reply)
expect {
PostDestroyer.new(moderator, reply, reviewable_id: reviewable.id).destroy
}.to change {
UserHistory.where(
action: UserHistory.actions[:delete_post],
reviewable_id: reviewable.id,
).count
}.by(1)
end
end
context "when deleted by user with access to moderate topic category" do
fab!(:review_user, :user)
before do
SiteSetting.enable_category_group_moderation = true
review_group = Fabricate(:group)
review_category = Fabricate(:category)
Fabricate(:category_moderation_group, category: review_category, group: review_group)
post.topic.update!(category: review_category)
review_group.users << review_user
end
context "when the post has a reviewable" do
it "deletes the post" do
author = post.user
reply = create_post(topic_id: post.topic_id, user: author)
ReviewableFlaggedPost.needs_review!(target: reply, created_by: Fabricate(:user))
post_count = author.post_count
history_count = UserHistory.count
PostDestroyer.new(review_user, reply).destroy
expect(reply.deleted_at).to be_present
expect(reply.deleted_by).to eq(review_user)
author.reload
expect(author.post_count).to eq(post_count - 1)
expect(UserHistory.count).to eq(history_count + 1)
end
end
end
context "as an admin" do
it "deletes the post" do
PostDestroyer.new(admin, post).destroy
expect(post.deleted_at).to be_present
expect(post.deleted_by).to eq(admin)
end
it "updates the user's topic_count for first post" do
author = post.user
expect {
PostDestroyer.new(admin, post).destroy
author.reload
}.to change { author.topic_count }.by(-1)
expect(author.user_stat.post_count).to eq(0)
end
it "updates the user's post_count for reply" do
author = post.user
reply = create_post(topic: post.topic, user: author)
expect {
PostDestroyer.new(admin, reply).destroy
author.reload
}.to change { author.post_count }.by(-1)
expect(author.user_stat.topic_count).to eq(1)
end
it "doesn't count whispers" do
user_stat = admin.user_stat
whisper =
PostCreator.new(
admin,
topic_id: post.topic.id,
reply_to_post_number: 1,
post_type: Post.types[:whisper],
raw: "this is a whispered reply",
).create
expect(user_stat.reload.post_count).to eq(0)
expect { PostDestroyer.new(admin, whisper).destroy }.to_not change {
user_stat.reload.post_count
}
end
end
end
describe "private message" do
fab!(:author, :user)
fab!(:private_message) { Fabricate(:private_message_topic, user: author) }
fab!(:first_post) { Fabricate(:post, topic: private_message, user: author) }
fab!(:second_post) { Fabricate(:post, topic: private_message, user: author, post_number: 2) }
it "doesn't update post_count for a reply" do
expect {
PostDestroyer.new(admin, second_post).destroy
author.reload
}.to_not change { author.post_count }
expect { PostDestroyer.new(admin, second_post).recover }.to_not change { author.post_count }
end
it "doesn't update topic_count for first post" do
expect {
PostDestroyer.new(admin, first_post).destroy
author.reload
}.to_not change { author.topic_count }
expect(author.post_count).to eq(0) # also unchanged
end
it "triggers the extensibility events" do
events = DiscourseEvent.track_events { PostDestroyer.new(admin, first_post).destroy }.last(2)
expect(events[0][:event_name]).to eq(:post_destroyed)
expect(events[0][:params].first).to eq(first_post)
expect(events[1][:event_name]).to eq(:topic_destroyed)
expect(events[1][:params].first).to eq(first_post.topic)
end
it "should not log a personal message view" do
SiteSetting.log_personal_messages_views = true
Fabricate(:topic_web_hook)
StaffActionLogger.any_instance.expects(:log_check_personal_message).never
PostDestroyer.new(admin, first_post).destroy
end
end
describe "deleting a post directly after a whisper" do
before { SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}" }
it "should not set Topic#last_post_user_id to a whisperer" do
post_1 = create_post(topic: post.topic, user: moderator)
create_post(topic: post.topic, user: Fabricate(:user), post_type: Post.types[:whisper])
whisper_2 =
create_post(topic: post.topic, user: Fabricate(:user), post_type: Post.types[:whisper])
PostDestroyer.new(admin, whisper_2).destroy
expect(post.topic.reload.last_post_user_id).to eq(post_1.user.id)
end
end
describe "deleting the second post in a topic" do
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
let!(:post) { create_post(user: user) }
let(:topic) { post.topic }
fab!(:second_user) { coding_horror }
let!(:second_post) { create_post(topic: topic, user: second_user) }
before do
PostDestroyer.new(moderator, second_post).destroy
topic.reload
end
it "resets the last_poster_id back to the OP" do
expect(topic.last_post_user_id).to eq(user.id)
end
it "resets the last_posted_at back to the OP" do
expect(topic.last_posted_at.to_i).to eq(post.created_at.to_i)
end
it "resets the highest_post_number" do
expect(topic.highest_post_number).to eq(post.post_number)
end
context "with topic_user" do
let(:topic_user) { second_user.topic_users.find_by(topic_id: topic.id) }
it "clears the posted flag for the second user" do
expect(topic_user.posted?).to eq(false)
end
it "sets the second user's last_read_post_number back to 1" do
expect(topic_user.last_read_post_number).to eq(1)
end
end
end
describe "deleting a post belonging to a deleted topic" do
let!(:topic) { post.topic }
let(:author) { post.user }
before do
topic.trash!(admin)
post.reload
end
context "as a moderator" do
before { PostDestroyer.new(moderator, post).destroy }
it "deletes the post" do
expect(post.deleted_at).to be_present
expect(post.deleted_by).to eq(moderator)
expect(author.user_stat.post_count).to eq(0)
end
end
context "as an admin" do
subject(:destroyer) { PostDestroyer.new(admin, post).destroy }
it "deletes the post" do
destroyer
expect(post.deleted_at).to be_present
expect(post.deleted_by).to eq(admin)
end
it "creates a new user history entry" do
expect { destroyer }.to change { UserHistory.count }.by(1)
end
it "triggers a extensibility event" do
events = DiscourseEvent.track_events { destroyer }
expect(events[0][:event_name]).to eq(:post_destroyed)
expect(events[0][:params].first).to eq(post)
end
end
end
describe "deleting a reply belonging to a deleted topic" do
let!(:topic) { post.topic }
let!(:reply) { create_post(topic_id: topic.id, user: post.user) }
let(:author) { reply.user }
before do
topic.trash!(admin)
post.reload
reply.reload
end
context "as a moderator" do
subject(:destroyer) { PostDestroyer.new(moderator, reply).destroy }
it "deletes the reply" do
destroyer
expect(reply.deleted_at).to be_present
expect(reply.deleted_by).to eq(moderator)
end
it "doesn't decrement post_count again" do
expect { destroyer }.to_not change { author.user_stat.post_count }
end
end
context "as an admin" do
subject(:destroyer) { PostDestroyer.new(admin, reply).destroy }
it "deletes the post" do
destroyer
expect(reply.deleted_at).to be_present
expect(reply.deleted_by).to eq(admin)
end
it "doesn't decrement post_count again" do
expect { destroyer }.to_not change { author.user_stat.post_count }
end
it "creates a new user history entry" do
expect { destroyer }.to change { UserHistory.count }.by(1)
end
end
end
it "deletes a post belonging to a non-existent topic" do
DB.exec("DELETE FROM topics WHERE id = ?", post.topic_id)
post.reload
PostDestroyer.new(admin, post).destroy
expect(post.deleted_at).to be_present
expect(post.deleted_by).to eq(admin)
end
describe "after delete" do
fab!(:coding_horror) { coding_horror }
fab!(:post) { Fabricate(:post, raw: "Hello @CodingHorror") }
it "should feature the users again (in case they've changed)" do
expect_enqueued_with(job: :feature_topic_users, args: { topic_id: post.topic_id }) do
PostDestroyer.new(moderator, post).destroy
end
end
context "with a reply" do
fab!(:reply) { Fabricate(:basic_reply, user: coding_horror, topic: post.topic) }
let!(:post_reply) { PostReply.create(post_id: post.id, reply_post_id: reply.id) }
it "changes the post count of the topic" do
post.reload
expect {
PostDestroyer.new(moderator, reply).destroy
post.topic.reload
}.to change(post.topic, :posts_count).by(-1)
end
it "lowers the reply_count when the reply is deleted" do
expect { PostDestroyer.new(moderator, reply).destroy }.to change(
post.post_replies,
:count,
).by(-1)
end
it "should increase the post_number when there are deletion gaps" do
PostDestroyer.new(moderator, reply).destroy
p = Fabricate(:post, user: post.user, topic: post.topic)
expect(p.post_number).to eq(3)
end
end
end
describe "@mentions" do
it "removes notifications when deleted" do
Jobs.run_immediately!
user = Fabricate(:evil_trout)
post = create_post(raw: "Hello @eviltrout")
expect { PostDestroyer.new(moderator, post).destroy }.to change(
user.notifications,
:count,
).by(-1)
end
end
describe "post actions" do
let(:second_post) { Fabricate(:post, topic_id: post.topic_id) }
let(:flag_result) { PostActionCreator.off_topic(moderator, second_post) }
let!(:flag) { flag_result.post_action }
before { Jobs::SendSystemMessage.clear }
it "should delete public post actions and agree with flags" do
url = second_post.url
PostDestroyer.new(moderator, second_post).destroy
off_topic = PostAction.find_by(id: flag.id)
expect(off_topic).not_to eq(nil)
expect(off_topic.agreed_at).not_to eq(nil)
second_post.reload
expect(second_post.off_topic_count).to eq(1)
jobs = Jobs::SendSystemMessage.jobs
expect(jobs.size).to eq(1)
Jobs::SendSystemMessage.new.execute(jobs[0]["args"][0].with_indifferent_access)
expect(Post.last.raw).to eq(
I18n.t(
"system_messages.flags_agreed_and_post_deleted.text_body_template",
flagged_post_raw_content: second_post.raw,
url: url,
flag_reason:
I18n.t(
"flag_reasons.#{PostActionType.flag_types[off_topic.post_action_type_id]}",
locale: SiteSetting.default_locale,
base_path: Discourse.base_path,
),
site_name: SiteSetting.title,
base_url: Discourse.base_url,
).strip,
)
end
it "should not send the flags_agreed_and_post_deleted message if it was deleted by system" do
expect(ReviewableFlaggedPost.pending.count).to eq(1)
PostDestroyer.new(Discourse.system_user, second_post, context: "Automated testing").destroy
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
expect(ReviewableFlaggedPost.pending.count).to eq(0)
end
it "should not send the flags_agreed_and_post_deleted message if it was deleted by author" do
SiteSetting.delete_removed_posts_after = 0
expect(ReviewableFlaggedPost.pending.count).to eq(1)
PostDestroyer.new(second_post.user, second_post).destroy
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
expect(ReviewableFlaggedPost.pending.count).to eq(0)
end
it "should not send the flags_agreed_and_post_deleted message if flags were ignored" do
expect(ReviewableFlaggedPost.pending.count).to eq(1)
flag_result.reviewable.perform(moderator, :ignore_and_do_nothing)
second_post.reload
expect(ReviewableFlaggedPost.pending.count).to eq(0)
PostDestroyer.new(moderator, second_post).destroy
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
end
it "should not send the flags_agreed_and_post_deleted message if defer_flags is true" do
expect(ReviewableFlaggedPost.pending.count).to eq(1)
PostDestroyer.new(moderator, second_post, defer_flags: true).destroy
expect(Jobs::SendSystemMessage.jobs.size).to eq(0)
expect(ReviewableFlaggedPost.pending.count).to eq(0)
end
context "when the flagged post is potentially illegal" do
before { ReviewableFlaggedPost.pending.update_all(potentially_illegal: true) }
it "does not automatically mark it as ignored or approved" do
expect { PostDestroyer.new(moderator, second_post).destroy }.not_to change {
ReviewableFlaggedPost.pending.count
}
expect(Jobs::SendSystemMessage.jobs).to be_empty
end
context "when deleting a reply with a reviewable option and notify_users_after_responses_deleted_on_flagged_post enabled" do
let(:parent_reviewable) { flag_result.reviewable }
let(:reply_reviewable) { second_post.reviewable_flag }
before { SiteSetting.notify_users_after_responses_deleted_on_flagged_post = true }
it "does not ignore a potentially illegal flag on the reply" do
expect {
PostDestroyer.new(moderator, second_post, reviewable_id: parent_reviewable.id).destroy
}.not_to change { reply_reviewable.reload.pending? }
end
end
end
context "when custom flags" do
fab!(:custom_flag) { Fabricate(:flag, name: "custom flag", notify_type: true) }
let(:third_post) { Fabricate(:post, topic_id: post.topic_id) }
it "should send message to user with correct translation" do
PostActionCreator.new(
moderator,
third_post,
custom_flag.id,
is_warning: false,
flag_topic: true,
).perform
PostDestroyer.new(moderator, third_post, { reviewable_id: Reviewable.last.id }).destroy
jobs = Jobs::SendSystemMessage.jobs
expect(jobs.size).to eq(1)
Jobs::SendSystemMessage.new.execute(jobs[0]["args"][0].with_indifferent_access)
expect(Post.last.raw).to match("custom flag")
custom_flag.destroy!
end
end
end
describe "user actions" do
let(:codinghorror) { coding_horror }
let(:second_post) { Fabricate(:post, topic_id: post.topic_id) }
def create_user_action(action_type)
UserAction.log_action!(
action_type: action_type,
user_id: codinghorror.id,
acting_user_id: codinghorror.id,
target_topic_id: second_post.topic_id,
target_post_id: second_post.id,
)
end
it "should delete the user actions" do
like = create_user_action(UserAction::LIKE)
PostDestroyer.new(moderator, second_post).destroy
expect(UserAction.find_by(id: like.id)).to be_nil
end
end
describe "topic links" do
fab!(:first_post, :post)
let!(:topic) { first_post.topic }
let!(:second_post) { Fabricate(:post_with_external_links, topic: topic) }
before { TopicLink.extract_from(second_post) }
it "should destroy the topic links when moderator destroys the post" do
PostDestroyer.new(moderator, second_post.reload).destroy
expect(topic.topic_links.count).to eq(0)
end
it "should destroy the topic links when the user destroys the post" do
PostDestroyer.new(second_post.user, second_post.reload).destroy
expect(topic.topic_links.count).to eq(0)
end
end
describe "internal links" do
fab!(:topic)
let!(:second_post) { Fabricate(:post, topic: topic) }
fab!(:other_topic, :topic)
let!(:other_post) { Fabricate(:post, topic: other_topic) }
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
let!(:base_url) { URI.parse(Discourse.base_url) }
let!(:guardian) { Guardian.new }
let!(:url) do
"http://#{base_url.host}/t/#{other_topic.slug}/#{other_topic.id}/#{other_post.post_number}"
end
it "should destroy internal links when user deletes own post" do
new_post = Post.create!(user: user, topic: topic, raw: "Link to other topic:\n\n#{url}\n")
TopicLink.extract_from(new_post)
link_counts = TopicLink.counts_for(guardian, other_topic.reload, [other_post])
expect(link_counts.count).to eq(1)
PostDestroyer.new(user, new_post).destroy
updated_link_counts = TopicLink.counts_for(guardian, other_topic.reload, [other_post])
expect(updated_link_counts.count).to eq(0)
end
it "should destroy internal links when moderator deletes post" do
new_post = create_post(user: user, topic: topic, raw: "Link to other topic:\n\n#{url}\n")
TopicLink.extract_from(new_post)
link_counts = TopicLink.counts_for(guardian, other_topic.reload, [other_post])
expect(link_counts.count).to eq(1)
PostDestroyer.new(moderator, new_post).destroy
TopicLink.extract_from(new_post)
updated_link_counts = TopicLink.counts_for(guardian, other_topic, [other_post])
expect(updated_link_counts.count).to eq(0)
end
end
describe ".delete_with_replies" do
subject(:delete_with_replies) do
PostDestroyer.delete_with_replies(reporter, post, defer_reply_flags: defer_reply_flags)
end
fab!(:post)
let(:reporter) { Fabricate(:moderator) }
let(:reply) { Fabricate(:post, topic: post.topic) }
let(:reviewable_reply) { PostActionCreator.off_topic(reporter, reply).reviewable }
before do
post.update(replies: [reply])
PostActionCreator.off_topic(reporter, post)
reviewable_reply
end
context "when deferring reply flags" do
let(:defer_reply_flags) { true }
it "ignores flagged replies" do
delete_with_replies
expect(reviewable_reply.reload).to be_ignored
end
end
context "when not deferring reply flags" do
let(:defer_reply_flags) { false }
it "approves flagged replies" do
delete_with_replies
expect(reviewable_reply.reload).to be_approved
end
end
end
describe "featured topics for user_profiles" do
fab!(:user)
it "clears the user_profiles featured_topic column" do
user.user_profile.update(featured_topic: post.topic)
PostDestroyer.new(admin, post).destroy
expect(user.user_profile.reload.featured_topic).to eq(nil)
end
end
describe "permanent destroy" do
fab!(:private_message_topic)
fab!(:private_post) { Fabricate(:private_message_post, topic: private_message_topic) }
fab!(:post_action) { Fabricate(:post_action, post: private_post) }
fab!(:reply) { Fabricate(:private_message_post, topic: private_message_topic) }
fab!(:post_revision) { Fabricate(:post_revision, post: private_post) }
fab!(:upload1) { Fabricate(:upload_s3, created_at: 5.hours.ago) }
fab!(:upload_reference) { UploadReference.create(target: private_post, upload: upload1) }
it "destroys the post and topic if deleting first post" do
PostDestroyer.new(reply.user, reply, permanent: true).destroy
expect { reply.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect(private_message_topic.reload.persisted?).to be true
PostDestroyer.new(private_post.user, private_post, permanent: true).destroy
expect { private_post.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { private_message_topic.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { post_action.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { post_revision.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { upload_reference.reload }.to raise_error(ActiveRecord::RecordNotFound)
Jobs::CleanUpUploads.new.reset_last_cleanup!
SiteSetting.clean_orphan_uploads_grace_period_hours = 1
Jobs::CleanUpUploads.new.execute({})
expect { upload1.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it "soft delete if not creator of post or not private message" do
PostDestroyer.new(moderator, reply, permanent: true).destroy
expect(reply.deleted_at).not_to eq(nil)
PostDestroyer.new(post.user, post, permanent: true).destroy
expect(post.user_deleted).to be true
expect(post_revision.reload.persisted?).to be true
end
it "destroys the post when force_destroy is true for soft deleted topics" do
Fabricate(:topic_web_hook)
post = Fabricate(:post)
topic = post.topic
PostDestroyer.new(moderator, post).destroy
post = Post.with_deleted.find_by(id: post.id)
expect(post).not_to eq(nil)
PostDestroyer.new(moderator, post, force_destroy: true).destroy
post = Post.with_deleted.find_by(id: post.id)
expect(post).to eq(nil)
topic = Topic.with_deleted.find_by(id: topic.id)
expect(topic).to eq(nil)
end
it "destroys the post when force_destroy is true for regular posts" do
PostDestroyer.new(moderator, reply, force_destroy: true).destroy
expect { reply.reload }.to raise_error(ActiveRecord::RecordNotFound)
regular_post = Fabricate(:post)
topic = regular_post.topic
PostDestroyer.new(moderator, regular_post, force_destroy: true).destroy
expect { regular_post.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { topic.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it "destroys the post when force_destroy is true for posts by deleted users" do
regular_post = Fabricate(:post, post_number: 2)
UserDestroyer.new(admin).destroy(regular_post.user, delete_posts: true)
regular_post.reload
PostDestroyer.new(moderator, regular_post, force_destroy: true).destroy
expect { regular_post.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
describe "publishes messages to subscribers" do
# timestamps are rounded because postgres truncates the timestamp. that would cause the comparison if we compared
# these timestamps with the one read from the database
fab!(:first_post) { Fabricate(:post, created_at: 10.days.ago.round) }
fab!(:walter_white)
let!(:topic) { first_post.topic }
let!(:reply) do
Fabricate(:post, topic: topic, created_at: 5.days.ago.round, user: coding_horror)
end
let!(:expendable_reply) do
Fabricate(:post, topic: topic, created_at: 2.days.ago.round, user: walter_white)
end
it "when a post is destroyed publishes updated topic stats" do
expect(topic.reload.posts_count).to eq(3)
messages =
MessageBus.track_publish("/topic/#{topic.id}") do
PostDestroyer.new(moderator, expendable_reply, force_destroy: true).destroy
end
expect { expendable_reply.reload }.to raise_error(ActiveRecord::RecordNotFound)
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
expect(stats_message).to be_present
expect(stats_message.data[:posts_count]).to eq(2)
expect(stats_message.data[:last_posted_at]).to eq(reply.created_at.as_json)
expect(stats_message.data[:last_poster]).to eq(
BasicUserSerializer.new(reply.user, root: false).as_json,
)
end
it "when a post is deleted publishes updated topic stats" do
expect(topic.reload.posts_count).to eq(3)
messages =
MessageBus.track_publish("/topic/#{topic.id}") do
PostDestroyer.new(moderator, expendable_reply).destroy
end
expect(expendable_reply.reload.deleted_at).not_to eq(nil)
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
expect(stats_message).to be_present
expect(stats_message.data[:posts_count]).to eq(2)
expect(stats_message.data[:last_posted_at]).to eq(reply.created_at.as_json)
expect(stats_message.data[:last_poster]).to eq(
BasicUserSerializer.new(reply.user, root: false).as_json,
)
end
it "when a post is recovered publishes update topic stats" do
expect(topic.reload.posts_count).to eq(3)
PostDestroyer.new(moderator, expendable_reply).destroy
expect(topic.reload.posts_count).to eq(2)
expendable_reply.reload
messages =
MessageBus.track_publish("/topic/#{topic.id}") do
PostDestroyer.new(admin, expendable_reply).recover
end
expect(topic.reload.posts_count).to eq(3)
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
expect(stats_message).to be_present
expect(stats_message.data[:posts_count]).to eq(3)
expect(stats_message.data[:last_posted_at]).to eq(expendable_reply.created_at.as_json)
expect(stats_message.data[:last_poster]).to eq(
BasicUserSerializer.new(expendable_reply.user, root: false).as_json,
)
end
end
describe "mailing_list_mode emails on recovery" do
fab!(:topic)
fab!(:post_1) { Fabricate(:post, topic: topic) }
fab!(:post_2) { Fabricate(:post, topic: topic) }
it "enqueues the notify_mailing_list_subscribers_job for the post" do
PostDestroyer.new(admin, post_2).destroy
post_2.reload
expect_enqueued_with(job: :notify_mailing_list_subscribers, args: { post_id: post_2.id }) do
PostDestroyer.new(admin, post_2).recover
end
end
it "enqueues the notify_mailing_list_subscribers_job for the op" do
PostDestroyer.new(admin, post_1).destroy
post_1.reload
expect_enqueued_with(job: :notify_mailing_list_subscribers, args: { post_id: post_1.id }) do
PostDestroyer.new(admin, post_1).recover
end
end
end
describe "deleting a last reply" do
let!(:topic) { post.topic }
let!(:second_last_reply) do
freeze_time 1.day.from_now
create_post(topic:, user: coding_horror)
end
fab!(:user)
let!(:last_reply) do
freeze_time 2.days.from_now
create_post(topic:, user:)
end
context "when deleting by the creator" do
before { PostDestroyer.new(user, last_reply).destroy }
it "will reset the topic's bumped_at" do
topic.reload
expect(topic.bumped_at).to eq_time(second_last_reply.created_at)
end
it "still can see the post" do
last_reply.reload
expect(last_reply.deleted_at).to be_blank
expect(last_reply.deleted_by).to be_blank
expect(last_reply.user_deleted).to eq(true)
expect(last_reply.raw).to eq(I18n.t("js.post.deleted_by_author_simple"))
end
end
context "when deleting by a staff user" do
before { PostDestroyer.new(moderator, last_reply).destroy }
it "will reset the topic's bumped_at" do
topic.reload
expect(topic.bumped_at).to eq_time(second_last_reply.created_at)
end
end
context "when recovering a deleted reply" do
it "restores bumped_at when the last reply is recovered" do
PostDestroyer.new(moderator, last_reply).destroy
PostDestroyer.new(moderator, last_reply.reload).recover
topic.reload
expect(topic.bumped_at).to eq_time(last_reply.created_at)
end
it "restores bumped_at when a user-deleted reply is recovered" do
PostDestroyer.new(user, last_reply).destroy
PostDestroyer.new(user, last_reply.reload).recover
topic.reload
expect(topic.bumped_at).to eq_time(last_reply.created_at)
end
context "when the recovered post is not the last reply" do
let!(:newer_reply) do
freeze_time 3.days.from_now
create_post(topic:, user: coding_horror)
end
it "does not change bumped_at" do
PostDestroyer.new(moderator, second_last_reply).destroy
PostDestroyer.new(moderator, second_last_reply.reload).recover
topic.reload
expect(topic.bumped_at).to eq_time(newer_reply.created_at)
end
end
end
end
end