discourse/spec/services/user_destroyer_spec.rb
Ted Johansson fb087b7ff6
DEV: Convert min_trust_to_post_links to groups ()
We're changing the implementation of trust levels to use groups. Part of this is to have site settings that reference trust levels use groups instead. It converts the min_trust_to_post_links  site setting to post_links_allowed_groups.

This isn't used by any of our plugins or themes, so very little fallout.
2024-01-18 14:08:40 +08:00

500 lines
17 KiB
Ruby

# frozen_string_literal: true
RSpec.describe UserDestroyer do
fab!(:user) { Fabricate(:user_with_secondary_email, refresh_auto_groups: true) }
fab!(:admin) { Fabricate(:admin, refresh_auto_groups: true) }
describe ".new" do
it "raises an error when user is nil" do
expect { UserDestroyer.new(nil) }.to raise_error(Discourse::InvalidParameters)
end
it "raises an error when user is not a User" do
expect { UserDestroyer.new(5) }.to raise_error(Discourse::InvalidParameters)
end
end
describe "#destroy" do
it "raises an error when user is nil" do
expect { UserDestroyer.new(admin).destroy(nil) }.to raise_error(Discourse::InvalidParameters)
end
it "raises an error when user is not a User" do
expect { UserDestroyer.new(admin).destroy("nothing") }.to raise_error(
Discourse::InvalidParameters,
)
end
it "raises an error when regular user tries to delete another user" do
expect { UserDestroyer.new(user).destroy(Fabricate(:user)) }.to raise_error(
Discourse::InvalidAccess,
)
end
shared_examples "successfully destroy a user" do
it "should delete the user" do
expect { destroy }.to change { User.count }.by(-1)
end
it "should return the deleted user record" do
return_value = destroy
expect(return_value).to eq(user)
expect(return_value).to be_destroyed
end
it "should log the action" do
StaffActionLogger.any_instance.expects(:log_user_deletion).with(user, anything).once
destroy
end
it "should not log the action if quiet is true" do
expect {
UserDestroyer.new(admin).destroy(user, destroy_opts.merge(quiet: true))
}.to_not change { UserHistory.where(action: UserHistory.actions[:delete_user]).count }
end
it "triggers a extensibility event" do
event = DiscourseEvent.track_events { destroy }.last
expect(event[:event_name]).to eq(:user_destroyed)
expect(event[:params].first).to eq(user)
end
end
shared_examples "email block list" do
it "doesn't add email to block list by default" do
ScreenedEmail.expects(:block).never
destroy
end
it "adds emails to block list if block_email is true" do
expect {
UserDestroyer.new(admin).destroy(user, destroy_opts.merge(block_email: true))
}.to change { ScreenedEmail.count }.by(2)
end
end
context "when user deletes self" do
subject(:destroy) { UserDestroyer.new(user).destroy(user, destroy_opts) }
let(:destroy_opts) { { delete_posts: true, context: "/u/username/preferences/account" } }
include_examples "successfully destroy a user"
it "should log proper context" do
destroy
expect(UserHistory.where(action: UserHistory.actions[:delete_user]).last.context).to eq(
I18n.t("staff_action_logs.user_delete_self", url: "/u/username/preferences/account"),
)
end
end
context "when context is missing" do
it "logs warning message if context is missing" do
logger = track_log_messages { UserDestroyer.new(admin).destroy(user) }
expect(logger.warnings).to include(/User destroyed without context from:/)
end
end
context "with a reviewable post" do
let!(:reviewable) { Fabricate(:reviewable, created_by: user) }
it "removes the queued post" do
UserDestroyer.new(admin).destroy(user)
expect(Reviewable.where(created_by_id: user.id).count).to eq(0)
end
end
context "with a reviewable user" do
let(:reviewable) { Fabricate(:reviewable, created_by: admin) }
it "sets the reviewable user as rejected" do
UserDestroyer.new(admin).destroy(reviewable.target)
expect(reviewable.reload).to be_rejected
end
end
context "with a directory item record" do
it "removes the directory item" do
DirectoryItem.create!(
user: user,
period_type: 1,
likes_received: 0,
likes_given: 0,
topics_entered: 0,
topic_count: 0,
post_count: 0,
)
UserDestroyer.new(admin).destroy(user)
expect(DirectoryItem.where(user_id: user.id).count).to eq(0)
end
end
context "with a draft" do
let!(:draft) { Draft.set(user, "test", 0, "test") }
it "removed the draft" do
UserDestroyer.new(admin).destroy(user)
expect(Draft.where(user_id: user.id).count).to eq(0)
end
end
context "when user has posts" do
let!(:topic_starter) { Fabricate(:user) }
let!(:topic) { Fabricate(:topic, user: topic_starter) }
let!(:first_post) { Fabricate(:post, user: topic_starter, topic: topic) }
let!(:post) { Fabricate(:post, user: user, topic: topic) }
context "when delete_posts is false" do
subject(:destroy) { UserDestroyer.new(admin).destroy(user) }
before do
user.stubs(:post_count).returns(1)
user.stubs(:first_post_created_at).returns(Time.zone.now)
end
it "should raise the right error" do
StaffActionLogger.any_instance.expects(:log_user_deletion).never
expect { destroy }.to raise_error(UserDestroyer::PostsExistError)
expect(user.reload.id).to be_present
end
end
context "when delete_posts is true" do
let(:destroy_opts) { { delete_posts: true } }
context "when staff deletes user" do
subject(:destroy) { UserDestroyer.new(admin).destroy(user, destroy_opts) }
include_examples "successfully destroy a user"
include_examples "email block list"
it "deletes the posts" do
destroy
expect(post.reload.deleted_at).not_to eq(nil)
expect(post.user_id).to eq(nil)
end
it "does not delete topics started by others in which the user has replies" do
destroy
expect(topic.reload.deleted_at).to eq(nil)
expect(topic.user_id).not_to eq(nil)
end
it "deletes topics started by the deleted user" do
spammer_topic = Fabricate(:topic, user: user)
Fabricate(:post, user: user, topic: spammer_topic)
destroy
expect(spammer_topic.reload.deleted_at).not_to eq(nil)
expect(spammer_topic.user_id).to eq(nil)
end
context "when delete_as_spammer is true" do
before { destroy_opts[:delete_as_spammer] = true }
it "approves reviewable flags" do
spammer_post = Fabricate(:post, user: user)
reviewable = PostActionCreator.inappropriate(admin, spammer_post).reviewable
expect(reviewable).to be_pending
destroy
reviewable.reload
expect(reviewable).to be_approved
end
it "rejects pending posts" do
post = Fabricate(:post, user: user)
reviewable =
Fabricate(
:reviewable,
type: "ReviewablePost",
target_type: "Post",
target_id: post.id,
created_by: Discourse.system_user,
target_created_by: user,
)
expect(reviewable).to be_pending
destroy
reviewable.reload
expect(reviewable).to be_rejected
end
end
end
context "when users deletes self" do
subject(:destroy) { UserDestroyer.new(user).destroy(user, destroy_opts) }
include_examples "successfully destroy a user"
include_examples "email block list"
it "deletes the posts" do
destroy
expect(post.reload.deleted_at).not_to eq(nil)
expect(post.user_id).to eq(nil)
end
end
end
end
context "when user was invited" do
it "should delete the invite of user" do
invite = Fabricate(:invite)
topic_invite = invite.topic_invites.create!(topic: Fabricate(:topic))
invited_group = invite.invited_groups.create!(group: Fabricate(:group))
user = Fabricate(:user)
user.user_emails.create!(email: invite.email)
UserDestroyer.new(admin).destroy(user)
expect(Invite.exists?(invite.id)).to eq(false)
expect(InvitedGroup.exists?(invited_group.id)).to eq(false)
expect(TopicInvite.exists?(topic_invite.id)).to eq(false)
end
end
context "when user created category" do
let!(:topic) { Fabricate(:topic, user: user) }
let!(:first_post) { Fabricate(:post, user: user, topic: topic) }
let!(:second_post) { Fabricate(:post, user: user, topic: topic) }
let!(:category) { Fabricate(:category, user: user, topic_id: topic.id) }
it "changes author of first category post to system user and still deletes second post" do
UserDestroyer.new(admin).destroy(user, delete_posts: true)
expect(first_post.reload.deleted_at).to eq(nil)
expect(first_post.user_id).to eq(Discourse.system_user.id)
expect(second_post.reload.deleted_at).not_to eq(nil)
expect(second_post.user_id).to eq(nil)
end
end
context "when user has no posts, but user_stats table has post_count > 0" do
subject(:destroy) { UserDestroyer.new(user).destroy(user, delete_posts: false) }
let(:destroy_opts) { {} }
before do
# out of sync user_stat data shouldn't break UserDestroyer
user.user_stat.update_attribute(:post_count, 1)
end
include_examples "successfully destroy a user"
end
context "when user has deleted posts" do
let!(:deleted_post) { Fabricate(:post, user: user, deleted_at: 1.hour.ago) }
it "should mark the user's deleted posts as belonging to a nuked user" do
expect { UserDestroyer.new(admin).destroy(user) }.to change { User.count }.by(-1)
expect(deleted_post.reload.user_id).to eq(nil)
end
end
context "when user has no posts" do
context "when destroy succeeds" do
subject(:destroy) { UserDestroyer.new(admin).destroy(user) }
let(:destroy_opts) { {} }
include_examples "successfully destroy a user"
include_examples "email block list"
end
context "when destroy fails" do
subject(:destroy) { UserDestroyer.new(admin).destroy(user) }
it "should not log the action" do
user.stubs(:destroy).returns(false)
StaffActionLogger.any_instance.expects(:log_user_deletion).never
destroy
end
end
end
context "when user has posts with links" do
context "with external links" do
before do
@post = Fabricate(:post_with_external_links, user: user)
TopicLink.extract_from(@post)
end
it "doesn't add ScreenedUrl records by default" do
ScreenedUrl.expects(:watch).never
UserDestroyer.new(admin).destroy(user, delete_posts: true)
end
it "adds ScreenedUrl records when :block_urls is true" do
ScreenedUrl.expects(:watch).with(anything, anything, has_key(:ip_address)).at_least_once
UserDestroyer.new(admin).destroy(user, delete_posts: true, block_urls: true)
end
end
context "with internal links" do
before do
@post = Fabricate(:post_with_external_links, user: user)
TopicLink.extract_from(@post)
TopicLink.where(user: user).update_all(internal: true)
end
it "doesn't add ScreenedUrl records" do
ScreenedUrl.expects(:watch).never
UserDestroyer.new(admin).destroy(user, delete_posts: true, block_urls: true)
end
end
context "with oneboxed links" do
before do
@post = Fabricate(:post_with_youtube, user: user)
TopicLink.extract_from(@post)
end
it "doesn't add ScreenedUrl records" do
ScreenedUrl.expects(:watch).never
UserDestroyer.new(admin).destroy(user, delete_posts: true, block_urls: true)
end
end
end
context "with ip address screening" do
it "doesn't create screened_ip_address records by default" do
ScreenedIpAddress.expects(:watch).never
UserDestroyer.new(admin).destroy(user)
end
context "when block_ip is true" do
it "creates a new screened_ip_address record" do
ScreenedIpAddress.expects(:watch).with(user.ip_address).returns(stub_everything)
UserDestroyer.new(admin).destroy(user, block_ip: true)
end
it "creates two new screened_ip_address records when registration_ip_address is different than last ip_address" do
user.registration_ip_address = "12.12.12.12"
ScreenedIpAddress.expects(:watch).with(user.ip_address).returns(stub_everything)
ScreenedIpAddress
.expects(:watch)
.with(user.registration_ip_address)
.returns(stub_everything)
UserDestroyer.new(admin).destroy(user, block_ip: true)
end
end
end
context "when user created a category" do
let!(:category) { Fabricate(:category_with_definition, user: user) }
it "assigns the system user to the categories" do
UserDestroyer.new(admin).destroy(user, delete_posts: true)
expect(category.reload.user_id).to eq(Discourse.system_user.id)
expect(category.topic).to be_present
expect(category.topic.user_id).to eq(Discourse.system_user.id)
end
end
describe "Destroying a user with security key" do
let!(:security_key) { Fabricate(:user_security_key_with_random_credential, user: user) }
it "removes the security key" do
UserDestroyer.new(admin).destroy(user)
expect(UserSecurityKey.where(user_id: user.id).count).to eq(0)
end
end
describe "Destroying a user with a bookmark" do
let!(:bookmark) { Fabricate(:bookmark, user: user) }
it "removes the bookmark" do
UserDestroyer.new(admin).destroy(user)
expect(Bookmark.where(user_id: user.id).count).to eq(0)
end
end
context "when user liked things" do
before do
@topic = Fabricate(:topic, user: Fabricate(:user))
@post = Fabricate(:post, user: @topic.user, topic: @topic)
PostActionCreator.like(user, @post)
end
it "should destroy the like" do
expect { UserDestroyer.new(admin).destroy(user, delete_posts: true) }.to change {
PostAction.count
}.by(-1)
expect(@post.reload.like_count).to eq(0)
end
end
context "when user belongs to groups that grant trust level" do
let(:group) { Fabricate(:group, grant_trust_level: 4) }
before { group.add(user) }
it "can delete the user" do
d = UserDestroyer.new(admin)
expect { d.destroy(user) }.to change { User.count }.by(-1)
end
it "can delete the user if they have a manual locked trust level and have no email" do
user.update(manual_locked_trust_level: 3)
UserEmail.where(user: user).delete_all
user.reload
expect { UserDestroyer.new(admin).destroy(user) }.to change { User.count }.by(-1)
end
it "can delete the user if they were to fall into another trust level and have no email" do
g2 = Fabricate(:group, grant_trust_level: 1)
g2.add(user)
UserEmail.where(user: user).delete_all
user.reload
expect { UserDestroyer.new(admin).destroy(user) }.to change { User.count }.by(-1)
end
end
context "when user has staff action logs" do
before do
logger = StaffActionLogger.new(user)
logger.log_site_setting_change(
"site_description",
"Our friendly community",
"My favourite community",
)
logger.log_site_setting_change(
"site_description",
"Our friendly community",
"My favourite community",
details: "existing details",
)
end
it "should keep the staff action log and add the username" do
username = user.username
ids =
UserHistory.staff_action_records(Discourse.system_user, acting_user: username).map(&:id)
UserDestroyer.new(admin).destroy(user, delete_posts: true)
details = UserHistory.where(id: ids).map(&:details)
expect(details).to contain_exactly(
"\nuser_id: #{user.id}\nusername: #{username}",
"existing details\nuser_id: #{user.id}\nusername: #{username}",
)
end
end
context "when user got an email" do
let!(:email_log) { Fabricate(:email_log, user: user) }
it "does not delete the email log" do
expect { UserDestroyer.new(admin).destroy(user, delete_posts: true) }.to_not change {
EmailLog.count
}
end
end
end
end