mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 09:26:18 +08:00
09932738e5
Before, whispers were only available for staff members. Config has been changed to allow to configure privileged groups with access to whispers. Post migration was added to move from the old setting into the new one. I considered having a boolean column `whisperer` on user model similar to `admin/moderator` for performance reason. Finally, I decided to keep looking for groups as queries are only done for current user and didn't notice any N+1 queries.
1170 lines
40 KiB
Ruby
1170 lines
40 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
describe PostDestroyer do
|
|
|
|
before do
|
|
UserActionManager.enable
|
|
end
|
|
|
|
fab!(:moderator) { Fabricate(:moderator) }
|
|
fab!(:admin) { Fabricate(:admin) }
|
|
fab!(:coding_horror) { Fabricate(:coding_horror) }
|
|
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)
|
|
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 "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 "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 }) do
|
|
PostDestroyer.new(@user, @reply.reload).recover
|
|
end
|
|
end
|
|
end
|
|
|
|
context "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 "recovered by user with access to moderate topic category" do
|
|
fab!(:review_user) { Fabricate(:user) }
|
|
|
|
before do
|
|
SiteSetting.enable_category_group_moderation = true
|
|
review_group = Fabricate(:group)
|
|
review_category = Fabricate(:category, reviewable_by_group_id: review_group.id)
|
|
@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).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 do
|
|
@reply.topic.trash!
|
|
end
|
|
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 "unmarks the matching incoming email for imap sync" do
|
|
SiteSetting.enable_imap = true
|
|
incoming = Fabricate(:incoming_email, imap_sync: true, post: post, topic: post.topic, imap_uid: 99)
|
|
PostDestroyer.new(moderator, post).recover
|
|
incoming.reload
|
|
expect(incoming.imap_sync).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe 'basic destroying' do
|
|
it "as the creator of the post, doesn't delete the post" do
|
|
begin
|
|
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
|
|
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 "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
|
|
end
|
|
|
|
context "deleted by user with access to moderate topic category" do
|
|
fab!(:review_user) { Fabricate(:user) }
|
|
|
|
before do
|
|
SiteSetting.enable_category_group_moderation = true
|
|
review_group = Fabricate(:group)
|
|
review_category = Fabricate(:category, reviewable_by_group_id: review_group.id)
|
|
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
|
|
|
|
context 'private message' do
|
|
fab!(:author) { Fabricate(: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 do
|
|
SiteSetting.enable_whispers = true
|
|
end
|
|
|
|
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
|
|
|
|
context 'deleting the second post in a topic' do
|
|
|
|
fab!(:user) { Fabricate(:user) }
|
|
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 '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
|
|
|
|
context "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 do
|
|
PostDestroyer.new(moderator, post).destroy
|
|
end
|
|
|
|
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 { PostDestroyer.new(admin, post).destroy }
|
|
|
|
it "deletes the post" do
|
|
subject
|
|
expect(post.deleted_at).to be_present
|
|
expect(post.deleted_by).to eq(admin)
|
|
end
|
|
|
|
it "creates a new user history entry" do
|
|
expect { subject }.to change { UserHistory.count }.by(1)
|
|
end
|
|
|
|
it 'triggers a extensibility event' do
|
|
events = DiscourseEvent.track_events { subject }
|
|
|
|
expect(events[0][:event_name]).to eq(:post_destroyed)
|
|
expect(events[0][:params].first).to eq(post)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "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 { PostDestroyer.new(moderator, reply).destroy }
|
|
|
|
it "deletes the reply" do
|
|
subject
|
|
expect(reply.deleted_at).to be_present
|
|
expect(reply.deleted_by).to eq(moderator)
|
|
end
|
|
|
|
it "doesn't decrement post_count again" do
|
|
expect { subject }.to_not change { author.user_stat.post_count }
|
|
end
|
|
end
|
|
|
|
context "as an admin" do
|
|
subject { PostDestroyer.new(admin, reply).destroy }
|
|
|
|
it "deletes the post" do
|
|
subject
|
|
expect(reply.deleted_at).to be_present
|
|
expect(reply.deleted_by).to eq(admin)
|
|
end
|
|
|
|
it "doesn't decrement post_count again" do
|
|
expect { subject }.to_not change { author.user_stat.post_count }
|
|
end
|
|
|
|
it "creates a new user history entry" do
|
|
expect { subject }.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
|
|
|
|
describe "incoming email and imap sync" do
|
|
fab!(:incoming) { Fabricate(:incoming_email, post: post, topic: post.topic) }
|
|
|
|
it "does nothing if imap not enabled" do
|
|
IncomingEmail.expects(:find_by).never
|
|
PostDestroyer.new(moderator, post).destroy
|
|
end
|
|
|
|
it "does nothing if the incoming email has no imap_uid" do
|
|
SiteSetting.enable_imap = true
|
|
PostDestroyer.new(moderator, post).destroy
|
|
expect(incoming.reload.imap_sync).to eq(false)
|
|
end
|
|
|
|
it "sets imap_sync to true for the matching incoming" do
|
|
SiteSetting.enable_imap = true
|
|
incoming.update(imap_uid: 999)
|
|
PostDestroyer.new(moderator, post).destroy
|
|
expect(incoming.reload.imap_sync).to eq(true)
|
|
end
|
|
end
|
|
|
|
describe '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
|
|
|
|
context '@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 do
|
|
Jobs::SendSystemMessage.clear
|
|
end
|
|
|
|
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).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)
|
|
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
|
|
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) { Fabricate(: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) { Fabricate(:topic) }
|
|
let!(:second_post) { Fabricate(:post, topic: topic) }
|
|
fab!(:other_topic) { Fabricate(:topic) }
|
|
let!(:other_post) { Fabricate(:post, topic: other_topic) }
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:base_url) { URI.parse(Discourse.base_url) }
|
|
let!(:guardian) { Guardian.new }
|
|
let!(:url) { "http://#{base_url.host}/t/#{other_topic.slug}/#{other_topic.id}/#{other_post.post_number}" }
|
|
|
|
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
|
|
let(:reporter) { Discourse.system_user }
|
|
fab!(:post) { Fabricate(:post) }
|
|
|
|
before do
|
|
reply = Fabricate(:post, topic: post.topic)
|
|
post.update(replies: [reply])
|
|
PostActionCreator.off_topic(reporter, post)
|
|
|
|
@reviewable_reply = PostActionCreator.off_topic(reporter, reply).reviewable
|
|
end
|
|
|
|
it 'ignores flagged replies' do
|
|
PostDestroyer.delete_with_replies(reporter, post)
|
|
|
|
expect(@reviewable_reply.reload.status).to eq Reviewable.statuses[:ignored]
|
|
end
|
|
|
|
it 'approves flagged replies' do
|
|
PostDestroyer.delete_with_replies(reporter, post, defer_reply_flags: false)
|
|
|
|
expect(@reviewable_reply.reload.status).to eq Reviewable.statuses[:approved]
|
|
end
|
|
end
|
|
|
|
describe "featured topics for user_profiles" do
|
|
fab!(:user) { Fabricate(: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) { Fabricate(: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 'always destroy the post when the force_destroy option is passed' do
|
|
PostDestroyer.new(moderator, reply, force_destroy: true).destroy
|
|
expect { reply.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
|
|
|
regular_post = Fabricate(:post)
|
|
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) { Fabricate(:walter_white) }
|
|
let!(:topic) { first_post.topic }
|
|
let!(:reply) { Fabricate(:post, topic: topic, created_at: 5.days.ago.round, user: coding_horror) }
|
|
let!(:expendable_reply) { Fabricate(:post, topic: topic, created_at: 2.days.ago.round, user: walter_white) }
|
|
|
|
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
|
|
end
|