# frozen_string_literal: true require "discourse_ip_info" require "rotp" RSpec.describe Admin::UsersController do fab!(:admin) fab!(:another_admin) { Fabricate(:admin) } fab!(:moderator) fab!(:user) fab!(:coding_horror) describe "#index" do context "when logged in as an admin" do before { sign_in(admin) } it "returns success with JSON" do get "/admin/users/list.json" expect(response.status).to eq(200) expect(response.parsed_body).to be_present end context "when showing emails" do it "returns email for all the users" do get "/admin/users/list.json", params: { show_emails: "true" } expect(response.status).to eq(200) data = response.parsed_body data.each { |user| expect(user["email"]).to be_present } end it "logs only 1 entry" do expect do get "/admin/users/list.json", params: { show_emails: "true" } end.to change { UserHistory.where( action: UserHistory.actions[:check_email], acting_user_id: admin.id, ).count }.by(1) expect(response.status).to eq(200) end it "can be ordered by emails" do get "/admin/users/list.json", params: { show_emails: "true", order: "email" } expect(response.status).to eq(200) end end end context "when logged in as a moderator" do before { sign_in(moderator) } it "returns users" do get "/admin/users/list.json" expect(response.status).to eq(200) expect(response.parsed_body).to be_present end end context "when logged in as a non-staff user" do before { sign_in(user) } it "denies access with a 404 response" do get "/admin/users/list.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end end describe "#show" do context "when logged in as an admin" do before { sign_in(admin) } context "with an existing user" do it "returns success" do get "/admin/users/#{user.id}.json" expect(response.status).to eq(200) end it "includes associated accounts" do user.user_associated_accounts.create!( provider_name: "pluginauth", provider_uid: "pluginauth_uid", ) get "/admin/users/#{user.id}.json" expect(response.status).to eq(200) expect(response.parsed_body["external_ids"].size).to eq(1) expect(response.parsed_body["external_ids"]["pluginauth"]).to eq("pluginauth_uid") end end context "with a non-existing user" do it "returns 404 error" do get "/admin/users/0.json" expect(response.status).to eq(404) end end end context "when logged in as a moderator" do before { sign_in(moderator) } it "returns user" do get "/admin/users/#{user.id}.json" expect(response.status).to eq(200) expect(response.parsed_body["id"]).to eq(user.id) end it "includes count of similiar users" do Fabricate(:user, ip_address: "88.88.88.88") Fabricate(:admin, ip_address: user.ip_address) Fabricate(:moderator, ip_address: user.ip_address) similar_user = Fabricate(:user, ip_address: user.ip_address) get "/admin/users/#{user.id}.json" expect(response.status).to eq(200) expect(response.parsed_body["similar_users_count"]).to eq(1) end end context "when logged in as a non-staff user" do before { sign_in(user) } it "denies access with a 404 response" do get "/admin/users/#{user.id}.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end end describe "#similar_users" do before { sign_in(admin) } it "includes similar users who aren't admin or mods" do Fabricate(:user, ip_address: "88.88.88.88") Fabricate(:admin, ip_address: user.ip_address) Fabricate(:moderator, ip_address: user.ip_address) similar_user = Fabricate(:user, ip_address: user.ip_address) get "/admin/users/#{user.id}/similar-users.json" expect(response.status).to eq(200) expect(response.parsed_body["users"].map { |u| u["id"] }).to contain_exactly(similar_user.id) end end describe "#approve" do let(:evil_trout) { Fabricate(:evil_trout) } before { SiteSetting.must_approve_users = true } shared_examples "user approval possible" do it "creates a reviewable if one does not exist" do evil_trout.update!(active: true) expect(ReviewableUser.find_by(target: evil_trout)).to be_blank put "/admin/users/#{evil_trout.id}/approve.json" expect(response.code).to eq("200") expect(ReviewableUser.find_by(target: evil_trout)).to be_present expect(evil_trout.reload).to be_approved end it "calls approve" do Jobs.run_immediately! evil_trout.activate put "/admin/users/#{evil_trout.id}/approve.json" expect(response.status).to eq(200) evil_trout.reload expect(evil_trout.approved).to eq(true) expect( UserHistory.where( action: UserHistory.actions[:approve_user], target_user_id: evil_trout.id, ).count, ).to eq(1) end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "user approval possible" end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "user approval possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents user approvals with a 404 response" do put "/admin/users/#{evil_trout.id}/approve.json" evil_trout.reload expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(evil_trout.approved).to eq(false) end end end describe "#approve_bulk" do let(:evil_trout) { Fabricate(:evil_trout) } before { SiteSetting.must_approve_users = true } shared_examples "bulk user approval possible" do it "does nothing without users" do put "/admin/users/approve-bulk.json" evil_trout.reload expect(response.status).to eq(200) expect(evil_trout.approved).to eq(false) end it "approves the user when permitted" do Jobs.run_immediately! evil_trout.activate put "/admin/users/approve-bulk.json", params: { users: [evil_trout.id] } expect(response.status).to eq(200) evil_trout.reload expect(evil_trout.approved).to eq(true) end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "bulk user approval possible" end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "bulk user approval possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents bulk user approvals with a 404 response" do put "/admin/users/approve-bulk.json", params: { users: [evil_trout.id] } evil_trout.reload expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(evil_trout.approved).to eq(false) end end end describe "#suspend" do fab!(:created_post) { Fabricate(:post) } fab!(:other_user) { Fabricate(:user) } let(:suspend_params) do { suspend_until: 5.hours.from_now, reason: "because of this post", post_id: created_post.id } end shared_examples "suspension of active user possible" do it "suspends user" do expect(user).not_to be_suspended expect do put "/admin/users/#{user.id}/suspend.json", params: { suspend_until: 5.hours.from_now, reason: "because I said so", } end.not_to change { Jobs::CriticalUserEmail.jobs.size } expect(response.status).to eq(200) user.reload expect(user).to be_suspended expect(user.suspended_at).to be_present expect(user.suspended_till).to be_present expect(user.suspend_record).to be_present log = UserHistory.where(target_user_id: user.id).order("id desc").first expect(log.details).to match(/because I said so/) end end shared_examples "suspension of staff users" do it "doesn't allow suspending a staff user" do put "/admin/users/#{another_admin.id}/suspend.json", params: { suspend_until: 5.hours.from_now, reason: "naughty boy", } expect(response.status).to eq(403) expect(another_admin.reload).not_to be_suspended end it "doesn't allow suspending a staff user via other_user_ids" do put "/admin/users/#{user.id}/suspend.json", params: { suspend_until: 5.hours.from_now, reason: "naughty boy", other_user_ids: [another_admin.id], } expect(response.status).to eq(403) expect(user.reload).not_to be_suspended expect(another_admin.reload).not_to be_suspended end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "suspension of active user possible" include_examples "suspension of staff users" it "checks if user is suspended" do put "/admin/users/#{user.id}/suspend.json", params: { suspend_until: 5.hours.from_now, reason: "because I said so", } put "/admin/users/#{user.id}/suspend.json", params: { suspend_until: 5.hours.from_now, reason: "because I said so too", } expect(response.status).to eq(409) expect(response.parsed_body["message"]).to eq( I18n.t( "user.already_suspended", staff: admin.username, time_ago: AgeWords.time_ago_in_words( user.suspend_record.created_at, true, scope: :"datetime.distance_in_words_verbose", ), ), ) end context "with webhook" do fab!(:user_web_hook) it "enqueues a user_suspended webhook event" do expect do put "/admin/users/#{user.id}/suspend.json", params: { suspend_until: 5.hours.from_now, reason: "because I said so", } end.to change { Jobs::EmitWebHookEvent.jobs.size }.by(2) user.reload job_args = Jobs::EmitWebHookEvent.jobs.last["args"].find do |args| args["event_name"] == "user_suspended" end expect(job_args).to be_present expect(job_args["id"]).to eq(user.id) expect(job_args["payload"]).to eq(WebHook.generate_payload(:user, user)) end end it "fails the request if the reason is too long" do expect(user).not_to be_suspended put "/admin/users/#{user.id}/suspend.json", params: { reason: "x" * 301, suspend_until: 5.hours.from_now, } expect(response.status).to eq(400) user.reload expect(user).not_to be_suspended end it "requires suspend_until and reason" do expect(user).not_to be_suspended put "/admin/users/#{user.id}/suspend.json", params: {} expect(response.status).to eq(400) user.reload expect(user).not_to be_suspended expect(user).not_to be_suspended put "/admin/users/#{user.id}/suspend.json", params: { suspend_until: 5.hours.from_now } expect(response.status).to eq(400) user.reload expect(user).not_to be_suspended end it "fails the request if other_user_ids is too big" do another_user = Fabricate(:user) other_user_ids = [another_user.id] other_user_ids.push(*(1..304).to_a) put "/admin/users/#{user.id}/suspend.json", params: { reason: "because I said so", suspend_until: 5.hours.from_now, other_user_ids:, } expect(response.status).to eq(400) user.reload expect(user).not_to be_suspended another_user.reload expect(another_user).not_to be_suspended end context "with an associated post" do it "can have an associated post" do put "/admin/users/#{user.id}/suspend.json", params: suspend_params expect(response.status).to eq(200) log = UserHistory.where(target_user_id: user.id).order("id desc").first expect(log.post_id).to eq(created_post.id) end it "can delete an associated post" do put "/admin/users/#{user.id}/suspend.json", params: suspend_params.merge(post_action: "delete") created_post.reload expect(created_post.deleted_at).to be_present expect(response.status).to eq(200) end it "won't delete a category topic" do c = Fabricate(:category_with_definition) cat_post = c.topic.posts.first put( "/admin/users/#{user.id}/suspend.json", params: suspend_params.merge(post_action: "delete", post_id: cat_post.id), ) cat_post.reload expect(cat_post.deleted_at).to be_blank expect(response.status).to eq(200) end it "won't delete a category topic by replies" do c = Fabricate(:category_with_definition) cat_post = c.topic.posts.first put( "/admin/users/#{user.id}/suspend.json", params: suspend_params.merge(post_action: "delete_replies", post_id: cat_post.id), ) cat_post.reload expect(cat_post.deleted_at).to be_blank expect(response.status).to eq(200) end it "can delete an associated post and its replies" do reply = PostCreator.create( Fabricate(:user), raw: "this is the reply text", reply_to_post_number: created_post.post_number, topic_id: created_post.topic_id, ) nested_reply = PostCreator.create( Fabricate(:user), raw: "this is the reply text2", reply_to_post_number: reply.post_number, topic_id: created_post.topic_id, ) put "/admin/users/#{user.id}/suspend.json", params: suspend_params.merge(post_action: "delete_replies") expect(created_post.reload.deleted_at).to be_present expect(reply.reload.deleted_at).to be_present expect(nested_reply.reload.deleted_at).to be_present expect(response.status).to eq(200) end it "can edit an associated post" do put "/admin/users/#{user.id}/suspend.json", params: suspend_params.merge(post_action: "edit", post_edit: "this is the edited content") expect(response.status).to eq(200) created_post.reload expect(created_post.deleted_at).to be_blank expect(created_post.raw).to eq("this is the edited content") expect(response.status).to eq(200) end end it "can send a message to the user" do put "/admin/users/#{user.id}/suspend.json", params: { suspend_until: 10.days.from_now, reason: "short reason", message: "long reason", } expect(response.status).to eq(200) expect(Jobs::CriticalUserEmail.jobs.size).to eq(1) job_args = Jobs::CriticalUserEmail.jobs.first["args"].first expect(job_args["type"]).to eq("account_suspended") expect(job_args["user_id"]).to eq(user.id) log = UserHistory.where(target_user_id: user.id).order("id desc").first expect(log).to be_present expect(log.details).to match(/short reason/) expect(log.details).to match(/long reason/) end it "also prevents use of any api keys" do api_key = Fabricate(:api_key, user: user) post "/bookmarks.json", params: { bookmarkable_id: Fabricate(:post).id, bookmarkable_type: "Post", }, headers: { HTTP_API_KEY: api_key.key, } expect(response.status).to eq(200) put "/admin/users/#{user.id}/suspend.json", params: suspend_params expect(response.status).to eq(200) user.reload expect(user).to be_suspended post "/bookmarks.json", params: { post_id: Fabricate(:post).id, }, headers: { HTTP_API_KEY: api_key.key, } expect(response.status).to eq(403) end it "can silence multiple users" do put "/admin/users/#{user.id}/suspend.json", params: { suspend_until: 10.days.from_now, reason: "short reason", message: "long reason", other_user_ids: [other_user.id], } expect(response.status).to eq(200) expect(user.reload).to be_suspended expect(other_user.reload).to be_suspended end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "suspension of active user possible" include_examples "suspension of staff users" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents user suspensions with a 404 response" do expect do put "/admin/users/#{user.id}/suspend.json", params: { suspend_until: 5.hours.from_now, reason: "because I said so", } end.not_to change { Jobs::CriticalUserEmail.jobs.size } expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) user.reload expect(user).not_to be_suspended expect(user.suspended_at).to be_nil expect(user.suspended_till).to be_nil expect(user.suspend_record).to be_nil end end end describe "#unsuspend" do context "when logged in as an admin" do before { sign_in(admin) } context "with webhook" do fab!(:user_web_hook) it "enqueues a user_unsuspended webhook event" do user.update!(suspended_at: DateTime.now, suspended_till: 2.years.from_now) expect do put "/admin/users/#{user.id}/unsuspend.json" end.to change { Jobs::EmitWebHookEvent.jobs.size }.by(1) user.reload job_args = Jobs::EmitWebHookEvent.jobs.last["args"].first expect(job_args["id"]).to eq(user.id) expect(job_args["payload"]).to eq(WebHook.generate_payload(:user, user)) end end end end describe "#revoke_admin" do fab!(:another_admin) { Fabricate(:admin) } context "when logged in as an admin" do before { sign_in(admin) } it "updates the admin flag" do put "/admin/users/#{another_admin.id}/revoke_admin.json" expect(response.status).to eq(200) another_admin.reload expect(another_admin.admin).to eq(false) expect(response.parsed_body["can_be_merged"]).to eq(true) expect(response.parsed_body["can_be_deleted"]).to eq(true) expect(response.parsed_body["can_be_anonymized"]).to eq(true) expect(response.parsed_body["can_delete_all_posts"]).to eq(true) end end shared_examples "admin access revocation not allowed" do it "prevents revoking admin access with a 404 response" do put "/admin/users/#{another_admin.id}/revoke_admin.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) another_admin.reload expect(another_admin.admin).to eq(true) end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "admin access revocation not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "admin access revocation not allowed" end end describe "#grant_admin" do fab!(:another_user) { coding_horror } after { Discourse.redis.flushdb } context "when logged in as an admin" do before { sign_in(admin) } it "returns a 404 if the username doesn't exist" do put "/admin/users/123123/grant_admin.json" expect(response.status).to eq(404) end it "sends a confirmation email if the acting admin does not have a second factor method enabled" do expect(AdminConfirmation.exists_for?(another_user.id)).to eq(false) put "/admin/users/#{another_user.id}/grant_admin.json" expect(response.status).to eq(200) expect(AdminConfirmation.exists_for?(another_user.id)).to eq(true) end it "asks the acting admin for second factor if it is enabled" do Fabricate(:user_second_factor_totp, user: admin) put "/admin/users/#{another_user.id}/grant_admin.json", xhr: true expect(response.parsed_body["second_factor_challenge_nonce"]).to be_present expect(another_user.reload.admin).to eq(false) end it "grants admin if second factor is correct" do user_second_factor = Fabricate(:user_second_factor_totp, user: admin) put "/admin/users/#{another_user.id}/grant_admin.json", xhr: true nonce = response.parsed_body["second_factor_challenge_nonce"] expect(nonce).to be_present expect(another_user.reload.admin).to eq(false) post "/session/2fa.json", params: { nonce: nonce, second_factor_token: ROTP::TOTP.new(user_second_factor.data).now, second_factor_method: UserSecondFactor.methods[:totp], } res = response.parsed_body expect(response.status).to eq(200) expect(res["ok"]).to eq(true) expect(res["callback_method"]).to eq("PUT") expect(res["callback_path"]).to eq("/admin/users/#{another_user.id}/grant_admin.json") expect(res["redirect_url"]).to eq( "/admin/users/#{another_user.id}/#{another_user.username}", ) expect(another_user.reload.admin).to eq(false) put res["callback_path"], params: { second_factor_nonce: nonce } expect(response.status).to eq(200) expect(another_user.reload.admin).to eq(true) end it "does not grant admin if second factor auth is not successful" do user_second_factor = Fabricate(:user_second_factor_totp, user: admin) put "/admin/users/#{another_user.id}/grant_admin.json", xhr: true nonce = response.parsed_body["second_factor_challenge_nonce"] expect(nonce).to be_present expect(another_user.reload.admin).to eq(false) token = ROTP::TOTP.new(user_second_factor.data).now.to_i token = (token == 999_999 ? token - 1 : token + 1).to_s post "/session/2fa.json", params: { nonce: nonce, second_factor_token: token, second_factor_method: UserSecondFactor.methods[:totp], } expect(response.status).to eq(400) expect(another_user.reload.admin).to eq(false) put "/admin/users/#{another_user.id}/grant_admin.json", params: { second_factor_nonce: nonce, } expect(response.status).to eq(401) expect(another_user.reload.admin).to eq(false) end it "does not grant admin if the acting admin loses permission in the middle of the process" do user_second_factor = Fabricate(:user_second_factor_totp, user: admin) put "/admin/users/#{another_user.id}/grant_admin.json", xhr: true nonce = response.parsed_body["second_factor_challenge_nonce"] expect(nonce).to be_present expect(another_user.reload.admin).to eq(false) post "/session/2fa.json", params: { nonce: nonce, second_factor_token: ROTP::TOTP.new(user_second_factor.data).now, second_factor_method: UserSecondFactor.methods[:totp], } res = response.parsed_body expect(response.status).to eq(200) expect(res["ok"]).to eq(true) expect(res["callback_method"]).to eq("PUT") expect(res["callback_path"]).to eq("/admin/users/#{another_user.id}/grant_admin.json") expect(res["redirect_url"]).to eq( "/admin/users/#{another_user.id}/#{another_user.username}", ) expect(another_user.reload.admin).to eq(false) admin.update!(admin: false) put res["callback_path"], params: { second_factor_nonce: nonce } expect(response.status).to eq(404) expect(another_user.reload.admin).to eq(false) end it "does not accept backup codes" do Fabricate(:user_second_factor_totp, user: admin) Fabricate(:user_second_factor_backup, user: admin) put "/admin/users/#{another_user.id}/grant_admin.json", xhr: true nonce = response.parsed_body["second_factor_challenge_nonce"] expect(nonce).to be_present expect(another_user.reload.admin).to eq(false) post "/session/2fa.json", params: { nonce: nonce, second_factor_token: "iAmValidBackupCode", second_factor_method: UserSecondFactor.methods[:backup_codes], } expect(response.status).to eq(403) expect(another_user.reload.admin).to eq(false) end end shared_examples "admin grants not allowed" do context "with 2FA enabled" do before { Fabricate(:user_second_factor_totp, user: user) } it "prevents granting admin with a 404 response" do put "/admin/users/#{another_user.id}/grant_admin.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(AdminConfirmation.exists_for?(another_user.id)).to eq(false) end end context "with 2FA disabled" do it "prevents granting admin with a 404 response" do put "/admin/users/#{another_user.id}/grant_admin.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(AdminConfirmation.exists_for?(another_user.id)).to eq(false) end end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "admin grants not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "admin grants not allowed" end end describe "#add_group" do fab!(:group) context "when logged in as an admin" do before { sign_in(admin) } it "adds the user to the group" do post "/admin/users/#{user.id}/groups.json", params: { group_id: group.id } expect(response.status).to eq(200) expect(GroupUser.where(user_id: user.id, group_id: group.id).exists?).to eq(true) group_history = GroupHistory.last expect(group_history.action).to eq(GroupHistory.actions[:add_user_to_group]) expect(group_history.acting_user).to eq(admin) expect(group_history.target_user).to eq(user) # Doing it again doesn't raise an error post "/admin/users/#{user.id}/groups.json", params: { group_id: group.id } expect(response.status).to eq(200) end it "returns not-found error when there is no group" do group.destroy! put "/admin/users/#{user.id}/groups.json", params: { group_id: group.id } expect(response.status).to eq(404) end it "does not allow adding users to an automatic group" do group.update!(automatic: true) expect do post "/admin/users/#{user.id}/groups.json", params: { group_id: group.id } end.to_not change { group.users.count } expect(response.status).to eq(422) expect(response.parsed_body["errors"]).to eq(["You cannot modify an automatic group"]) end end shared_examples "adding users to groups not allowed" do it "prevents adding user to group with a 404 response" do post "/admin/users/#{user.id}/groups.json", params: { group_id: group.id } expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(GroupUser.where(user_id: user.id, group_id: group.id).exists?).to eq(false) end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "adding users to groups not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "adding users to groups not allowed" end end describe "#remove_group" do context "when logged in as an admin" do before { sign_in(admin) } it "also clears the user's primary group" do group = Fabricate(:group, users: [user]) user.update!(primary_group_id: group.id) delete "/admin/users/#{user.id}/groups/#{group.id}.json" expect(response.status).to eq(200) expect(user.reload.primary_group).to eq(nil) end it "returns not-found error when there is no group" do delete "/admin/users/#{user.id}/groups/9090.json" expect(response.status).to eq(404) end it "does not allow removing owners from an automatic group" do group = Fabricate(:group, users: [user], automatic: true) delete "/admin/users/#{user.id}/groups/#{group.id}.json" expect(response.status).to eq(422) expect(response.parsed_body["errors"]).to eq(["You cannot modify an automatic group"]) end end shared_examples "removing user from groups not allowed" do it "prevents removing user from group with a 404 response" do group = Fabricate(:group, users: [user]) user.update!(primary_group_id: group.id) delete "/admin/users/#{user.id}/groups/#{group.id}.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(user.reload.primary_group).to eq(group) end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "removing user from groups not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "removing user from groups not allowed" end end describe "#trust_level" do fab!(:another_user) do coding_horror.update!(created_at: 1.month.ago) coding_horror end shared_examples "trust level updates possible" do it "returns a 404 if the username doesn't exist" do put "/admin/users/123123/trust_level.json" expect(response.status).to eq(404) end it "upgrades the user's trust level" do put "/admin/users/#{another_user.id}/trust_level.json", params: { level: 2 } expect(response.status).to eq(200) another_user.reload expect(another_user.trust_level).to eq(2) expect( UserHistory.where( target_user: another_user, acting_user: acting_user, action: UserHistory.actions[:change_trust_level], ).count, ).to eq(1) end it "raises no error when demoting a user below their current trust level (locks trust level)" do stat = another_user.user_stat stat.topics_entered = SiteSetting.tl1_requires_topics_entered + 1 stat.posts_read_count = SiteSetting.tl1_requires_read_posts + 1 stat.time_read = SiteSetting.tl1_requires_time_spent_mins * 60 stat.save! another_user.update(trust_level: TrustLevel[1]) put "/admin/users/#{another_user.id}/trust_level.json", params: { level: TrustLevel[0] } expect(response.status).to eq(200) another_user.reload expect(another_user.trust_level).to eq(TrustLevel[0]) expect(another_user.manual_locked_trust_level).to eq(TrustLevel[0]) end end context "when logged in as an admin" do let(:acting_user) { admin } before { sign_in(admin) } include_examples "trust level updates possible" end context "when logged in as a moderator" do let(:acting_user) { moderator } before { sign_in(moderator) } include_examples "trust level updates possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents updates trust level with a 404 response" do put "/admin/users/#{another_user.id}/trust_level.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end end describe "#grant_moderation" do fab!(:another_user) { coding_horror } context "when logged in as an admin" do before { sign_in(admin) } it "returns a 404 if the username doesn't exist" do put "/admin/users/123123/grant_moderation.json" expect(response.status).to eq(404) end it "updates the moderator flag" do expect_enqueued_with( job: :send_system_message, args: { user_id: another_user.id, message_type: "welcome_staff", message_options: { role: :moderator, }, }, ) { put "/admin/users/#{another_user.id}/grant_moderation.json" } expect(response.status).to eq(200) another_user.reload expect(another_user.moderator).to eq(true) expect(response.parsed_body["can_be_merged"]).to eq(false) expect(response.parsed_body["can_be_anonymized"]).to eq(false) end end shared_examples "moderator access grant not allowed" do it "prevents granting moderation rights to user with a 404 response" do put "/admin/users/#{another_user.id}/grant_moderation.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "moderator access grant not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "moderator access grant not allowed" end end describe "#revoke_moderation" do fab!(:another_moderator) { Fabricate(:moderator) } context "when logged in as an admin" do before { sign_in(admin) } it "updates the moderator flag" do put "/admin/users/#{another_moderator.id}/revoke_moderation.json" expect(response.status).to eq(200) another_moderator.reload expect(another_moderator.moderator).to eq(false) expect(response.parsed_body["can_be_merged"]).to eq(true) expect(response.parsed_body["can_be_anonymized"]).to eq(true) end end shared_examples "moderator access revocation not allowed" do it "prevents revocation of moderator access with a 404 response" do put "/admin/users/#{another_moderator.id}/revoke_moderation.json" another_moderator.reload expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(another_moderator.moderator).to eq(true) end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "moderator access revocation not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "moderator access revocation not allowed" end end describe "#primary_group" do fab!(:group) fab!(:another_user) { coding_horror } fab!(:another_group) { Fabricate(:group, title: "New") } shared_examples "primary group updates possible" do it "returns a 404 if the user doesn't exist" do put "/admin/users/123123/primary_group.json" expect(response.status).to eq(404) end it "changes the user's primary group" do group.add(another_user) put "/admin/users/#{another_user.id}/primary_group.json", params: { primary_group_id: group.id, } expect(response.status).to eq(200) another_user.reload expect(another_user.primary_group_id).to eq(group.id) end it "doesn't change primary group if they aren't a member of the group" do put "/admin/users/#{another_user.id}/primary_group.json", params: { primary_group_id: group.id, } expect(response.status).to eq(200) another_user.reload expect(another_user.primary_group_id).to eq(nil) end it "remove user's primary group" do group.add(another_user) put "/admin/users/#{another_user.id}/primary_group.json", params: { primary_group_id: "" } expect(response.status).to eq(200) another_user.reload expect(another_user.primary_group_id).to eq(nil) end it "updates user's title when it matches the previous primary group title" do group.update_columns(primary_group: true, title: "Previous") group.add(another_user) another_group.add(another_user) expect(another_user.reload.title).to eq("Previous") put "/admin/users/#{another_user.id}/primary_group.json", params: { primary_group_id: another_group.id, } another_user.reload expect(response.status).to eq(200) expect(another_user.primary_group_id).to eq(another_group.id) expect(another_user.title).to eq("New") end it "doesn't update user's title when it does not match the previous primary group title" do another_user.update_columns(title: "Different") group.update_columns(primary_group: true, title: "Previous") another_group.add(another_user) group.add(another_user) expect(another_user.reload.title).to eq("Different") put "/admin/users/#{another_user.id}/primary_group.json", params: { primary_group_id: another_group.id, } another_user.reload expect(response.status).to eq(200) expect(another_user.primary_group_id).to eq(another_group.id) expect(another_user.title).to eq("Different") end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "primary group updates possible" end context "when logged in as a moderator" do before { sign_in(moderator) } context "when moderators_manage_categories_and_groups site setting is enabled" do before { SiteSetting.moderators_manage_categories_and_groups = true } include_examples "primary group updates possible" end context "when moderators_manage_categories_and_groups site setting is disabled" do before { SiteSetting.moderators_manage_categories_and_groups = false } it "prevents setting primary group with a 403 response" do group.add(another_user) put "/admin/users/#{another_user.id}/primary_group.json", params: { primary_group_id: group.id, } expect(response.status).to eq(403) expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) another_user.reload expect(another_user.primary_group_id).to eq(nil) end end end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents setting primary group with a 404 response" do group.add(another_user) put "/admin/users/#{another_user.id}/primary_group.json", params: { primary_group_id: group.id, } expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) another_user.reload expect(another_user.primary_group_id).to eq(nil) end end end describe "#destroy" do fab!(:delete_me) { Fabricate(:user, refresh_auto_groups: true) } shared_examples "user deletion possible" do it "returns a 403 if the user doesn't exist" do delete "/admin/users/123123drink.json" expect(response.status).to eq(403) end context "when user has post" do let(:topic) { Fabricate(:topic, user: delete_me) } let!(:post) { Fabricate(:post, topic: topic, user: delete_me) } it "returns an api response that the user can't be deleted because it has posts" do post_count = delete_me.posts.joins(:topic).count delete_me_topic = Fabricate(:topic) Fabricate(:post, topic: delete_me_topic, user: delete_me) PostDestroyer.new(admin, delete_me_topic.first_post, context: "Deleted by admin").destroy delete "/admin/users/#{delete_me.id}.json" expect(response.status).to eq(403) json = response.parsed_body expect(json["deleted"]).to eq(false) expect(json["message"]).to eq( I18n.t("user.cannot_delete_has_posts", username: delete_me.username, count: post_count), ) end it "doesn't return an error if delete_posts == true" do delete "/admin/users/#{delete_me.id}.json", params: { delete_posts: true } expect(response.status).to eq(200) expect(Post.where(id: post.id).count).to eq(0) expect(Topic.where(id: topic.id).count).to eq(0) expect(User.where(id: delete_me.id).count).to eq(0) end context "when user has reviewable flagged post which was handled" do let!(:reviewable) do Fabricate( :reviewable_flagged_post, created_by: admin, target_created_by: delete_me, target: post, topic: topic, status: 4, ) end it "deletes the user record" do delete "/admin/users/#{delete_me.id}.json", params: { delete_posts: true, delete_as_spammer: true, } expect(response.status).to eq(200) expect(User.where(id: delete_me.id).count).to eq(0) end end end it "blocks the e-mail if block_email param is is true" do user_emails = delete_me.user_emails.pluck(:email) delete "/admin/users/#{delete_me.id}.json", params: { block_email: true } expect(response.status).to eq(200) expect(ScreenedEmail.exists?(email: user_emails)).to eq(true) end it "does not block the e-mails if block_email param is is false" do user_emails = delete_me.user_emails.pluck(:email) delete "/admin/users/#{delete_me.id}.json", params: { block_email: false } expect(response.status).to eq(200) expect(ScreenedEmail.exists?(email: user_emails)).to eq(false) end it "does not block the e-mails by default" do user_emails = delete_me.user_emails.pluck(:email) delete "/admin/users/#{delete_me.id}.json" expect(response.status).to eq(200) expect(ScreenedEmail.exists?(email: user_emails)).to eq(false) end it "blocks the ip address if block_ip param is true" do ip_address = delete_me.ip_address delete "/admin/users/#{delete_me.id}.json", params: { block_ip: true } expect(response.status).to eq(200) expect(ScreenedIpAddress.exists?(ip_address: ip_address)).to eq(true) end it "does not block the ip address if block_ip param is false" do ip_address = delete_me.ip_address delete "/admin/users/#{delete_me.id}.json", params: { block_ip: false } expect(response.status).to eq(200) expect(ScreenedIpAddress.exists?(ip_address: ip_address)).to eq(false) end it "does not block the ip address by default" do ip_address = delete_me.ip_address delete "/admin/users/#{delete_me.id}.json" expect(response.status).to eq(200) expect(ScreenedIpAddress.exists?(ip_address: ip_address)).to eq(false) end context "with param block_url" do before do @post = Fabricate(:post_with_external_links, user: delete_me) TopicLink.extract_from(@post) @urls = TopicLink .where(user: delete_me, internal: false) .pluck(:url) .map { |url| ScreenedUrl.normalize_url(url) } end it "blocks the urls if block_url param is true" do delete "/admin/users/#{delete_me.id}.json", params: { delete_posts: true, block_urls: true, } expect(response.status).to eq(200) expect(ScreenedUrl.exists?(url: @urls)).to eq(true) end it "does not block the urls if block_url param is false" do delete "/admin/users/#{delete_me.id}.json", params: { delete_posts: true, block_urls: false, } expect(response.status).to eq(200) expect(ScreenedUrl.exists?(url: @urls)).to eq(false) end it "does not block the urls by default" do delete "/admin/users/#{delete_me.id}.json", params: { delete_posts: true } expect(response.status).to eq(200) expect(ScreenedUrl.exists?(url: @urls)).to eq(false) end end it "deletes the user record" do delete "/admin/users/#{delete_me.id}.json" expect(response.status).to eq(200) expect(User.where(id: delete_me.id).count).to eq(0) end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "user deletion possible" end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "user deletion possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents deleting user with a 404 response" do delete "/admin/users/#{delete_me.id}.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(User.where(id: delete_me.id).count).to eq(1) end end end describe "#destroy_bulk" do fab!(:deleted_users) { Fabricate.times(3, :user) } shared_examples "bulk user deletion possible" do before { sign_in(current_user) } it "can delete multiple users" do delete "/admin/users/destroy-bulk.json", params: { user_ids: deleted_users.map(&:id) } expect(response.status).to eq(200) expect(User.where(id: deleted_users.map(&:id)).count).to eq(0) end it "responds with 404 when sending an empty user_ids list" do delete "/admin/users/destroy-bulk.json", params: { user_ids: [] } expect(response.status).to eq(404) end it "doesn't allow deleting a user that can't be deleted" do deleted_users[0].update!(admin: true) delete "/admin/users/destroy-bulk.json", params: { user_ids: deleted_users.map(&:id) } expect(response.status).to eq(403) expect(User.where(id: deleted_users.map(&:id)).count).to eq(3) end it "doesn't accept more than 100 user ids" do delete "/admin/users/destroy-bulk.json", params: { user_ids: deleted_users.map(&:id) + (1..101).to_a, } expect(response.status).to eq(400) expect(User.where(id: deleted_users.map(&:id)).count).to eq(3) end it "doesn't fail when a user id doesn't exist" do user_id = (User.unscoped.maximum(:id) || 0) + 1 delete "/admin/users/destroy-bulk.json", params: { user_ids: deleted_users.map(&:id).push(user_id), } expect(response.status).to eq(200) expect(User.where(id: deleted_users.map(&:id)).count).to eq(0) end it "blocks emails and IPs of deleted users if block_ip_and_email is true" do current_user.update!(ip_address: IPAddr.new("127.189.34.11")) deleted_users[0].update!(ip_address: IPAddr.new("127.189.34.11")) deleted_users[1].update!(ip_address: IPAddr.new("249.21.44.3")) deleted_users[2].update!(ip_address: IPAddr.new("3.1.22.88")) expect do delete "/admin/users/destroy-bulk.json", params: { user_ids: deleted_users.map(&:id), block_ip_and_email: true, } end.to change { ScreenedIpAddress.where(action_type: ScreenedIpAddress.actions[:block]).count }.by(2).and change { ScreenedEmail.where(action_type: ScreenedEmail.actions[:block]).count }.by(3) expect( ScreenedIpAddress.exists?( ip_address: "249.21.44.3", action_type: ScreenedIpAddress.actions[:block], ), ).to be_truthy expect( ScreenedIpAddress.exists?( ip_address: "3.1.22.88", action_type: ScreenedIpAddress.actions[:block], ), ).to be_truthy expect(ScreenedIpAddress.exists?(ip_address: current_user.ip_address)).to be_falsey expect( ScreenedEmail.exists?( email: deleted_users[0].email, action_type: ScreenedEmail.actions[:block], ), ).to be_truthy expect( ScreenedEmail.exists?( email: deleted_users[1].email, action_type: ScreenedEmail.actions[:block], ), ).to be_truthy expect( ScreenedEmail.exists?( email: deleted_users[2].email, action_type: ScreenedEmail.actions[:block], ), ).to be_truthy expect(response.status).to eq(200) expect(User.where(id: deleted_users.map(&:id)).count).to eq(0) end it "doesn't block emails and IPs of deleted users if block_ip_and_email is false" do expect do delete "/admin/users/destroy-bulk.json", params: { user_ids: deleted_users.map(&:id), block_ip_and_email: false, } end.to not_change { ScreenedIpAddress.where(action_type: ScreenedIpAddress.actions[:block]).count }.and not_change { ScreenedEmail.where(action_type: ScreenedEmail.actions[:block]).count } expect(response.status).to eq(200) expect(User.where(id: deleted_users.map(&:id)).count).to eq(0) end end context "when logged in as an admin" do include_examples "bulk user deletion possible" do let(:current_user) { admin } end end context "when logged in as a moderator" do include_examples "bulk user deletion possible" do let(:current_user) { moderator } end end context "when logged in as a non-staff user" do before { sign_in(user) } it "responds with a 404 and doesn't delete users" do delete "/admin/users/destroy-bulk.json", params: { user_ids: deleted_users.map(&:id) } expect(response.status).to eq(404) expect(User.where(id: deleted_users.map(&:id)).count).to eq(3) end end end describe "#activate" do fab!(:reg_user) { Fabricate(:inactive_user) } shared_examples "user activation possible" do it "returns success" do put "/admin/users/#{reg_user.id}/activate.json" expect(response.status).to eq(200) json = response.parsed_body expect(json["success"]).to eq("OK") reg_user.reload expect(reg_user.active).to eq(true) end it "should confirm email even when the tokens are expired" do reg_user.email_tokens.update_all(confirmed: false, expired: true) reg_user.reload expect(reg_user.email_confirmed?).to eq(false) put "/admin/users/#{reg_user.id}/activate.json" expect(response.status).to eq(200) reg_user.reload expect(reg_user.email_confirmed?).to eq(true) end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "user activation possible" end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "user activation possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents activation of user with a 404 response" do put "/admin/users/#{reg_user.id}/activate.json" reg_user.reload expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(reg_user.active).to eq(false) end end end describe "#deactivate" do fab!(:reg_user) { Fabricate(:active_user) } shared_examples "user deactivation possible" do it "returns success" do put "/admin/users/#{reg_user.id}/deactivate.json" expect(response.status).to eq(200) json = response.parsed_body expect(json["success"]).to eq("OK") reg_user.reload expect(reg_user.active).to eq(false) end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "user deactivation possible" end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "user deactivation possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents deactivation of user with a 404 response" do put "/admin/users/#{reg_user.id}/deactivate.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) reg_user.reload expect(reg_user.active).to eq(true) end end end describe "#log_out" do fab!(:reg_user) { Fabricate(:user) } context "when logged in as an admin" do before { sign_in(admin) } it "returns success" do post "/admin/users/#{reg_user.id}/log_out.json" expect(response.status).to eq(200) json = response.parsed_body expect(json["success"]).to eq("OK") end it "returns 404 when user_id does not exist" do post "/admin/users/123123drink/log_out.json" expect(response.status).to eq(404) end end shared_examples "user log out not allowed" do it "prevents logging out of user with a 404 response" do post "/admin/users/#{reg_user.id}/log_out.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "user log out not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "user log out not allowed" end end describe "#silence" do fab!(:reg_user) { Fabricate(:user) } fab!(:other_user) { Fabricate(:user) } context "when logged in as an admin" do before { sign_in(admin) } it "returns a 404 if the user doesn't exist" do put "/admin/users/123123/silence.json" expect(response.status).to eq(404) end it "doesn't allow silencing another admin" do put "/admin/users/#{another_admin.id}/silence.json", params: { reason: "because reasons", silenced_till: 6.hours.from_now, } expect(response.status).to eq(403) expect(another_admin.reload).to_not be_silenced end it "doesn't allow silencing another admin via other_user_ids" do put "/admin/users/#{reg_user.id}/silence.json", params: { other_user_ids: [another_admin.id], reason: "because reasons", silenced_till: 6.hours.from_now, } expect(response.status).to eq(403) expect(another_admin.reload).to_not be_silenced expect(reg_user.reload).to_not be_silenced end it "punishes the user for spamming" do put "/admin/users/#{reg_user.id}/silence.json", params: { reason: "because reasons", silenced_till: 7.hours.from_now, } expect(response.status).to eq(200) reg_user.reload expect(reg_user).to be_silenced expect(reg_user.silenced_record).to be_present end it "can have an associated post" do silence_post = Fabricate(:post, user: reg_user) put "/admin/users/#{reg_user.id}/silence.json", params: { reason: "because reasons", silenced_till: 7.hours.from_now, post_id: silence_post.id, post_action: "edit", post_edit: "this is the new contents for the post", } expect(response.status).to eq(200) silence_post.reload expect(silence_post.raw).to eq("this is the new contents for the post") log = UserHistory.where( target_user_id: reg_user.id, action: UserHistory.actions[:silence_user], ).first expect(log).to be_present expect(log.post_id).to eq(silence_post.id) reg_user.reload expect(reg_user).to be_silenced end it "will set a length of time if provided" do future_date = 1.month.from_now.to_date put "/admin/users/#{reg_user.id}/silence.json", params: { reason: "because reasons", silenced_till: future_date, } expect(response.status).to eq(200) reg_user.reload expect(reg_user).to be_silenced expect(reg_user.silenced_till).to eq(future_date) end it "will send a message if provided" do expect do put "/admin/users/#{reg_user.id}/silence.json", params: { reason: "none of your biz", silenced_till: 666.hours.from_now, message: "Email this to the user", } end.to change { Jobs::CriticalUserEmail.jobs.size }.by(1) expect(response.status).to eq(200) reg_user.reload expect(reg_user).to be_silenced end it "checks if user is silenced" do put "/admin/users/#{user.id}/silence.json", params: { silenced_till: 5.hours.from_now, reason: "because I said so", } put "/admin/users/#{user.id}/silence.json", params: { silenced_till: 5.hours.from_now, reason: "because I said so too", } expect(response.status).to eq(409) expect(response.parsed_body["message"]).to eq( I18n.t( "user.already_silenced", staff: admin.username, time_ago: AgeWords.time_ago_in_words( user.silenced_record.created_at, true, scope: :"datetime.distance_in_words_verbose", ), ), ) end it "can silence multiple users" do put "/admin/users/#{reg_user.id}/silence.json", params: { reason: "because I want to", silenced_till: 14.hours.from_now, other_user_ids: [other_user.id], } expect(response.status).to eq(200) expect(reg_user.reload).to be_silenced expect(other_user.reload).to be_silenced end it "fails the request if the reason is too long" do expect(user).not_to be_silenced put "/admin/users/#{user.id}/silence.json", params: { reason: "x" * 301, silenced_till: 5.hours.from_now, } expect(response.status).to eq(400) user.reload expect(user).not_to be_suspended end it "fails the request if other_user_ids is too big" do another_user = Fabricate(:user) other_user_ids = [another_user.id] other_user_ids.push(*(1..304).to_a) put "/admin/users/#{user.id}/silence.json", params: { reason: "because I said so", silenced_till: 5.hours.from_now, other_user_ids:, } expect(response.status).to eq(400) user.reload expect(user).not_to be_silenced another_user.reload expect(another_user).not_to be_silenced end end context "when logged in as a moderator" do before { sign_in(moderator) } it "silences user" do put "/admin/users/#{reg_user.id}/silence.json", params: { reason: "cuz I wanna", silenced_till: 66.hours.from_now, } expect(response.status).to eq(200) reg_user.reload expect(reg_user).to be_silenced expect(reg_user.silenced_record).to be_present end it "doesn't allow silencing another admin" do put "/admin/users/#{another_admin.id}/silence.json", params: { reason: "because reasons", silenced_till: 3.hours.from_now, } expect(response.status).to eq(403) expect(another_admin.reload).to_not be_silenced end it "doesn't allow silencing another admin via other_user_ids" do put "/admin/users/#{reg_user.id}/silence.json", params: { other_user_ids: [another_admin.id], reason: "because reasons", silenced_till: 3.hours.from_now, } expect(response.status).to eq(403) expect(another_admin.reload).to_not be_silenced expect(reg_user.reload).to_not be_silenced end end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents silencing user with a 404 response" do put "/admin/users/#{reg_user.id}/silence.json" reg_user.reload expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(reg_user).not_to be_silenced end end end describe "#unsilence" do fab!(:reg_user) { Fabricate(:user, silenced_till: 10.years.from_now) } shared_examples "unsilencing user possible" do it "returns a 403 if the user doesn't exist" do put "/admin/users/123123/unsilence.json" expect(response.status).to eq(404) end it "unsilences the user" do put "/admin/users/#{reg_user.id}/unsilence.json" expect(response.status).to eq(200) reg_user.reload expect(reg_user.silenced?).to eq(false) log = UserHistory.where( target_user_id: reg_user.id, action: UserHistory.actions[:unsilence_user], ).first expect(log).to be_present end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "unsilencing user possible" end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "unsilencing user possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents unsilencing user with a 404 response" do put "/admin/users/#{reg_user.id}/unsilence.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end end describe "#ip_info" do shared_examples "IP info retrieval possible" do it "retrieves IP info" do ip = "81.2.69.142" DiscourseIpInfo.open_db(File.join(Rails.root, "spec", "fixtures", "mmdb")) Resolv::DNS.any_instance.stubs(:getname).with(ip).returns("ip-81-2-69-142.example.com") get "/admin/users/ip-info.json", params: { ip: ip } expect(response.status).to eq(200) expect(response.parsed_body.symbolize_keys).to eq( city: "London", country: "United Kingdom", country_code: "GB", geoname_ids: [6_255_148, 2_635_167, 2_643_743, 6_269_131], hostname: "ip-81-2-69-142.example.com", location: "London, England, United Kingdom", region: "England", latitude: 51.5142, longitude: -0.0931, ) end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "IP info retrieval possible" end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "IP info retrieval possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents retrieval of IP info with a 404 response" do ip = "81.2.69.142" DiscourseIpInfo.open_db(File.join(Rails.root, "spec", "fixtures", "mmdb")) Resolv::DNS.any_instance.stubs(:getname).with(ip).returns("ip-81-2-69-142.example.com") get "/admin/users/ip-info.json", params: { ip: ip } expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end end describe "#delete_other_accounts_with_same_ip" do shared_examples "deleting other accounts with same ip possible" do it "works" do user_a = Fabricate(:user, ip_address: "42.42.42.42") user_b = Fabricate(:user, ip_address: "42.42.42.42") delete "/admin/users/delete-others-with-same-ip.json", params: { ip: "42.42.42.42", exclude: -1, order: "trust_level DESC", } expect(response.status).to eq(200) expect(User.where(id: user_a.id).count).to eq(0) expect(User.where(id: user_b.id).count).to eq(0) end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "deleting other accounts with same ip possible" end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "deleting other accounts with same ip possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents deletion of other accounts with same ip with a 404 response" do user_a = Fabricate(:user, ip_address: "42.42.42.42") user_b = Fabricate(:user, ip_address: "42.42.42.42") delete "/admin/users/delete-others-with-same-ip.json", params: { ip: "42.42.42.42", exclude: -1, order: "trust_level DESC", } expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(User.where(id: user_a.id).count).to eq(1) expect(User.where(id: user_b.id).count).to eq(1) end end end describe "#sync_sso" do let(:sso) { DiscourseConnectBase.new } let(:sso_secret) { "sso secret" } before do SiteSetting.email_editable = false SiteSetting.discourse_connect_url = "https://www.example.com/sso" SiteSetting.enable_discourse_connect = true SiteSetting.auth_overrides_email = true SiteSetting.auth_overrides_name = true SiteSetting.auth_overrides_username = true SiteSetting.discourse_connect_secret = sso_secret sso.sso_secret = sso_secret end context "when logged in as an admin" do before { sign_in(admin) } it "can sync up with the sso" do sso.name = "Bob The Bob" sso.username = "bob" sso.email = "bob@bob.com" sso.external_id = "1" user = DiscourseConnect.parse( sso.payload, secure_session: read_secure_session, ).lookup_or_create_user sso.name = "Bill" sso.username = "Hokli$$!!" sso.email = "bob2@bob.com" post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) expect(response.status).to eq(200) user.reload expect(user.email).to eq("bob2@bob.com") expect(user.name).to eq("Bill") expect(user.username).to eq("Hokli") end it "can sync up with the sso without email" do sso.name = "Bob The Bob" sso.username = "bob" sso.email = "bob@bob.com" sso.external_id = "1" user = DiscourseConnect.parse( sso.payload, secure_session: read_secure_session, ).lookup_or_create_user sso.name = "Bill" sso.username = "Hokli$$!!" sso.email = nil post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) expect(response.status).to eq(200) end it "should create new users" do sso.name = "Dr. Claw" sso.username = "dr_claw" sso.email = "dr@claw.com" sso.external_id = "2" post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) expect(response.status).to eq(200) user = User.find_by_email("dr@claw.com") expect(user).to be_present expect(user.ip_address).to be_blank end it "triggers :sync_sso DiscourseEvent" do sso.name = "Bob The Bob" sso.username = "bob" sso.email = "bob@bob.com" sso.external_id = "1" user = DiscourseConnect.parse( sso.payload, secure_session: read_secure_session, ).lookup_or_create_user sso.name = "Bill" sso.username = "Hokli$$!!" sso.email = "bob2@bob.com" events = DiscourseEvent.track_events do post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) end expect(events).to include(event_name: :sync_sso, params: [user]) end it "should return the right message if the record is invalid" do sso.email = "" sso.name = "" sso.external_id = "1" post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) expect(response.status).to eq(403) expect(response.parsed_body["message"]).to include("Primary email can't be blank") end it "should return the right message if the signature is invalid" do sso.name = "Dr. Claw" sso.username = "dr_claw" sso.email = "dr@claw.com" sso.external_id = "2" correct_payload = Rack::Utils.parse_query(sso.payload) post "/admin/users/sync_sso.json", params: correct_payload.merge(sig: "someincorrectsignature") expect(response.status).to eq(422) expect(response.parsed_body["message"]).to include(I18n.t("discourse_connect.login_error")) expect(response.parsed_body["message"]).not_to include(correct_payload["sig"]) end it "returns 404 if the external id does not exist" do sso.name = "Dr. Claw" sso.username = "dr_claw" sso.email = "dr@claw.com" sso.external_id = "" post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) expect(response.status).to eq(422) expect(response.parsed_body["message"]).to include( I18n.t("discourse_connect.blank_id_error"), ) end end shared_examples "sso sync not allowed" do it "prevents sso sync with a 404 response" do sso.name = "Bob The Bob" sso.username = "bob" sso.email = "bob@bob.com" sso.external_id = "1" user = DiscourseConnect.parse( sso.payload, secure_session: read_secure_session, ).lookup_or_create_user sso.name = "Bill" sso.username = "Hokli$$!!" sso.email = "bob2@bob.com" post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) user.reload expect(user.email).to eq("bob@bob.com") expect(user.name).to eq("Bob The Bob") expect(user.username).to eq("bob") end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "sso sync not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "sso sync not allowed" end end describe "#disable_second_factor" do let(:second_factor) { user.create_totp(enabled: true) } let(:second_factor_backup) { user.generate_backup_codes } let(:security_key) { Fabricate(:user_security_key, user: user) } before do second_factor second_factor_backup security_key end context "when logged in as an admin" do before do sign_in(admin) expect(user.reload.user_second_factors.totps.first).to eq(second_factor) end it "should able to disable the second factor for another user" do expect do put "/admin/users/#{user.id}/disable_second_factor.json" end.to change { Jobs::CriticalUserEmail.jobs.length }.by(1) expect(response.status).to eq(200) expect(user.reload.user_second_factors).to be_empty expect(user.reload.security_keys).to be_empty job_args = Jobs::CriticalUserEmail.jobs.first["args"].first expect(job_args["user_id"]).to eq(user.id) expect(job_args["type"]).to eq("account_second_factor_disabled") end it "should not be able to disable the second factor for the current user" do put "/admin/users/#{admin.id}/disable_second_factor.json" expect(response.status).to eq(403) end describe "when user has only one second factor type enabled" do it "should succeed with security keys" do user.user_second_factors.destroy_all put "/admin/users/#{user.id}/disable_second_factor.json" expect(response.status).to eq(200) end it "should succeed with totp" do user.security_keys.destroy_all put "/admin/users/#{user.id}/disable_second_factor.json" expect(response.status).to eq(200) end end describe "when user does not have second factor enabled" do it "should raise the right error" do user.user_second_factors.destroy_all user.security_keys.destroy_all put "/admin/users/#{user.id}/disable_second_factor.json" expect(response.status).to eq(400) end end end context "when logged in as a moderator" do before { sign_in(moderator) } it "prevents disabling the second factor with a 403 response" do expect do put "/admin/users/#{user.id}/disable_second_factor.json" end.not_to change { Jobs::CriticalUserEmail.jobs.length } expect(response.status).to eq(403) expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) expect(user.reload.user_second_factors).not_to be_empty expect(user.reload.security_keys).not_to be_empty end end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents disabling the second factor with a 403 response" do expect do put "/admin/users/#{user.id}/disable_second_factor.json" end.not_to change { Jobs::CriticalUserEmail.jobs.length } expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(user.reload.user_second_factors).not_to be_empty expect(user.reload.security_keys).not_to be_empty end end end describe "#penalty_history" do let(:logger) { StaffActionLogger.new(admin) } context "when logged in as an admin" do before { sign_in(admin) } def find_logs(action) UserHistory.where(target_user_id: user.id, action: UserHistory.actions[action]) end it "allows admins to clear a user's history" do logger.log_user_suspend(user, "suspend reason") logger.log_user_unsuspend(user) logger.log_unsilence_user(user) logger.log_silence_user(user) delete "/admin/users/#{user.id}/penalty_history.json" expect(response.code).to eq("200") expect(find_logs(:suspend_user)).to be_blank expect(find_logs(:unsuspend_user)).to be_blank expect(find_logs(:silence_user)).to be_blank expect(find_logs(:unsilence_user)).to be_blank expect(find_logs(:removed_suspend_user)).to be_present expect(find_logs(:removed_unsuspend_user)).to be_present expect(find_logs(:removed_silence_user)).to be_present expect(find_logs(:removed_unsilence_user)).to be_present end end shared_examples "penalty history deletion not allowed" do it "prevents clearing of a user's penalty history with a 404 response" do delete "/admin/users/#{user.id}/penalty_history.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "penalty history deletion not allowed" end context "when logged in as a non-staff user" do before { sign_in(user) } include_examples "penalty history deletion not allowed" end end describe "#delete_posts_batch" do shared_examples "post batch deletion possible" do context "when user is is invalid" do it "should return the right response" do put "/admin/users/nothing/delete_posts_batch.json" expect(response.status).to eq(404) end end context "when there are user posts" do before do post = Fabricate(:post, user: user) Fabricate(:post, topic: post.topic, user: user) Fabricate(:post, user: user) end it "returns how many posts were deleted" do put "/admin/users/#{user.id}/delete_posts_batch.json" expect(response.status).to eq(200) expect(response.parsed_body["posts_deleted"]).to eq(3) end end context "when there are no posts left to be deleted" do it "returns correct json" do put "/admin/users/#{user.id}/delete_posts_batch.json" expect(response.status).to eq(200) expect(response.parsed_body["posts_deleted"]).to eq(0) end end end context "when logged in as an admin" do before { sign_in(admin) } include_examples "post batch deletion possible" end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "post batch deletion possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents batch deletion of posts with a 404 response" do put "/admin/users/#{user.id}/delete_posts_batch.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(response.parsed_body["posts_deleted"]).to be_nil end end end describe "#merge" do fab!(:target_user) { Fabricate(:user) } fab!(:topic) { Fabricate(:topic, user: user) } fab!(:first_post) { Fabricate(:post, topic: topic, user: user) } context "when logged in as an admin" do before { sign_in(admin) } it "should merge source user to target user" do Jobs.run_immediately! post "/admin/users/#{user.id}/merge.json", params: { target_username: target_user.username } expect(response.status).to eq(200) expect(topic.reload.user_id).to eq(target_user.id) expect(first_post.reload.user_id).to eq(target_user.id) end end context "when logged in as a moderator" do before { sign_in(moderator) } it "fails to merge source user to target user with 403 response" do Jobs.run_immediately! post "/admin/users/#{user.id}/merge.json", params: { target_username: target_user.username } expect(response.status).to eq(403) expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) expect(topic.reload.user_id).to eq(user.id) expect(first_post.reload.user_id).to eq(user.id) end end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents merging source user to target user with a 404 response" do Jobs.run_immediately! post "/admin/users/#{user.id}/merge.json", params: { target_username: target_user.username } expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(topic.reload.user_id).to eq(user.id) expect(first_post.reload.user_id).to eq(user.id) end end end describe "#sso_record" do fab!(:sso_record) do SingleSignOnRecord.create!( user_id: user.id, external_id: "12345", external_email: user.email, last_payload: "", ) end before do SiteSetting.discourse_connect_url = "https://www.example.com/sso" SiteSetting.enable_discourse_connect = true end context "when logged in as an admin" do before { sign_in(admin) } it "deletes the record" do delete "/admin/users/#{user.id}/sso_record.json" expect(response.status).to eq(200) expect(user.single_sign_on_record).to eq(nil) end end context "when logged in as a moderator" do before { sign_in(moderator) } it "prevents deletion of sso record with a 403 response" do delete "/admin/users/#{user.id}/sso_record.json" expect(response.status).to eq(403) expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) expect(user.single_sign_on_record).to be_present end end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents deletion of sso record with a 404 response" do delete "/admin/users/#{user.id}/sso_record.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(user.single_sign_on_record).to be_present end end end describe "#delete_associated_accounts" do fab!(:user_associated_accounts) do UserAssociatedAccount.create!( provider_name: "github", provider_uid: "123456789", user_id: user.id, last_used: 1.seconds.ago, ) end context "when logged in as an admin" do before { sign_in(admin) } it "deletes the record and logs the deletion" do put "/admin/users/#{user.id}/delete_associated_accounts.json" expect(response.status).to eq(200) expect(user.user_associated_accounts).to eq([]) expect(UserHistory.last).to have_attributes( acting_user_id: admin.id, target_user_id: user.id, action: UserHistory.actions[:delete_associated_accounts], ) expect(UserHistory.last.previous_value).to include(':uid=>"123456789"') end end context "when logged in as a moderator" do before { sign_in(moderator) } it "prevents deletion of associated accounts with a 403 response" do put "/admin/users/#{user.id}/delete_associated_accounts.json" expect(response.status).to eq(403) expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) expect(user.user_associated_accounts).to be_present end end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents deletion of associated accounts with a 404 response" do put "/admin/users/#{user.id}/delete_associated_accounts.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(user.user_associated_accounts).to be_present end end end describe "#anonymize" do shared_examples "user anonymization possible" do it "will make the user anonymous" do put "/admin/users/#{user.id}/anonymize.json" expect(response.status).to eq(200) expect(response.parsed_body["username"]).to be_present end it "supports `anonymize_ip`" do Jobs.run_immediately! sl = Fabricate(:search_log, user_id: user.id) put "/admin/users/#{user.id}/anonymize.json?anonymize_ip=127.0.0.2" expect(response.status).to eq(200) expect(response.parsed_body["username"]).to be_present expect(sl.reload.ip_address).to eq("127.0.0.2") end end context "when logged in as admin" do before { sign_in(admin) } include_examples "user anonymization possible" end context "when logged in as a moderator" do before { sign_in(moderator) } include_examples "user anonymization possible" end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents anonymizing user with a 404 response" do put "/admin/users/#{user.id}/anonymize.json" expect(response.status).to eq(404) expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(response.parsed_body["username"]).to be_nil end end end describe "#reset_bounce_score" do before { user.user_stat.update!(bounce_score: 10) } context "when logged in as a moderator" do before { sign_in(moderator) } it "will reset the bounce score" do post "/admin/users/#{user.id}/reset-bounce-score.json" expect(response.status).to eq(200) expect(user.reload.user_stat.bounce_score).to eq(0) expect(UserHistory.last.action).to eq(UserHistory.actions[:reset_bounce_score]) end end context "when logged in as a non-staff user" do before { sign_in(user) } it "prevents resetting the bounce score with a 404 response" do post "/admin/users/#{user.id}/reset-bounce-score.json" expect(response.status).to eq(404) expect(user.reload.user_stat.bounce_score).to eq(10) end end end end