diff --git a/spec/requests/admin/admin_controller_spec.rb b/spec/requests/admin/admin_controller_spec.rb index 5b209798e58..41d067f36d4 100644 --- a/spec/requests/admin/admin_controller_spec.rb +++ b/spec/requests/admin/admin_controller_spec.rb @@ -1,19 +1,65 @@ # frozen_string_literal: true RSpec.describe Admin::AdminController do - describe '#index' do - it "needs you to be logged in" do - get "/admin.json" - expect(response.status).to eq(404) + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + + describe "#index" do + context "when unauthenticated" do + it "denies access with a 404 response" do + get "/admin.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end - it "should return the right response if user isn't a staff" do - sign_in(Fabricate(:user)) - get "/admin", params: { api_key: 'asdiasiduga' } - expect(response.status).to eq(404) + context "when authenticated" do + context "as an admin" do + it "permits access with a 200 response" do + sign_in(admin) + get "/admin.json" - get "/admin" - expect(response.status).to eq(404) + expect(response.status).to eq(200) + end + end + + context "as a non-admin" do + it "denies access with a 403 response" do + sign_in(moderator) + get "/admin.json" + + expect(response.status).to eq(403) + expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) + end + end + + context "when user is admin with api key" do + it "permits access with a 200 response" do + api_key = Fabricate(:api_key, user: admin) + + get "/admin.json", headers: { + HTTP_API_KEY: api_key.key, + HTTP_API_USERNAME: admin.username + } + + expect(response.status).to eq(200) + end + end + + context "when user is a non-admin with api key" do + it "denies access with a 403 response" do + api_key = Fabricate(:api_key, user: moderator) + + get "/admin.json", headers: { + HTTP_API_KEY: api_key.key, + HTTP_API_USERNAME: moderator.username + } + + expect(response.status).to eq(403) + expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) + end + end end end end diff --git a/spec/requests/admin/api_controller_spec.rb b/spec/requests/admin/api_controller_spec.rb index 454d3a5f813..ccc4db57410 100644 --- a/spec/requests/admin/api_controller_spec.rb +++ b/spec/requests/admin/api_controller_spec.rb @@ -1,47 +1,73 @@ # frozen_string_literal: true RSpec.describe Admin::ApiController do - - it "is a subclass of AdminController" do - expect(Admin::ApiController < Admin::AdminController).to eq(true) - end - fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } fab!(:key1, refind: false) { Fabricate(:api_key, description: "my key") } fab!(:key2, refind: false) { Fabricate(:api_key, user: admin) } fab!(:key3, refind: false) { Fabricate(:api_key, user: admin) } - context "as an admin" do - before do - sign_in(admin) - end + describe '#index' do + context "when logged in as an admin" do + before { sign_in(admin) } - describe '#index' do - it "succeeds" do + it "returns keys successfully" do get "/admin/api/keys.json" + expect(response.status).to eq(200) expect(response.parsed_body["keys"].length).to eq(3) end it "can paginate results" do get "/admin/api/keys.json?offset=0&limit=2" + expect(response.status).to eq(200) expect(response.parsed_body["keys"].map { |x| x["id"] }).to contain_exactly(key3.id, key2.id) get "/admin/api/keys.json?offset=1&limit=2" + expect(response.status).to eq(200) expect(response.parsed_body["keys"].map { |x| x["id"] }).to contain_exactly(key2.id, key1.id) get "/admin/api/keys.json?offset=2&limit=2" + expect(response.status).to eq(200) expect(response.parsed_body["keys"].map { |x| x["id"] }).to contain_exactly(key1.id) end end - describe '#show' do - it "succeeds" do + shared_examples "keys inaccessible" do + it "denies keys access with a 404 response" do + get "/admin/api/keys.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(response.parsed_body["keys"]).to be_nil + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "keys inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "keys inaccessible" + end + end + + describe '#show' do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "returns key successfully" do get "/admin/api/keys/#{key1.id}.json" + expect(response.status).to eq(200) data = response.parsed_body["key"] expect(data["id"]).to eq(key1.id) @@ -51,7 +77,33 @@ RSpec.describe Admin::ApiController do end end - describe '#update' do + shared_examples "key inaccessible" do + it "denies key access with a 404 response" do + get "/admin/api/keys/#{key1.id}.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(response.parsed_body["key"]).to be_nil + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "key inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "key inaccessible" + end + end + + describe '#update' do + context "when logged in as an admin" do + before { sign_in(admin) } + it "allows updating the description" do original_key = key1.key @@ -82,7 +134,44 @@ RSpec.describe Admin::ApiController do end end - describe "#destroy" do + shared_examples "key update not allowed" do + it "prevents key updates with a 404 response" do + key1.reload + original_key = key1.key + original_description = key1.description + + put "/admin/api/keys/#{key1.id}.json", params: { + key: { + description: "my new description", + key: "overridekey" + } + } + + key1.reload + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(key1.description).to eq(original_description) + expect(key1.key).to eq(original_key) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "key update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "key update not allowed" + end + end + + describe "#destroy" do + context "when logged in as an admin" do + before { sign_in(admin) } + it "works" do expect(ApiKey.exists?(key1.id)).to eq(true) @@ -96,7 +185,35 @@ RSpec.describe Admin::ApiController do end end - describe "#create" do + shared_examples "key deletion not allowed" do + it "prevents key deletion with a 404 response" do + expect(ApiKey.exists?(key1.id)).to eq(true) + + delete "/admin/api/keys/#{key1.id}.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(ApiKey.exists?(key1.id)).to eq(true) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "key deletion not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "key deletion not allowed" + end + end + + describe "#create" do + context "when logged in as an admin" do + before { sign_in(admin) } + it "can create a master key" do post "/admin/api/keys.json", params: { key: { @@ -207,7 +324,37 @@ RSpec.describe Admin::ApiController do end end - describe "#revoke and #undo_revoke" do + shared_examples "key creation not allowed" do + it "prevents key creation with a 404 response" do + post "/admin/api/keys.json", params: { + key: { + description: "master key description" + } + } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(response.parsed_body["key"]).to be_nil + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "key creation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "key creation not allowed" + end + end + + describe "#revoke and #undo_revoke" do + context "when logged in as an admin" do + before { sign_in(admin) } + it "works correctly" do post "/admin/api/keys/#{key1.id}/revoke.json" expect(response.status).to eq 200 @@ -229,37 +376,79 @@ RSpec.describe Admin::ApiController do end end - describe '#scopes' do - it 'includes scopes' do - get '/admin/api/keys/scopes.json' + shared_examples "key revocation/revocation undoing not allowed" do + it "prevents revoking/un-revoking key with a 404 response" do + key1.reload + post "/admin/api/keys/#{key1.id}/revoke.json" - scopes = response.parsed_body['scopes'] + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(key1.revoked_at).to eq(nil) - expect(scopes.keys).to contain_exactly('topics', 'users', 'email', 'posts', 'uploads', 'global', 'badges', 'categories', 'wordpress') + post "/admin/api/keys/#{key1.id}/undo-revoke.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(key1.revoked_at).to eq(nil) end end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "key revocation/revocation undoing not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "key revocation/revocation undoing not allowed" + end end - context "as a moderator" do - before do - sign_in(Fabricate(:moderator)) + describe "#scopes" do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "includes scopes" do + get "/admin/api/keys/scopes.json" + + scopes = response.parsed_body["scopes"] + + expect(scopes.keys).to contain_exactly( + "topics", + "users", + "email", + "posts", + "uploads", + "global", + "badges", + "categories", + "wordpress" + ) + end end - it "doesn't allow access" do - get "/admin/api/keys.json" - expect(response.status).to eq(404) + shared_examples "key scopes inaccessible" do + it "denies key scopes access with a 404 response" do + get "/admin/api/keys/scopes.json" - get "/admin/api/key/#{key1.id}.json" - expect(response.status).to eq(404) + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(response.parsed_body["scopes"]).to be_nil + end + end - post "/admin/api/keys.json", params: { - key: { - description: "master key description" - } - } - expect(response.status).to eq(404) + context "when logged in as a moderator" do + before { sign_in(moderator) } - expect(ApiKey.count).to eq(3) + include_examples "key scopes inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "key scopes inaccessible" end end end diff --git a/spec/requests/admin/backups_controller_spec.rb b/spec/requests/admin/backups_controller_spec.rb index 2f269bb9c84..069e6f1859a 100644 --- a/spec/requests/admin/backups_controller_spec.rb +++ b/spec/requests/admin/backups_controller_spec.rb @@ -2,6 +2,9 @@ RSpec.describe Admin::BackupsController do fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } + let(:backup_filename) { "2014-02-10-065935.tar.gz" } let(:backup_filename2) { "2014-02-11-065935.tar.gz" } @@ -23,12 +26,7 @@ RSpec.describe Admin::BackupsController do end.to_h end - it "is a subclass of AdminController" do - expect(Admin::BackupsController < Admin::AdminController).to eq(true) - end - before do - sign_in(admin) SiteSetting.backup_location = BackupLocationSiteSetting::LOCAL end @@ -40,201 +38,544 @@ RSpec.describe Admin::BackupsController do end describe "#index" do - it "raises an error when backups are disabled" do - SiteSetting.enable_backups = false - get "/admin/backups.json" - expect(response.status).to eq(403) + context "when logged in as an admin" do + before { sign_in(admin) } + + it "raises an error when backups are disabled" do + SiteSetting.enable_backups = false + get "/admin/backups.json" + expect(response.status).to eq(403) + end + + context "with html format" do + it "preloads important data" do + get "/admin/backups.html" + expect(response.status).to eq(200) + + preloaded = map_preloaded + expect(preloaded["operations_status"].symbolize_keys).to eq(BackupRestore.operations_status) + expect(preloaded["logs"].size).to eq(BackupRestore.logs.size) + end + end + + context "with json format" do + it "returns a list of all the backups" do + begin + create_backup_files(backup_filename, backup_filename2) + + get "/admin/backups.json" + expect(response.status).to eq(200) + + filenames = response.parsed_body.map { |backup| backup["filename"] } + expect(filenames).to include(backup_filename) + expect(filenames).to include(backup_filename2) + end + end + end end - context "with html format" do - it "preloads important data" do + shared_examples "backups inaccessible" do + it "denies access with a 404 response" do get "/admin/backups.html" + + expect(response.status).to eq(404) + + get "/admin/backups.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 "backups inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "backups inaccessible" + end + end + + describe '#status' do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "returns the current backups status" do + get "/admin/backups/status.json" + expect(response.body).to eq(BackupRestore.operations_status.to_json) + expect(response.status).to eq(200) + end + end + + shared_examples "status inaccessible" do + it "denies access with a 404 response" do + get "/admin/backups/status.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 "status inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "status inaccessible" + end + end + + describe '#create' do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "starts a backup" do + BackupRestore.expects(:backup!).with(admin.id, { publish_to_message_bus: true, with_uploads: false, client_id: "foo" }) + + post "/admin/backups.json", params: { + with_uploads: false, client_id: "foo" + } + + expect(response.status).to eq(200) + end + end + + shared_examples "backups creation not allowed" do + it "prevents backups creation with a 404 response" do + post "/admin/backups.json", params: { + with_uploads: false, + client_id: "foo" + } + + 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 "backups creation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "backups creation not allowed" + end + end + + describe '#show' do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "uses send_file to transmit the backup" do + begin + token = EmailBackupToken.set(admin.id) + create_backup_files(backup_filename) + + expect do + get "/admin/backups/#{backup_filename}.json", params: { token: token } + end.to change { UserHistory.where(action: UserHistory.actions[:backup_download]).count }.by(1) + + expect(response.headers['Content-Length']).to eq("11") + expect(response.headers['Content-Disposition']).to match(/attachment; filename/) + end + end + + it "returns 422 when token is bad" do + begin + get "/admin/backups/#{backup_filename}.json", params: { token: "bad_value" } + + expect(response.status).to eq(422) + expect(response.headers['Content-Disposition']).not_to match(/attachment; filename/) + expect(response.body).to include(I18n.t("download_backup_mailer.no_token")) + end + end + + it "returns 404 when the backup does not exist" do + token = EmailBackupToken.set(admin.id) + get "/admin/backups/#{backup_filename}.json", params: { token: token } + + expect(response.status).to eq(404) + end + end + + shared_examples "backup inaccessible" do + it "denies access with a 404 response" do + begin + token = EmailBackupToken.set(admin.id) + create_backup_files(backup_filename) + + expect do + get "/admin/backups/#{backup_filename}.json", params: { token: token } + end.not_to change { UserHistory.where(action: UserHistory.actions[:backup_download]).count } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(response.headers['Content-Disposition']).not_to match(/attachment; filename/) + end + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "backup inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "backup inaccessible" + end + end + + describe '#destroy' do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "removes the backup if found" do + begin + path = backup_path(backup_filename) + create_backup_files(backup_filename) + expect(File.exist?(path)).to eq(true) + + expect do + delete "/admin/backups/#{backup_filename}.json" + end.to change { UserHistory.where(action: UserHistory.actions[:backup_destroy]).count }.by(1) + + expect(response.status).to eq(200) + expect(File.exist?(path)).to eq(false) + end + end + + it "doesn't remove the backup if not found" do + delete "/admin/backups/#{backup_filename}.json" + expect(response.status).to eq(404) + end + end + + shared_examples "backup deletion not allowed" do + it "prevents deletion with a 404 response" do + begin + path = backup_path(backup_filename) + create_backup_files(backup_filename) + expect(File.exist?(path)).to eq(true) + + expect do + delete "/admin/backups/#{backup_filename}.json" + end.not_to change { UserHistory.where(action: UserHistory.actions[:backup_destroy]).count } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(File.exist?(path)).to eq(true) + end + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "backup deletion not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "backup deletion not allowed" + end + end + + describe '#logs' do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "preloads important data" do + get "/admin/backups/logs.html" expect(response.status).to eq(200) preloaded = map_preloaded + expect(preloaded["operations_status"].symbolize_keys).to eq(BackupRestore.operations_status) expect(preloaded["logs"].size).to eq(BackupRestore.logs.size) end end - context "with json format" do - it "returns a list of all the backups" do - begin - create_backup_files(backup_filename, backup_filename2) + shared_examples "backup logs inaccessible" do + it "denies access with a 404 response" do + get "/admin/backups/logs.html" - get "/admin/backups.json" - expect(response.status).to eq(200) - - filenames = response.parsed_body.map { |backup| backup["filename"] } - expect(filenames).to include(backup_filename) - expect(filenames).to include(backup_filename2) - end - end - end - end - - describe '#status' do - it "returns the current backups status" do - get "/admin/backups/status.json" - expect(response.body).to eq(BackupRestore.operations_status.to_json) - expect(response.status).to eq(200) - end - end - - describe '#create' do - it "starts a backup" do - BackupRestore.expects(:backup!).with(admin.id, { publish_to_message_bus: true, with_uploads: false, client_id: "foo" }) - - post "/admin/backups.json", params: { - with_uploads: false, client_id: "foo" - } - - expect(response.status).to eq(200) - end - end - - describe '#show' do - it "uses send_file to transmit the backup" do - begin - token = EmailBackupToken.set(admin.id) - create_backup_files(backup_filename) - - expect do - get "/admin/backups/#{backup_filename}.json", params: { token: token } - end.to change { UserHistory.where(action: UserHistory.actions[:backup_download]).count }.by(1) - - expect(response.headers['Content-Length']).to eq("11") - expect(response.headers['Content-Disposition']).to match(/attachment; filename/) + expect(response.status).to eq(404) end end - it "returns 422 when token is bad" do - begin - get "/admin/backups/#{backup_filename}.json", params: { token: "bad_value" } + context "when logged in as a moderator" do + before { sign_in(moderator) } - expect(response.status).to eq(422) - expect(response.headers['Content-Disposition']).not_to match(/attachment; filename/) - expect(response.body).to include(I18n.t("download_backup_mailer.no_token")) - end + include_examples "backup logs inaccessible" end - it "returns 404 when the backup does not exist" do - token = EmailBackupToken.set(admin.id) - get "/admin/backups/#{backup_filename}.json", params: { token: token } + context "when logged in as a non-staff user" do + before { sign_in(user) } - expect(response.status).to eq(404) - end - end - - describe '#destroy' do - it "removes the backup if found" do - begin - path = backup_path(backup_filename) - create_backup_files(backup_filename) - expect(File.exist?(path)).to eq(true) - - expect do - delete "/admin/backups/#{backup_filename}.json" - end.to change { UserHistory.where(action: UserHistory.actions[:backup_destroy]).count }.by(1) - - expect(response.status).to eq(200) - expect(File.exist?(path)).to eq(false) - end - end - - it "doesn't remove the backup if not found" do - delete "/admin/backups/#{backup_filename}.json" - expect(response.status).to eq(404) - end - end - - describe '#logs' do - it "preloads important data" do - get "/admin/backups/logs.html" - expect(response.status).to eq(200) - - preloaded = map_preloaded - - expect(preloaded["operations_status"].symbolize_keys).to eq(BackupRestore.operations_status) - expect(preloaded["logs"].size).to eq(BackupRestore.logs.size) + include_examples "backup logs inaccessible" end end describe '#restore' do - it "starts a restore" do - BackupRestore.expects(:restore!).with(admin.id, { filename: backup_filename, publish_to_message_bus: true, client_id: "foo" }) + context "when logged in as an admin" do + before { sign_in(admin) } - post "/admin/backups/#{backup_filename}/restore.json", params: { client_id: "foo" } + it "starts a restore" do + BackupRestore.expects(:restore!).with(admin.id, { filename: backup_filename, publish_to_message_bus: true, client_id: "foo" }) - expect(response.status).to eq(200) + post "/admin/backups/#{backup_filename}/restore.json", params: { client_id: "foo" } + + expect(response.status).to eq(200) + end + end + + shared_examples "backup restoration not allowed" do + it "prevents restoration with a 404 response" do + post "/admin/backups/#{backup_filename}/restore.json", params: { client_id: "foo" } + + 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 "backup restoration not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "backup restoration not allowed" end end describe '#readonly' do - it "enables readonly mode" do - expect(Discourse.readonly_mode?).to eq(false) + context "when logged in as an admin" do + before { sign_in(admin) } - expect { put "/admin/backups/readonly.json", params: { enable: true } } - .to change { UserHistory.where(action: UserHistory.actions[:change_readonly_mode], new_value: "t").count }.by(1) + it "enables readonly mode" do + expect(Discourse.readonly_mode?).to eq(false) - expect(Discourse.readonly_mode?).to eq(true) - expect(response.status).to eq(200) + expect { put "/admin/backups/readonly.json", params: { enable: true } } + .to change { UserHistory.where(action: UserHistory.actions[:change_readonly_mode], new_value: "t").count }.by(1) + + expect(Discourse.readonly_mode?).to eq(true) + expect(response.status).to eq(200) + end + + it "disables readonly mode" do + Discourse.enable_readonly_mode(Discourse::USER_READONLY_MODE_KEY) + expect(Discourse.readonly_mode?).to eq(true) + + expect { put "/admin/backups/readonly.json", params: { enable: false } } + .to change { UserHistory.where(action: UserHistory.actions[:change_readonly_mode], new_value: "f").count }.by(1) + + expect(response.status).to eq(200) + expect(Discourse.readonly_mode?).to eq(false) + end end - it "disables readonly mode" do - Discourse.enable_readonly_mode(Discourse::USER_READONLY_MODE_KEY) - expect(Discourse.readonly_mode?).to eq(true) + shared_examples "enabling readonly mode not allowed" do + it "prevents enabling readonly mode with a 404 response" do + expect(Discourse.readonly_mode?).to eq(false) - expect { put "/admin/backups/readonly.json", params: { enable: false } } - .to change { UserHistory.where(action: UserHistory.actions[:change_readonly_mode], new_value: "f").count }.by(1) + expect do + put "/admin/backups/readonly.json", params: { enable: true } + end.not_to change { UserHistory.where(action: UserHistory.actions[:change_readonly_mode], new_value: "t").count } - expect(response.status).to eq(200) - expect(Discourse.readonly_mode?).to eq(false) + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(Discourse.readonly_mode?).to eq(false) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "enabling readonly mode not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "enabling readonly mode not allowed" end end describe "#upload_backup_chunk" do - describe "when filename contains invalid characters" do - it "should raise an error" do - ['灰色.tar.gz', '; echo \'haha\'.tar.gz'].each do |invalid_filename| - described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) + context "when logged in as an admin" do + before { sign_in(admin) } - post "/admin/backups/upload", params: { - resumableFilename: invalid_filename, + describe "when filename contains invalid characters" do + it "should raise an error" do + ['灰色.tar.gz', '; echo \'haha\'.tar.gz'].each do |invalid_filename| + described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) + + post "/admin/backups/upload", params: { + resumableFilename: invalid_filename, + resumableTotalSize: 1, + resumableIdentifier: 'test' + } + + expect(response.status).to eq(415) + expect(response.body).to eq(I18n.t('backup.invalid_filename')) + end + end + end + + describe "when resumableIdentifier is invalid" do + it "should raise an error" do + filename = 'test_site-0123456789.tar.gz' + @paths = [backup_path(File.join('tmp', 'test', "#{filename}.part1"))] + + post "/admin/backups/upload.json", params: { + resumableFilename: filename, resumableTotalSize: 1, - resumableIdentifier: 'test' + resumableIdentifier: '../test', + resumableChunkNumber: '1', + resumableChunkSize: '1', + resumableCurrentChunkSize: '1', + file: fixture_file_upload(Tempfile.new) } - expect(response.status).to eq(415) - expect(response.body).to eq(I18n.t('backup.invalid_filename')) + expect(response.status).to eq(400) + end + end + + describe "when filename is valid" do + it "should upload the file successfully" do + freeze_time + described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) + + filename = 'test_Site-0123456789.tar.gz' + + post "/admin/backups/upload.json", params: { + resumableFilename: filename, + resumableTotalSize: 1, + resumableIdentifier: 'test', + resumableChunkNumber: '1', + resumableChunkSize: '1', + resumableCurrentChunkSize: '1', + file: fixture_file_upload(Tempfile.new) + } + expect_job_enqueued(job: :backup_chunks_merger, args: { + filename: filename, identifier: 'test', chunks: 1 + }, at: 5.seconds.from_now) + + expect(response.status).to eq(200) + expect(response.body).to eq("") + end + end + + describe "completing an upload by enqueuing backup_chunks_merger" do + let(:filename) { 'test_Site-0123456789.tar.gz' } + + it "works with a single chunk" do + freeze_time + described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) + + # 2MB file, 2MB chunks = 1x 2MB chunk + post "/admin/backups/upload.json", params: { + resumableFilename: filename, + resumableTotalSize: '2097152', + resumableIdentifier: 'test', + resumableChunkNumber: '1', + resumableChunkSize: '2097152', + resumableCurrentChunkSize: '2097152', + file: fixture_file_upload(Tempfile.new) + } + expect_job_enqueued(job: :backup_chunks_merger, args: { + filename: filename, identifier: 'test', chunks: 1 + }, at: 5.seconds.from_now) + end + + it "works with multiple chunks when the final chunk is chunk_size + remainder" do + freeze_time + described_class.any_instance.expects(:has_enough_space_on_disk?).twice.returns(true) + + # 5MB file, 2MB chunks = 1x 2MB chunk + 1x 3MB chunk with resumable.js + post "/admin/backups/upload.json", params: { + resumableFilename: filename, + resumableTotalSize: '5242880', + resumableIdentifier: 'test', + resumableChunkNumber: '1', + resumableChunkSize: '2097152', + resumableCurrentChunkSize: '2097152', + file: fixture_file_upload(Tempfile.new) + } + post "/admin/backups/upload.json", params: { + resumableFilename: filename, + resumableTotalSize: '5242880', + resumableIdentifier: 'test', + resumableChunkNumber: '2', + resumableChunkSize: '2097152', + resumableCurrentChunkSize: '3145728', + file: fixture_file_upload(Tempfile.new) + } + expect_job_enqueued(job: :backup_chunks_merger, args: { + filename: filename, identifier: 'test', chunks: 2 + }, at: 5.seconds.from_now) + end + + it "works with multiple chunks when the final chunk is just the remaninder" do + freeze_time + described_class.any_instance.expects(:has_enough_space_on_disk?).times(3).returns(true) + + # 5MB file, 2MB chunks = 2x 2MB chunk + 1x 1MB chunk with uppy.js + post "/admin/backups/upload.json", params: { + resumableFilename: filename, + resumableTotalSize: '5242880', + resumableIdentifier: 'test', + resumableChunkNumber: '1', + resumableChunkSize: '2097152', + resumableCurrentChunkSize: '2097152', + file: fixture_file_upload(Tempfile.new) + } + post "/admin/backups/upload.json", params: { + resumableFilename: filename, + resumableTotalSize: '5242880', + resumableIdentifier: 'test', + resumableChunkNumber: '2', + resumableChunkSize: '2097152', + resumableCurrentChunkSize: '2097152', + file: fixture_file_upload(Tempfile.new) + } + post "/admin/backups/upload.json", params: { + resumableFilename: filename, + resumableTotalSize: '5242880', + resumableIdentifier: 'test', + resumableChunkNumber: '3', + resumableChunkSize: '2097152', + resumableCurrentChunkSize: '1048576', + file: fixture_file_upload(Tempfile.new) + } + expect_job_enqueued(job: :backup_chunks_merger, args: { + filename: filename, identifier: 'test', chunks: 3 + }, at: 5.seconds.from_now) end end end - describe "when resumableIdentifier is invalid" do - it "should raise an error" do - filename = 'test_site-0123456789.tar.gz' - @paths = [backup_path(File.join('tmp', 'test', "#{filename}.part1"))] - - post "/admin/backups/upload.json", params: { - resumableFilename: filename, - resumableTotalSize: 1, - resumableIdentifier: '../test', - resumableChunkNumber: '1', - resumableChunkSize: '1', - resumableCurrentChunkSize: '1', - file: fixture_file_upload(Tempfile.new) - } - - expect(response.status).to eq(400) - end - end - - describe "when filename is valid" do - it "should upload the file successfully" do + shared_examples "uploading backup chunk not allowed" do + it "prevents uploading of backup chunk with a 404 response" do freeze_time - described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) - filename = 'test_Site-0123456789.tar.gz' post "/admin/backups/upload.json", params: { @@ -246,172 +587,206 @@ RSpec.describe Admin::BackupsController do resumableCurrentChunkSize: '1', file: fixture_file_upload(Tempfile.new) } - expect_job_enqueued(job: :backup_chunks_merger, args: { + + expect_not_enqueued_with(job: :backup_chunks_merger, args: { filename: filename, identifier: 'test', chunks: 1 }, at: 5.seconds.from_now) - expect(response.status).to eq(200) - expect(response.body).to eq("") + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end - describe "completing an upload by enqueuing backup_chunks_merger" do - let(:filename) { 'test_Site-0123456789.tar.gz' } + context "when logged in as a moderator" do + before { sign_in(moderator) } - it "works with a single chunk" do - freeze_time - described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) + include_examples "uploading backup chunk not allowed" + end - # 2MB file, 2MB chunks = 1x 2MB chunk - post "/admin/backups/upload.json", params: { - resumableFilename: filename, - resumableTotalSize: '2097152', - resumableIdentifier: 'test', - resumableChunkNumber: '1', - resumableChunkSize: '2097152', - resumableCurrentChunkSize: '2097152', - file: fixture_file_upload(Tempfile.new) - } - expect_job_enqueued(job: :backup_chunks_merger, args: { - filename: filename, identifier: 'test', chunks: 1 - }, at: 5.seconds.from_now) - end + context "when logged in as a non-staff user" do + before { sign_in(user) } - it "works with multiple chunks when the final chunk is chunk_size + remainder" do - freeze_time - described_class.any_instance.expects(:has_enough_space_on_disk?).twice.returns(true) - - # 5MB file, 2MB chunks = 1x 2MB chunk + 1x 3MB chunk with resumable.js - post "/admin/backups/upload.json", params: { - resumableFilename: filename, - resumableTotalSize: '5242880', - resumableIdentifier: 'test', - resumableChunkNumber: '1', - resumableChunkSize: '2097152', - resumableCurrentChunkSize: '2097152', - file: fixture_file_upload(Tempfile.new) - } - post "/admin/backups/upload.json", params: { - resumableFilename: filename, - resumableTotalSize: '5242880', - resumableIdentifier: 'test', - resumableChunkNumber: '2', - resumableChunkSize: '2097152', - resumableCurrentChunkSize: '3145728', - file: fixture_file_upload(Tempfile.new) - } - expect_job_enqueued(job: :backup_chunks_merger, args: { - filename: filename, identifier: 'test', chunks: 2 - }, at: 5.seconds.from_now) - end - - it "works with multiple chunks when the final chunk is just the remaninder" do - freeze_time - described_class.any_instance.expects(:has_enough_space_on_disk?).times(3).returns(true) - - # 5MB file, 2MB chunks = 2x 2MB chunk + 1x 1MB chunk with uppy.js - post "/admin/backups/upload.json", params: { - resumableFilename: filename, - resumableTotalSize: '5242880', - resumableIdentifier: 'test', - resumableChunkNumber: '1', - resumableChunkSize: '2097152', - resumableCurrentChunkSize: '2097152', - file: fixture_file_upload(Tempfile.new) - } - post "/admin/backups/upload.json", params: { - resumableFilename: filename, - resumableTotalSize: '5242880', - resumableIdentifier: 'test', - resumableChunkNumber: '2', - resumableChunkSize: '2097152', - resumableCurrentChunkSize: '2097152', - file: fixture_file_upload(Tempfile.new) - } - post "/admin/backups/upload.json", params: { - resumableFilename: filename, - resumableTotalSize: '5242880', - resumableIdentifier: 'test', - resumableChunkNumber: '3', - resumableChunkSize: '2097152', - resumableCurrentChunkSize: '1048576', - file: fixture_file_upload(Tempfile.new) - } - expect_job_enqueued(job: :backup_chunks_merger, args: { - filename: filename, identifier: 'test', chunks: 3 - }, at: 5.seconds.from_now) - end + include_examples "uploading backup chunk not allowed" end end describe "#check_backup_chunk" do - describe "when resumableIdentifier is invalid" do - it "should raise an error" do - get "/admin/backups/upload", params: { - resumableIdentifier: "../some_file", - resumableFilename: "test_site-0123456789.tar.gz", - resumableChunkNumber: '1', - resumableCurrentChunkSize: '1' - } + context "when logged in as an admin" do + before { sign_in(admin) } - expect(response.status).to eq(400) + describe "when resumableIdentifier is invalid" do + it "should raise an error" do + get "/admin/backups/upload", params: { + resumableidentifier: "../some_file", + resumablefilename: "test_site-0123456789.tar.gz", + resumablechunknumber: '1', + resumablecurrentchunksize: '1' + } + + expect(response.status).to eq(400) + end end end + + shared_examples "checking backup chunk not allowed" do + it "denies access with a 404 response" do + get "/admin/backups/upload", params: { + resumableidentifier: "../some_file", + resumablefilename: "test_site-0123456789.tar.gz", + resumablechunknumber: '1', + resumablecurrentchunksize: '1' + } + + expect(response.status).to eq(404) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "checking backup chunk not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "checking backup chunk not allowed" + end end describe '#rollback' do - it 'should rollback the restore' do - BackupRestore.expects(:rollback!) + context "when logged in as an admin" do + before { sign_in(admin) } - post "/admin/backups/rollback.json" + it 'should rollback the restore' do + BackupRestore.expects(:rollback!) - expect(response.status).to eq(200) + post "/admin/backups/rollback.json" + + expect(response.status).to eq(200) + end + + it 'should not allow rollback via a GET request' do + get "/admin/backups/rollback.json" + expect(response.status).to eq(404) + end end - it 'should not allow rollback via a GET request' do - get "/admin/backups/rollback.json" - expect(response.status).to eq(404) + shared_examples "backup rollback not allowed" do + it "prevents rollbacks with a 404 response" do + post "/admin/backups/rollback.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 "backup rollback not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "backup rollback not allowed" end end describe '#cancel' do - it "should cancel an backup" do - BackupRestore.expects(:cancel!) + context "when logged in as an admin" do + before { sign_in(admin) } - delete "/admin/backups/cancel.json" + it "should cancel an backup" do + BackupRestore.expects(:cancel!) - expect(response.status).to eq(200) + delete "/admin/backups/cancel.json" + + expect(response.status).to eq(200) + end + + it 'should not allow cancel via a GET request' do + get "/admin/backups/cancel.json" + expect(response.status).to eq(404) + end end - it 'should not allow cancel via a GET request' do - get "/admin/backups/cancel.json" - expect(response.status).to eq(404) + shared_examples "backup cancellation not allowed" do + it "prevents cancellation with a 404 response" do + delete "/admin/backups/cancel.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 "backup cancellation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "backup cancellation not allowed" end end describe "#email" do - it "enqueues email job" do + context "when logged in as an admin" do + before { sign_in(admin) } - # might as well test this here if we really want www.example.com - SiteSetting.force_hostname = "www.example.com" + it "enqueues email job" do - create_backup_files(backup_filename) + # might as well test this here if we really want www.example.com + SiteSetting.force_hostname = "www.example.com" - expect { + create_backup_files(backup_filename) + + expect { + put "/admin/backups/#{backup_filename}.json" + }.to change { Jobs::DownloadBackupEmail.jobs.size }.by(1) + + job_args = Jobs::DownloadBackupEmail.jobs.last["args"].first + expect(job_args["user_id"]).to eq(admin.id) + expect(job_args["backup_file_path"]).to eq("http://www.example.com/admin/backups/#{backup_filename}") + + expect(response.status).to eq(200) + end + + it "returns 404 when the backup does not exist" do put "/admin/backups/#{backup_filename}.json" - }.to change { Jobs::DownloadBackupEmail.jobs.size }.by(1) - job_args = Jobs::DownloadBackupEmail.jobs.last["args"].first - expect(job_args["user_id"]).to eq(admin.id) - expect(job_args["backup_file_path"]).to eq("http://www.example.com/admin/backups/#{backup_filename}") - - expect(response.status).to eq(200) + expect(response).to be_not_found + end end - it "returns 404 when the backup does not exist" do - put "/admin/backups/#{backup_filename}.json" + shared_examples "backup emails not allowed" do + it "prevents sending backup emails with a 404 response" do + SiteSetting.force_hostname = "www.example.com" + create_backup_files(backup_filename) - expect(response).to be_not_found + expect do + put "/admin/backups/#{backup_filename}.json" + end.not_to change { Jobs::DownloadBackupEmail.jobs.size } + + 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 "backup emails not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "backup emails not allowed" end end @@ -432,12 +807,8 @@ RSpec.describe Admin::BackupsController do ) end - context "when the user is not admin" do - before do - admin.update(admin: false) - end - - it "errors with invalid access error" do + shared_examples "multipart uploads not allowed" do + it "prevents multipart uploads with a 404 response" do post "/admin/backups/create-multipart.json", params: { file_name: "test.tar.gz", upload_type: upload_type, @@ -447,7 +818,21 @@ RSpec.describe Admin::BackupsController do end end + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "multipart uploads not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "multipart uploads not allowed" + end + context "when the user is admin" do + before { sign_in(admin) } + def stub_create_multipart_backup_request BackupRestore::S3BackupStore.any_instance.stubs(:temporary_upload_path).returns( "temp/default/#{test_bucket_prefix}/28fccf8259bbe75b873a2bd2564b778c/2u98j832nx93272x947823.gz" diff --git a/spec/requests/admin/badges_controller_spec.rb b/spec/requests/admin/badges_controller_spec.rb index 71368be5a36..96f4f9294b5 100644 --- a/spec/requests/admin/badges_controller_spec.rb +++ b/spec/requests/admin/badges_controller_spec.rb @@ -1,22 +1,47 @@ # frozen_string_literal: true RSpec.describe Admin::BadgesController do - context "while logged in as an admin" do - fab!(:admin) { Fabricate(:admin) } - fab!(:badge) { Fabricate(:badge) } + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user, email: 'user1@test.com', username: 'username1') } + fab!(:badge) { Fabricate(:badge) } - before do - sign_in(admin) - end + describe '#index' do + context "when logged in as an admin" do + before { sign_in(admin) } - describe '#index' do - it 'returns badge index' do + it "returns badge index" do get "/admin/badges.json" expect(response.status).to eq(200) end end - describe '#preview' do + shared_examples "badges inaccessible" do + it "denies access to badges with a 404 response" do + get "/admin/badges.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 "badges inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "badges inaccessible" + end + end + + describe '#preview' do + context "when logged in as an admin" do + before { sign_in(admin) } + it 'allows preview enable_badge_sql is enabled' do SiteSetting.enable_badge_sql = true @@ -39,7 +64,36 @@ RSpec.describe Admin::BadgesController do end end - describe '#create' do + shared_examples "badge preview inaccessible" do + it "denies access to badge preview with a 404 response" do + SiteSetting.enable_badge_sql = true + + post "/admin/badges/preview.json", params: { + sql: 'select id as user_id, created_at granted_at from users' + } + + 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 "badge preview inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "badge preview inaccessible" + end + end + + describe '#create' do + context "when logged in as an admin" do + before { sign_in(admin) } + it 'can create badges correctly' do SiteSetting.enable_badge_sql = true @@ -56,7 +110,36 @@ RSpec.describe Admin::BadgesController do end end - describe '#save_badge_groupings' do + shared_examples "badge creation not allowed" do + it "prevents badge creation with a 404 response" do + SiteSetting.enable_badge_sql = true + + post "/admin/badges.json", params: { + name: 'test', query: 'select 1 as user_id, null as granted_at', badge_type_id: 1 + } + + 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 "badge creation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "badge creation not allowed" + end + end + + describe '#save_badge_groupings' do + context "when logged in as an admin" do + before { sign_in(admin) } + it 'can save badge groupings' do groupings = BadgeGrouping.all.order(:position).to_a groupings << BadgeGrouping.new(name: 'Test 1') @@ -78,7 +161,41 @@ RSpec.describe Admin::BadgesController do end end - describe '#badge_types' do + shared_examples "badge grouping creation not allowed" do + it "prevents creation of badge groupings with a 404 response" do + groupings = BadgeGrouping.all.order(:position).to_a + groupings << BadgeGrouping.new(name: "Test 1") + groupings << BadgeGrouping.new(name: "Test 2") + + groupings.shuffle! + + names = groupings.map { |g| g.name } + ids = groupings.map { |g| g.id.to_s } + + post "/admin/badges/badge_groupings.json", params: { ids: ids, names: names } + + 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 "badge grouping creation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "badge grouping creation not allowed" + end + end + + describe '#badge_types' do + context "when logged in as an admin" do + before { sign_in(admin) } + it 'returns JSON' do get "/admin/badges/types.json" @@ -87,7 +204,32 @@ RSpec.describe Admin::BadgesController do end end - describe '#destroy' do + shared_examples "badge types inaccessible" do + it "denies access to badge types with a 404 response" do + get "/admin/badges/types.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 "badge types inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "badge types inaccessible" + end + end + + describe '#destroy' do + context "when logged in as an admin" do + before { sign_in(admin) } + it 'deletes the badge' do delete "/admin/badges/#{badge.id}.json" expect(response.status).to eq(200) @@ -96,7 +238,33 @@ RSpec.describe Admin::BadgesController do end end - describe '#update' do + shared_examples "badge deletion not allowed" do + it "prevents deletion of badges with a 404 response" do + delete "/admin/badges/#{badge.id}.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(Badge.where(id: badge.id).exists?).to eq(true) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "badge deletion not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "badge deletion not allowed" + end + end + + describe '#update' do + context "when logged in as an admin" do + before { sign_in(admin) } + it 'does not update the name of system badges' do editor_badge = Badge.find(Badge::Editor) editor_badge_name = editor_badge.name @@ -180,8 +348,49 @@ RSpec.describe Admin::BadgesController do end end - describe '#mass_award' do - fab!(:user) { Fabricate(:user, email: 'user1@test.com', username: 'username1') } + shared_examples "badge update not allowed" do + it "prevents badge update with a 404 response" do + SiteSetting.enable_badge_sql = true + + sql = "select id user_id, created_at granted_at from users" + image = Fabricate(:upload) + + put "/admin/badges/#{badge.id}.json", params: { + name: "123456", + query: sql, + badge_type_id: badge.badge_type_id, + allow_title: false, + multiple_grant: false, + enabled: true, + image_upload_id: image.id, + icon: "fa-rocket", + } + + badge.reload + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(badge.name).not_to eq('123456') + expect(badge.query).not_to eq(sql) + expect(badge.icon).not_to eq("fa-rocket") + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "badge update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "badge update not allowed" + end + end + + describe '#mass_award' do + context "when logged in as an admin" do + before { sign_in(admin) } it 'does nothing when there is no file' do post "/admin/badges/award/#{badge.id}.json", params: { file: '' } @@ -359,5 +568,29 @@ RSpec.describe Admin::BadgesController do end end end + + shared_examples "mass badge award not allowed" do + it "prevents mass badge award with a 404 response" do + file = file_from_fixtures('user_emails.csv', 'csv') + + post "/admin/badges/award/#{badge.id}.json", params: { file: fixture_file_upload(file) } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(UserBadge.where(user: user, badge: badge).count).to eq(0) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "mass badge award not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "mass badge award not allowed" + end end end diff --git a/spec/requests/admin/color_schemes_controller_spec.rb b/spec/requests/admin/color_schemes_controller_spec.rb index b45517bbf57..b5a539ac266 100644 --- a/spec/requests/admin/color_schemes_controller_spec.rb +++ b/spec/requests/admin/color_schemes_controller_spec.rb @@ -1,26 +1,23 @@ # frozen_string_literal: true RSpec.describe Admin::ColorSchemesController do - it "is a subclass of AdminController" do - expect(described_class < Admin::AdminController).to eq(true) - end + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } - context "while logged in as an admin" do - fab!(:admin) { Fabricate(:admin) } - let(:valid_params) { { color_scheme: { - name: 'Such Design', - colors: [ - { name: 'primary', hex: 'FFBB00' }, - { name: 'secondary', hex: '888888' } - ] - } - } } + let(:valid_params) { { color_scheme: { + name: 'Such Design', + colors: [ + { name: 'primary', hex: 'FFBB00' }, + { name: 'secondary', hex: '888888' } + ] + } + } } - before do - sign_in(admin) - end + describe "#index" do + context "when logged in as an admin" do + before { sign_in(admin) } - describe "#index" do it "returns JSON" do scheme_name = Fabricate(:color_scheme).name get "/admin/color_schemes.json" @@ -36,7 +33,32 @@ RSpec.describe Admin::ColorSchemesController do end end - describe "#create" do + shared_examples "color schemes inaccessible" do + it "denies access with a 404 response" do + get "/admin/color_schemes.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 "color schemes inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "color schemes inaccessible" + end + end + + describe "#create" do + context "when logged in as an admin" do + before { sign_in(admin) } + it "returns JSON" do post "/admin/color_schemes.json", params: valid_params @@ -55,8 +77,38 @@ RSpec.describe Admin::ColorSchemesController do end end - describe "#update" do - fab!(:existing) { Fabricate(:color_scheme) } + shared_examples "color scheme creation not allowed" do + it "prevents creation with a 404 response" do + params = valid_params + params[:color_scheme][:colors][0][:hex] = 'cool color please' + + expect do + post "/admin/color_schemes.json", params: valid_params + end.not_to change { ColorScheme.count } + + 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 "color scheme creation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "color scheme creation not allowed" + end + end + + describe "#update" do + fab!(:existing) { Fabricate(:color_scheme) } + + context "when logged in as an admin" do + before { sign_in(admin) } it "returns success" do put "/admin/color_schemes/#{existing.id}.json", params: valid_params @@ -84,8 +136,33 @@ RSpec.describe Admin::ColorSchemesController do end end - describe "#destroy" do - fab!(:existing) { Fabricate(:color_scheme) } + shared_examples "color scheme update not allowed" do + it "prevents update with a 404 response" do + put "/admin/color_schemes/#{existing.id}.json", params: valid_params + + 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 "color scheme update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "color scheme update not allowed" + end + end + + describe "#destroy" do + fab!(:existing) { Fabricate(:color_scheme) } + + context "when logged in as an admin" do + before { sign_in(admin) } it "returns success" do expect { @@ -94,5 +171,26 @@ RSpec.describe Admin::ColorSchemesController do expect(response.status).to eq(200) end end + + shared_examples "color scheme deletion not allowed" do + it "prevents deletion with a 404 response" do + delete "/admin/color_schemes/#{existing.id}.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 "color scheme deletion not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "color scheme deletion not allowed" + end end end diff --git a/spec/requests/admin/dashboard_controller_spec.rb b/spec/requests/admin/dashboard_controller_spec.rb index 6f9e34881a2..219a2855eab 100644 --- a/spec/requests/admin/dashboard_controller_spec.rb +++ b/spec/requests/admin/dashboard_controller_spec.rb @@ -1,60 +1,116 @@ # frozen_string_literal: true RSpec.describe Admin::DashboardController do + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } + before do AdminDashboardData.stubs(:fetch_cached_stats).returns(reports: []) Jobs::VersionCheck.any_instance.stubs(:execute).returns(true) end - it "is a subclass of StaffController" do - expect(Admin::DashboardController < Admin::StaffController).to eq(true) + def populate_new_features + sample_features = [ + { + "id" => "1", + "emoji" => "🤾", + "title" => "Cool Beans", + "description" => "Now beans are included", + "created_at" => Time.zone.now - 40.minutes + }, + { + "id" => "2", + "emoji" => "🙈", + "title" => "Fancy Legumes", + "description" => "Legumes too!", + "created_at" => Time.zone.now - 20.minutes + } + ] + + Discourse.redis.set('new_features', MultiJson.dump(sample_features)) end - context 'while logged in as an admin' do - fab!(:admin) { Fabricate(:admin) } + describe '#index' do + shared_examples "version info present" do + it "returns discourse version info" do + get "/admin/dashboard.json" - def populate_new_features - sample_features = [ - { "id" => "1", "emoji" => "🤾", "title" => "Cool Beans", "description" => "Now beans are included", "created_at" => Time.zone.now - 40.minutes }, - { "id" => "2", "emoji" => "🙈", "title" => "Fancy Legumes", "description" => "Legumes too!", "created_at" => Time.zone.now - 20.minutes } - ] - - Discourse.redis.set('new_features', MultiJson.dump(sample_features)) + expect(response.status).to eq(200) + expect(response.parsed_body["version_check"]).to be_present + end end - before do - sign_in(admin) + shared_examples "version info absent" do + before do + SiteSetting.version_checks = false + end + + it "does not return discourse version info" do + get "/admin/dashboard.json" + + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["version_check"]).not_to be_present + end end - describe '#index' do - context 'when version checking is enabled' do + context "when logged in as an admin" do + before { sign_in(admin) } + + context "when version checking is enabled" do before do SiteSetting.version_checks = true end - it 'returns discourse version info' do - get "/admin/dashboard.json" - - expect(response.status).to eq(200) - expect(response.parsed_body['version_check']).to be_present - end + include_examples "version info present" end - context 'when version checking is disabled' do + context "when version checking is disabled" do before do SiteSetting.version_checks = false end - it 'does not return discourse version info' do - get "/admin/dashboard.json" - expect(response.status).to eq(200) - json = response.parsed_body - expect(json['version_check']).not_to be_present - end + include_examples "version info absent" end end - describe '#problems' do + context "when logged in as a moderator" do + before { sign_in(moderator) } + + context "when version checking is enabled" do + before do + SiteSetting.version_checks = true + end + + include_examples "version info present" + end + + context "when version checking is disabled" do + before do + SiteSetting.version_checks = false + end + + include_examples "version info absent" + 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/dashboard.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end + end + end + + describe '#problems' do + context "when logged in as an admin" do + before { sign_in(admin) } + context 'when there are no problems' do before do AdminDashboardData.stubs(:fetch_problems).returns([]) @@ -85,8 +141,40 @@ RSpec.describe Admin::DashboardController do end end - describe '#new_features' do + context "when logged in as a moderator" do before do + sign_in(moderator) + AdminDashboardData + .stubs(:fetch_problems) + .returns(['Not enough awesome', 'Too much sass']) + end + + it 'returns a list of problems' do + get "/admin/dashboard/problems.json" + + expect(response.status).to eq(200) + json = response.parsed_body + expect(json['problems'].size).to eq(2) + expect(json['problems']).to contain_exactly('Not enough awesome', 'Too much sass') + 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/dashboard/problems.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end + end + end + + describe '#new_features' do + context "when logged in as an admin" do + before do + sign_in(admin) Discourse.redis.del "new_features_last_seen_user_#{admin.id}" Discourse.redis.del "new_features" end @@ -131,7 +219,39 @@ RSpec.describe Admin::DashboardController do end end - describe '#mark_new_features_as_seen' do + context "when logged in as a moderator" do + before { sign_in(moderator) } + + it 'includes new features when available' do + populate_new_features + + get "/admin/dashboard/new-features.json" + + json = response.parsed_body + + expect(json['new_features'].length).to eq(2) + expect(json['new_features'][0]["emoji"]).to eq("🙈") + expect(json['new_features'][0]["title"]).to eq("Fancy Legumes") + expect(json['has_unseen_features']).to eq(true) + 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/dashboard/new-features.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end + end + end + + describe '#mark_new_features_as_seen' do + context "when logged in as an admin" do + before { sign_in(admin) } + it 'resets last seen for a given user' do populate_new_features put "/admin/dashboard/mark-new-features-as-seen.json" @@ -141,5 +261,30 @@ RSpec.describe Admin::DashboardController do expect(DiscourseUpdates.has_unseen_features?(admin.id)).to eq(false) end end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + it "resets last seen for moderator" do + populate_new_features + + put "/admin/dashboard/mark-new-features-as-seen.json" + + expect(response.status).to eq(200) + expect(DiscourseUpdates.new_features_last_seen(moderator.id)).not_to eq(nil) + expect(DiscourseUpdates.has_unseen_features?(moderator.id)).to eq(false) + end + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + it "prevents marking new feature as seen with a 404 response" do + put "/admin/dashboard/mark-new-features-as-seen.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end + end end end diff --git a/spec/requests/admin/email_controller_spec.rb b/spec/requests/admin/email_controller_spec.rb index 3801250af4f..0f0b7acdf9e 100644 --- a/spec/requests/admin/email_controller_spec.rb +++ b/spec/requests/admin/email_controller_spec.rb @@ -2,33 +2,52 @@ RSpec.describe Admin::EmailController do fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } fab!(:email_log) { Fabricate(:email_log) } - before do - sign_in(admin) - end - - it "is a subclass of AdminController" do - expect(Admin::EmailController < Admin::AdminController).to eq(true) - end - describe '#index' do - before do - Admin::EmailController.any_instance - .expects(:action_mailer_settings) - .returns( - username: 'username', - password: 'secret' - ) + context "when logged in as an admin" do + before do + sign_in(admin) + Admin::EmailController.any_instance + .expects(:action_mailer_settings) + .returns( + username: 'username', + password: 'secret' + ) + end + + it 'does not include the password in the response' do + get "/admin/email.json" + mail_settings = response.parsed_body['settings'] + + expect( + mail_settings.select { |setting| setting['name'] == 'password' } + ).to be_empty + end end - it 'does not include the password in the response' do - get "/admin/email.json" - mail_settings = response.parsed_body['settings'] + shared_examples "email settings inaccessible" do + it "denies access with a 404 response" do + get "/admin/email.json" - expect( - mail_settings.select { |setting| setting['name'] == 'password' } - ).to be_empty + expect(response.status).to eq(404) + expect(response.parsed_body["settings"]).to be_nil + 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 "email settings inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "email settings inaccessible" end end @@ -40,40 +59,61 @@ RSpec.describe Admin::EmailController do Fabricate(:post_reply_key, post: post, user: email_log.user) end - it "should return the right response" do - email_log - get "/admin/email/sent.json" + context "when logged in as an admin" do + before { sign_in(admin) } - expect(response.status).to eq(200) - log = response.parsed_body.first - expect(log["id"]).to eq(email_log.id) - expect(log["reply_key"]).to eq(nil) + it "should return the right response" do + email_log + get "/admin/email/sent.json" - post_reply_key + expect(response.status).to eq(200) + log = response.parsed_body.first + expect(log["id"]).to eq(email_log.id) + expect(log["reply_key"]).to eq(nil) - get "/admin/email/sent.json" + post_reply_key - expect(response.status).to eq(200) - log = response.parsed_body.first - expect(log["id"]).to eq(email_log.id) - expect(log["reply_key"]).to eq(post_reply_key.reply_key) - end + get "/admin/email/sent.json" - it 'should be able to filter by reply key' do - email_log_2 = Fabricate(:email_log, post: post) + expect(response.status).to eq(200) + log = response.parsed_body.first + expect(log["id"]).to eq(email_log.id) + expect(log["reply_key"]).to eq(post_reply_key.reply_key) + end - post_reply_key_2 = Fabricate(:post_reply_key, - post: post, - user: email_log_2.user, - reply_key: "2d447423-c625-4fb9-8717-ff04ac60eee8" - ) + it 'should be able to filter by reply key' do + email_log_2 = Fabricate(:email_log, post: post) + + post_reply_key_2 = Fabricate(:post_reply_key, + post: post, + user: email_log_2.user, + reply_key: "2d447423-c625-4fb9-8717-ff04ac60eee8" + ) + + [ + "17ff04", + "2d447423c6254fb98717ff04ac60eee8" + ].each do |reply_key| + get "/admin/email/sent.json", params: { + reply_key: reply_key + } + + expect(response.status).to eq(200) + + logs = response.parsed_body + + expect(logs.size).to eq(1) + expect(logs.first["reply_key"]).to eq(post_reply_key_2.reply_key) + end + end + + it 'should be able to filter by smtp_transaction_response' do + email_log_2 = Fabricate(:email_log, smtp_transaction_response: <<~RESPONSE) + 250 Ok: queued as pYoKuQ1aUG5vdpgh-k2K11qcpF4C1ZQ5qmvmmNW25SM=@mailhog.example + RESPONSE - [ - "17ff04", - "2d447423c6254fb98717ff04ac60eee8" - ].each do |reply_key| get "/admin/email/sent.json", params: { - reply_key: reply_key + smtp_transaction_response: "pYoKu" } expect(response.status).to eq(200) @@ -81,219 +121,448 @@ RSpec.describe Admin::EmailController do logs = response.parsed_body expect(logs.size).to eq(1) - expect(logs.first["reply_key"]).to eq(post_reply_key_2.reply_key) + expect(logs.first["smtp_transaction_response"]).to eq(email_log_2.smtp_transaction_response) end end - it 'should be able to filter by smtp_transaction_response' do - email_log_2 = Fabricate(:email_log, smtp_transaction_response: <<~RESPONSE) - 250 Ok: queued as pYoKuQ1aUG5vdpgh-k2K11qcpF4C1ZQ5qmvmmNW25SM=@mailhog.example - RESPONSE + shared_examples "sent emails inaccessible" do + it "denies access with a 404 response" do + get "/admin/email/sent.json" - get "/admin/email/sent.json", params: { - smtp_transaction_response: "pYoKu" - } + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end + end - expect(response.status).to eq(200) + context "when logged in as a moderator" do + before { sign_in(moderator) } - logs = response.parsed_body + include_examples "sent emails inaccessible" + end - expect(logs.size).to eq(1) - expect(logs.first["smtp_transaction_response"]).to eq(email_log_2.smtp_transaction_response) + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "sent emails inaccessible" end end describe '#skipped' do - fab!(:user) { Fabricate(:user) } + # fab!(:user) { Fabricate(:user) } fab!(:log1) { Fabricate(:skipped_email_log, user: user, created_at: 20.minutes.ago) } fab!(:log2) { Fabricate(:skipped_email_log, created_at: 10.minutes.ago) } - it "succeeds" do - get "/admin/email/skipped.json" + context "when logged in as an admin" do + before { sign_in(admin) } - expect(response.status).to eq(200) - - logs = response.parsed_body - - expect(logs.first["id"]).to eq(log2.id) - expect(logs.last["id"]).to eq(log1.id) - end - - describe 'when filtered by username' do - it 'should return the right response' do - get "/admin/email/skipped.json", params: { - user: user.username - } + it "succeeds" do + get "/admin/email/skipped.json" expect(response.status).to eq(200) logs = response.parsed_body - expect(logs.count).to eq(1) - expect(logs.first["id"]).to eq(log1.id) + expect(logs.first["id"]).to eq(log2.id) + expect(logs.last["id"]).to eq(log1.id) end + + context "when filtered by username" do + it 'should return the right response' do + get "/admin/email/skipped.json", params: { + user: user.username + } + + expect(response.status).to eq(200) + + logs = response.parsed_body + + expect(logs.count).to eq(1) + expect(logs.first["id"]).to eq(log1.id) + end + end + end + + shared_examples "skipped emails inaccessible" do + it "denies access with a 404 response" do + get "/admin/email/skipped.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 "skipped emails inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "skipped emails inaccessible" end end describe '#test' do - it 'raises an error without the email parameter' do - post "/admin/email/test.json" - expect(response.status).to eq(400) + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'raises an error without the email parameter' do + post "/admin/email/test.json" + expect(response.status).to eq(400) + end + + context 'with an email address' do + it 'enqueues a test email job' do + post "/admin/email/test.json", params: { email_address: 'eviltrout@test.domain' } + + expect(response.status).to eq(200) + expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include('eviltrout@test.domain') + end + end + + context 'with SiteSetting.disable_emails' do + fab!(:eviltrout) { Fabricate(:evil_trout) } + fab!(:admin) { Fabricate(:admin) } + + it 'bypasses disable when setting is "yes"' do + SiteSetting.disable_emails = 'yes' + post "/admin/email/test.json", params: { email_address: admin.email } + + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( + admin.email + ) + + incoming = response.parsed_body + expect(incoming['sent_test_email_message']).to eq(I18n.t("admin.email.sent_test")) + end + + it 'bypasses disable when setting is "non-staff"' do + SiteSetting.disable_emails = 'non-staff' + + post "/admin/email/test.json", params: { email_address: eviltrout.email } + + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( + eviltrout.email + ) + + incoming = response.parsed_body + expect(incoming['sent_test_email_message']).to eq(I18n.t("admin.email.sent_test")) + end + + it 'works when setting is "no"' do + SiteSetting.disable_emails = 'no' + + post "/admin/email/test.json", params: { email_address: eviltrout.email } + + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( + eviltrout.email + ) + + incoming = response.parsed_body + expect(incoming['sent_test_email_message']).to eq(I18n.t("admin.email.sent_test")) + end + end end - context 'with an email address' do - it 'enqueues a test email job' do + shared_examples "email tests not allowed" do + it "prevents email tests with a 404 response" do post "/admin/email/test.json", params: { email_address: 'eviltrout@test.domain' } - expect(response.status).to eq(200) - expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include('eviltrout@test.domain') + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end - context 'with SiteSetting.disable_emails' do - fab!(:eviltrout) { Fabricate(:evil_trout) } - fab!(:admin) { Fabricate(:admin) } + context "when logged in as a moderator" do + before { sign_in(moderator) } - it 'bypasses disable when setting is "yes"' do - SiteSetting.disable_emails = 'yes' - post "/admin/email/test.json", params: { email_address: admin.email } + include_examples "email tests not allowed" + end - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - admin.email - ) + context "when logged in as a non-staff user" do + before { sign_in(user) } - incoming = response.parsed_body - expect(incoming['sent_test_email_message']).to eq(I18n.t("admin.email.sent_test")) - end - - it 'bypasses disable when setting is "non-staff"' do - SiteSetting.disable_emails = 'non-staff' - - post "/admin/email/test.json", params: { email_address: eviltrout.email } - - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - eviltrout.email - ) - - incoming = response.parsed_body - expect(incoming['sent_test_email_message']).to eq(I18n.t("admin.email.sent_test")) - end - - it 'works when setting is "no"' do - SiteSetting.disable_emails = 'no' - - post "/admin/email/test.json", params: { email_address: eviltrout.email } - - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - eviltrout.email - ) - - incoming = response.parsed_body - expect(incoming['sent_test_email_message']).to eq(I18n.t("admin.email.sent_test")) - end + include_examples "email tests not allowed" end end describe '#preview_digest' do - it 'raises an error without the last_seen_at parameter' do - get "/admin/email/preview-digest.json" - expect(response.status).to eq(400) + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'raises an error without the last_seen_at parameter' do + get "/admin/email/preview-digest.json" + expect(response.status).to eq(400) + end + + it "returns the right response when username is invalid" do + get "/admin/email/preview-digest.json", params: { + last_seen_at: 1.week.ago, username: "somerandomeusername" + } + + expect(response.status).to eq(400) + end + + it "previews the digest" do + get "/admin/email/preview-digest.json", params: { + last_seen_at: 1.week.ago, username: admin.username + } + expect(response.status).to eq(200) + end end - it "returns the right response when username is invalid" do - get "/admin/email/preview-digest.json", params: { - last_seen_at: 1.week.ago, username: "somerandomeusername" - } + shared_examples "preview digest inaccessible" do + it "denies access with a 404 response" do + get "/admin/email/preview-digest.json", params: { + last_seen_at: 1.week.ago, username: moderator.username + } - expect(response.status).to eq(400) + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end - it "previews the digest" do - get "/admin/email/preview-digest.json", params: { - last_seen_at: 1.week.ago, username: admin.username - } - expect(response.status).to eq(200) + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "preview digest inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "preview digest inaccessible" end end describe '#handle_mail' do - it "returns a bad request if neither email parameter is present" do - post "/admin/email/handle_mail.json" - expect(response.status).to eq(400) - expect(response.body).to include("param is missing") - end + context "when logged in as an admin" do + before { sign_in(admin) } - it 'should enqueue the right job, and show a deprecation warning (email_encoded param should be used)' do - expect_enqueued_with( - job: :process_email, - args: { mail: email('cc'), retry_on_rate_limit: true, source: :handle_mail } - ) do - post "/admin/email/handle_mail.json", params: { email: email('cc') } + it "returns a bad request if neither email parameter is present" do + post "/admin/email/handle_mail.json" + expect(response.status).to eq(400) + expect(response.body).to include("param is missing") + end + + it 'should enqueue the right job, and show a deprecation warning (email_encoded param should be used)' do + expect_enqueued_with( + job: :process_email, + args: { mail: email('cc'), retry_on_rate_limit: true, source: :handle_mail } + ) do + post "/admin/email/handle_mail.json", params: { email: email('cc') } + end + expect(response.status).to eq(200) + expect(response.body).to eq("warning: the email parameter is deprecated. all POST requests to this route should be sent with a base64 strict encoded email_encoded parameter instead. email has been received and is queued for processing") + end + + it 'should enqueue the right job, decoding the raw email param' do + expect_enqueued_with( + job: :process_email, + args: { mail: email('cc'), retry_on_rate_limit: true, source: :handle_mail } + ) do + post "/admin/email/handle_mail.json", params: { email_encoded: Base64.strict_encode64(email('cc')) } + end + expect(response.status).to eq(200) + expect(response.body).to eq("email has been received and is queued for processing") + end + + it "retries enqueueing with forced UTF-8 encoding when encountering Encoding::UndefinedConversionError" do + post "/admin/email/handle_mail.json", params: { email_encoded: Base64.strict_encode64(email('encoding_undefined_conversion')) } + expect(response.status).to eq(200) + expect(response.body).to eq("email has been received and is queued for processing") end - expect(response.status).to eq(200) - expect(response.body).to eq("warning: the email parameter is deprecated. all POST requests to this route should be sent with a base64 strict encoded email_encoded parameter instead. email has been received and is queued for processing") end - it 'should enqueue the right job, decoding the raw email param' do - expect_enqueued_with( - job: :process_email, - args: { mail: email('cc'), retry_on_rate_limit: true, source: :handle_mail } - ) do + shared_examples "email handling not allowed" do + it "prevents email handling with a 404 response" do post "/admin/email/handle_mail.json", params: { email_encoded: Base64.strict_encode64(email('cc')) } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end - expect(response.status).to eq(200) - expect(response.body).to eq("email has been received and is queued for processing") end - it "retries enqueueing with forced UTF-8 encoding when encountering Encoding::UndefinedConversionError" do - post "/admin/email/handle_mail.json", params: { email_encoded: Base64.strict_encode64(email('encoding_undefined_conversion')) } - expect(response.status).to eq(200) - expect(response.body).to eq("email has been received and is queued for processing") + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "email handling not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "email handling not allowed" end end describe '#rejected' do - it 'should provide a string for a blank error' do - Fabricate(:incoming_email, error: "") - get "/admin/email/rejected.json" - expect(response.status).to eq(200) - rejected = response.parsed_body - expect(rejected.first['error']).to eq(I18n.t("emails.incoming.unrecognized_error")) + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'should provide a string for a blank error' do + Fabricate(:incoming_email, error: "") + get "/admin/email/rejected.json" + expect(response.status).to eq(200) + rejected = response.parsed_body + expect(rejected.first['error']).to eq(I18n.t("emails.incoming.unrecognized_error")) + end + end + + shared_examples "rejected emails inaccessible" do + it "denies access with a 404 response" do + get "/admin/email/rejected.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 "rejected emails inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "rejected emails inaccessible" end end describe '#incoming' do - it 'should provide a string for a blank error' do - incoming_email = Fabricate(:incoming_email, error: "") - get "/admin/email/incoming/#{incoming_email.id}.json" - expect(response.status).to eq(200) - incoming = response.parsed_body - expect(incoming['error']).to eq(I18n.t("emails.incoming.unrecognized_error")) + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'should provide a string for a blank error' do + incoming_email = Fabricate(:incoming_email, error: "") + get "/admin/email/incoming/#{incoming_email.id}.json" + expect(response.status).to eq(200) + incoming = response.parsed_body + expect(incoming['error']).to eq(I18n.t("emails.incoming.unrecognized_error")) + end + end + + shared_examples "incoming emails inaccessible" do + it "denies access with a 404 response" do + incoming_email = Fabricate(:incoming_email, error: "") + + get "/admin/email/incoming/#{incoming_email.id}.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 "incoming emails inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "incoming emails inaccessible" end end describe '#incoming_from_bounced' do - it 'raises an error when the email log entry does not exist' do - get "/admin/email/incoming_from_bounced/12345.json" - expect(response.status).to eq(404) + context "when logged in as an admin" do + before { sign_in(admin) } - json = response.parsed_body - expect(json["errors"]).to include("Discourse::InvalidParameters") + it 'raises an error when the email log entry does not exist' do + get "/admin/email/incoming_from_bounced/12345.json" + expect(response.status).to eq(404) + + json = response.parsed_body + expect(json["errors"]).to include("Discourse::InvalidParameters") + end + + it 'raises an error when the email log entry is not marked as bounced' do + get "/admin/email/incoming_from_bounced/#{email_log.id}.json" + expect(response.status).to eq(404) + + json = response.parsed_body + expect(json["errors"]).to include("Discourse::InvalidParameters") + end + + context 'when bounced email log entry exists' do + fab!(:email_log) { Fabricate(:email_log, bounced: true, bounce_key: SecureRandom.hex) } + let(:error_message) { "Email::Receiver::BouncedEmailError" } + + it 'returns an incoming email sent to the reply_by_email_address' do + SiteSetting.reply_by_email_address = "replies+%{reply_key}@example.com" + + Fabricate(:incoming_email, + is_bounce: true, + error: error_message, + to_addresses: Email::Sender.bounce_address(email_log.bounce_key) + ) + + get "/admin/email/incoming_from_bounced/#{email_log.id}.json" + expect(response.status).to eq(200) + + json = response.parsed_body + expect(json["error"]).to eq(error_message) + end + + it 'returns an incoming email sent to the notification_email address' do + Fabricate(:incoming_email, + is_bounce: true, + error: error_message, + to_addresses: SiteSetting.notification_email.sub("@", "+verp-#{email_log.bounce_key}@") + ) + + get "/admin/email/incoming_from_bounced/#{email_log.id}.json" + expect(response.status).to eq(200) + + json = response.parsed_body + expect(json["error"]).to eq(error_message) + end + + it 'returns an incoming email sent to the notification_email address' do + SiteSetting.reply_by_email_address = "replies+%{reply_key}@subdomain.example.com" + Fabricate(:incoming_email, + is_bounce: true, + error: error_message, + to_addresses: "subdomain+verp-#{email_log.bounce_key}@example.com" + ) + + get "/admin/email/incoming_from_bounced/#{email_log.id}.json" + expect(response.status).to eq(200) + + json = response.parsed_body + expect(json["error"]).to eq(error_message) + end + + it 'raises an error if the bounce_key is blank' do + email_log.update(bounce_key: nil) + + get "/admin/email/incoming_from_bounced/#{email_log.id}.json" + expect(response.status).to eq(404) + + json = response.parsed_body + expect(json["errors"]).to include("Discourse::InvalidParameters") + end + + it 'raises an error if there is no incoming email' do + get "/admin/email/incoming_from_bounced/#{email_log.id}.json" + expect(response.status).to eq(404) + + json = response.parsed_body + expect(json["errors"]).to include("Discourse::NotFound") + end + end end - it 'raises an error when the email log entry is not marked as bounced' do - get "/admin/email/incoming_from_bounced/#{email_log.id}.json" - expect(response.status).to eq(404) - - json = response.parsed_body - expect(json["errors"]).to include("Discourse::InvalidParameters") - end - - context 'when bounced email log entry exists' do - fab!(:email_log) { Fabricate(:email_log, bounced: true, bounce_key: SecureRandom.hex) } - let(:error_message) { "Email::Receiver::BouncedEmailError" } - - it 'returns an incoming email sent to the reply_by_email_address' do + shared_examples "bounced incoming emails inaccessible" do + it "denies access with a 404 response" do + email_log = Fabricate(:email_log, bounced: true, bounce_key: SecureRandom.hex) + error_message = "Email::Receiver::BouncedEmailError" SiteSetting.reply_by_email_address = "replies+%{reply_key}@example.com" Fabricate(:incoming_email, @@ -303,82 +572,75 @@ RSpec.describe Admin::EmailController do ) get "/admin/email/incoming_from_bounced/#{email_log.id}.json" - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["error"]).to eq(error_message) - end - - it 'returns an incoming email sent to the notification_email address' do - Fabricate(:incoming_email, - is_bounce: true, - error: error_message, - to_addresses: SiteSetting.notification_email.sub("@", "+verp-#{email_log.bounce_key}@") - ) - - get "/admin/email/incoming_from_bounced/#{email_log.id}.json" - expect(response.status).to eq(200) - - json = response.parsed_body - expect(json["error"]).to eq(error_message) - end - - it 'returns an incoming email sent to the notification_email address' do - SiteSetting.reply_by_email_address = "replies+%{reply_key}@subdomain.example.com" - Fabricate(:incoming_email, - is_bounce: true, - error: error_message, - to_addresses: "subdomain+verp-#{email_log.bounce_key}@example.com" - ) - - get "/admin/email/incoming_from_bounced/#{email_log.id}.json" - expect(response.status).to eq(200) - - json = response.parsed_body - expect(json["error"]).to eq(error_message) - end - - it 'raises an error if the bounce_key is blank' do - email_log.update(bounce_key: nil) - - get "/admin/email/incoming_from_bounced/#{email_log.id}.json" expect(response.status).to eq(404) - - json = response.parsed_body - expect(json["errors"]).to include("Discourse::InvalidParameters") + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end + end - it 'raises an error if there is no incoming email' do - get "/admin/email/incoming_from_bounced/#{email_log.id}.json" - expect(response.status).to eq(404) + context "when logged in as a moderator" do + before { sign_in(moderator) } - json = response.parsed_body - expect(json["errors"]).to include("Discourse::NotFound") - end + include_examples "bounced incoming emails inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "bounced incoming emails inaccessible" end end describe '#advanced_test' do - it 'should ...' do - email = <<~EMAIL - From: "somebody" - To: someone@example.com - Date: Mon, 3 Dec 2018 00:00:00 -0000 - Subject: This is some subject - Content-Type: text/plain; charset="UTF-8" + let(:email) do + <<~EMAIL + From: "somebody" + To: someone@example.com + Date: Mon, 3 Dec 2018 00:00:00 -0000 + Subject: This is some subject + Content-Type: text/plain; charset="UTF-8" - Hello, this is a test! + Hello, this is a test! - --- + --- - This part should be elided. - EMAIL - post "/admin/email/advanced-test.json", params: { email: email } - expect(response.status).to eq(200) - incoming = response.parsed_body - expect(incoming['format']).to eq(1) - expect(incoming['text']).to eq("Hello, this is a test!") - expect(incoming['elided']).to eq("---\n\nThis part should be elided.") + This part should be elided. + EMAIL + end + + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'should ...' do + post "/admin/email/advanced-test.json", params: { email: email } + + expect(response.status).to eq(200) + incoming = response.parsed_body + expect(incoming['format']).to eq(1) + expect(incoming['text']).to eq("Hello, this is a test!") + expect(incoming['elided']).to eq("---\n\nThis part should be elided.") + end + end + + shared_examples "advanced email tests not allowed" do + it "prevents advanced email tests with a 404 response" do + post "/admin/email/advanced-test.json", params: { email: email } + + 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 "advanced email tests not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "advanced email tests not allowed" end end end diff --git a/spec/requests/admin/email_styles_controller_spec.rb b/spec/requests/admin/email_styles_controller_spec.rb index 6f857ef320c..a7727ffb580 100644 --- a/spec/requests/admin/email_styles_controller_spec.rb +++ b/spec/requests/admin/email_styles_controller_spec.rb @@ -2,41 +2,61 @@ RSpec.describe Admin::EmailStylesController do fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } + let(:default_html) { File.read("#{Rails.root}/app/views/email/default_template.html") } let(:default_css) { "" } - before do - sign_in(admin) - end - after do SiteSetting.remove_override!(:email_custom_template) SiteSetting.remove_override!(:email_custom_css) end - it "is a subclass of AdminController" do - expect(Admin::EmailStylesController < Admin::AdminController).to eq(true) - end - describe 'show' do - it 'returns default values' do - get '/admin/customize/email_style.json' - expect(response.status).to eq(200) + context "when logged in as an admin" do + before { sign_in(admin) } - json = response.parsed_body['email_style'] - expect(json['html']).to eq(default_html) - expect(json['css']).to eq(default_css) + it 'returns default values' do + get '/admin/customize/email_style.json' + expect(response.status).to eq(200) + + json = response.parsed_body['email_style'] + expect(json['html']).to eq(default_html) + expect(json['css']).to eq(default_css) + end + + it 'returns customized values' do + SiteSetting.email_custom_template = "For you: %{email_content}" + SiteSetting.email_custom_css = ".user-name { font-size: 24px; }" + get '/admin/customize/email_style.json' + expect(response.status).to eq(200) + + json = response.parsed_body['email_style'] + expect(json['html']).to eq("For you: %{email_content}") + expect(json['css']).to eq(".user-name { font-size: 24px; }") + end end - it 'returns customized values' do - SiteSetting.email_custom_template = "For you: %{email_content}" - SiteSetting.email_custom_css = ".user-name { font-size: 24px; }" - get '/admin/customize/email_style.json' - expect(response.status).to eq(200) + shared_examples "email styles inaccessible" do + it "denies access with a 404 response" do + get '/admin/customize/email_style.json' - json = response.parsed_body['email_style'] - expect(json['html']).to eq("For you: %{email_content}") - expect(json['css']).to eq(".user-name { font-size: 24px; }") + 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 "email styles inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "email styles inaccessible" end end @@ -48,26 +68,51 @@ RSpec.describe Admin::EmailStylesController do } end - it 'changes the settings' do - SiteSetting.email_custom_css = ".user-name { font-size: 24px; }" - put '/admin/customize/email_style.json', params: { email_style: valid_params } - expect(response.status).to eq(200) - expect(SiteSetting.email_custom_template).to eq(valid_params[:html]) - expect(SiteSetting.email_custom_css).to eq(valid_params[:css]) + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'changes the settings' do + SiteSetting.email_custom_css = ".user-name { font-size: 24px; }" + put '/admin/customize/email_style.json', params: { email_style: valid_params } + expect(response.status).to eq(200) + expect(SiteSetting.email_custom_template).to eq(valid_params[:html]) + expect(SiteSetting.email_custom_css).to eq(valid_params[:css]) + end + + it 'reports errors' do + put '/admin/customize/email_style.json', params: { + email_style: valid_params.merge(html: 'No email content') + } + expect(response.status).to eq(422) + json = response.parsed_body + expect(json['errors']).to include( + I18n.t( + 'email_style.html_missing_placeholder', + placeholder: '%{email_content}' + ) + ) + end end - it 'reports errors' do - put '/admin/customize/email_style.json', params: { - email_style: valid_params.merge(html: 'No email content') - } - expect(response.status).to eq(422) - json = response.parsed_body - expect(json['errors']).to include( - I18n.t( - 'email_style.html_missing_placeholder', - placeholder: '%{email_content}' - ) - ) + shared_examples "email style update not allowed" do + it "denies access with a 404 response" do + put '/admin/customize/email_style.json', params: { email_style: valid_params } + + 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 "email style update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "email style update not allowed" end end end diff --git a/spec/requests/admin/email_templates_controller_spec.rb b/spec/requests/admin/email_templates_controller_spec.rb index acdb55bd568..ec6fc3d315b 100644 --- a/spec/requests/admin/email_templates_controller_spec.rb +++ b/spec/requests/admin/email_templates_controller_spec.rb @@ -18,90 +18,70 @@ RSpec.describe Admin::EmailTemplatesController do I18n.reload! end - it "is a subclass of AdminController" do - expect(Admin::EmailTemplatesController < Admin::AdminController).to eq(true) - end - describe "#index" do - it "raises an error if you aren't logged in" do - get '/admin/customize/email_templates.json' - expect(response.status).to eq(404) + context "when logged in as an admin" do + before { sign_in(admin) } + + it "should work if you are an admin" do + get '/admin/customize/email_templates.json' + + expect(response.status).to eq(200) + + json = response.parsed_body + expect(json['email_templates']).to be_present + end + + it 'returns overridden = true if subject or body has translation_overrides record' do + put '/admin/customize/email_templates/user_notifications.admin_login', params: { + email_template: { subject: original_subject, body: original_body } + }, headers: headers + expect(response.status).to eq(200) + + get '/admin/customize/email_templates.json' + expect(response.status).to eq(200) + templates = response.parsed_body['email_templates'] + template = templates.find { |t| t['id'] == 'user_notifications.admin_login' } + expect(template['can_revert']).to eq(true) + + TranslationOverride.destroy_all + + get '/admin/customize/email_templates.json' + expect(response.status).to eq(200) + templates = response.parsed_body['email_templates'] + template = templates.find { |t| t['id'] == 'user_notifications.admin_login' } + expect(template['can_revert']).to eq(false) + end end - it "raises an error if you aren't an admin" do - sign_in(user) - get '/admin/customize/email_templates.json' - expect(response.status).to eq(404) + shared_examples "email templates inaccessible" do + it "denies access with a 404 response" do + get "/admin/customize/email_templates.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end - it "raises an error if you are a moderator" do - sign_in(moderator) - get "/admin/customize/email_templates.json" - expect(response.status).to eq(404) + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "email templates inaccessible" end - it "should work if you are an admin" do - sign_in(admin) - get '/admin/customize/email_templates.json' + context "when logged in as a non-staff user" do + before { sign_in(user) } - expect(response.status).to eq(200) - - json = response.parsed_body - expect(json['email_templates']).to be_present + include_examples "email templates inaccessible" end - it 'returns overridden = true if subject or body has translation_overrides record' do - sign_in(admin) - - put '/admin/customize/email_templates/user_notifications.admin_login', params: { - email_template: { subject: original_subject, body: original_body } - }, headers: headers - expect(response.status).to eq(200) - - get '/admin/customize/email_templates.json' - expect(response.status).to eq(200) - templates = response.parsed_body['email_templates'] - template = templates.find { |t| t['id'] == 'user_notifications.admin_login' } - expect(template['can_revert']).to eq(true) - - TranslationOverride.destroy_all - - get '/admin/customize/email_templates.json' - expect(response.status).to eq(200) - templates = response.parsed_body['email_templates'] - template = templates.find { |t| t['id'] == 'user_notifications.admin_login' } - expect(template['can_revert']).to eq(false) + context "when not logged in" do + include_examples "email templates inaccessible" end end describe "#update" do - it "raises an error if you aren't logged in" do - put '/admin/customize/email_templates/some_id', params: { - email_template: { subject: 'Subject', body: 'Body' } - }, headers: headers - expect(response.status).to eq(404) - end - - it "raises an error if you aren't an admin" do - sign_in(user) - put '/admin/customize/email_templates/some_id', params: { - email_template: { subject: 'Subject', body: 'Body' } - }, headers: headers - expect(response.status).to eq(404) - end - - it "raises an error if you are a moderator" do - sign_in(moderator) - put "/admin/customize/email_templates/some_id", params: { - email_template: { subject: "Subject", body: "Body" } - }, headers: headers - expect(response.status).to eq(404) - end - - context "when logged in as admin" do - before do - sign_in(admin) - end + context "when logged in as an admin" do + before { sign_in(admin) } it "returns 'not found' when an unknown email template id is used" do put '/admin/customize/email_templates/non_existent_template', params: { @@ -273,30 +253,37 @@ RSpec.describe Admin::EmailTemplatesController do end end + shared_examples "email template update not allowed" do + it "prevents updates with a 404 response" do + put "/admin/customize/email_templates/some_id", params: { + email_template: { subject: 'Subject', body: 'Body' } + }, headers: headers + + 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 "email template update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "email template update not allowed" + end + + context "when not logged in" do + include_examples "email template update not allowed" + end end describe "#revert" do - it "raises an error if you aren't logged in" do - delete '/admin/customize/email_templates/some_id', headers: headers - expect(response.status).to eq(404) - end - - it "raises an error if you aren't an admin" do - sign_in(user) - delete '/admin/customize/email_templates/some_id', headers: headers - expect(response.status).to eq(404) - end - - it "raises an error if you are a moderator" do - sign_in(moderator) - delete "/admin/customize/email_templates/some_id", headers: headers - expect(response.status).to eq(404) - end - - context "when logged in as admin" do - before do - sign_in(admin) - end + context "when logged in as an admin" do + before { sign_in(admin) } it "returns 'not found' when an unknown email template id is used" do delete '/admin/customize/email_templates/non_existent_template', headers: headers @@ -364,6 +351,30 @@ RSpec.describe Admin::EmailTemplatesController do end end + shared_examples "email template reversal not allowed" do + it "prevents reversals with a 404 response" do + delete "/admin/customize/email_templates/some_id", headers: headers + + 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 "email template reversal not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "email template reversal not allowed" + end + + context "when not logged in" do + include_examples "email template reversal not allowed" + end end it "uses only existing email templates" do diff --git a/spec/requests/admin/embeddable_hosts_controller_spec.rb b/spec/requests/admin/embeddable_hosts_controller_spec.rb index 2d6dbea801b..cd3ab6689f1 100644 --- a/spec/requests/admin/embeddable_hosts_controller_spec.rb +++ b/spec/requests/admin/embeddable_hosts_controller_spec.rb @@ -1,19 +1,15 @@ # frozen_string_literal: true RSpec.describe Admin::EmbeddableHostsController do - it "is a subclass of AdminController" do - expect(Admin::EmbeddableHostsController < Admin::AdminController).to eq(true) - end + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } + fab!(:embeddable_host) { Fabricate(:embeddable_host) } - context 'while logged in as an admin' do - fab!(:admin) { Fabricate(:admin) } - fab!(:embeddable_host) { Fabricate(:embeddable_host) } + describe '#create' do + context "when logged in as an admin" do + before { sign_in(admin) } - before do - sign_in(admin) - end - - describe '#create' do it "logs embeddable host create" do post "/admin/embeddable_hosts.json", params: { embeddable_host: { host: "test.com" } @@ -25,7 +21,34 @@ RSpec.describe Admin::EmbeddableHostsController do end end - describe '#update' do + shared_examples "embeddable host creation not allowed" do + it "prevents embeddable host creation with a 404 response" do + post "/admin/embeddable_hosts.json", params: { + embeddable_host: { host: "test.com" } + } + + 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 "embeddable host creation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "embeddable host creation not allowed" + end + end + + describe '#update' do + context "when logged in as an admin" do + before { sign_in(admin) } + it "logs embeddable host update" do category = Fabricate(:category) @@ -41,11 +64,39 @@ RSpec.describe Admin::EmbeddableHostsController do new_value: "category_id: #{category.id}, class_name: test-class, host: test.com").exists? expect(history_exists).to eq(true) - end end - describe '#destroy' do + shared_examples "embeddable host update not allowed" do + it "prevents updates with a 404 response" do + category = Fabricate(:category) + + put "/admin/embeddable_hosts/#{embeddable_host.id}.json", params: { + embeddable_host: { host: "test.com", class_name: "test-class", category_id: category.id } + } + + 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 "embeddable host update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "embeddable host update not allowed" + end + end + + describe '#destroy' do + context "when logged in as an admin" do + before { sign_in(admin) } + it "logs embeddable host destroy" do delete "/admin/embeddable_hosts/#{embeddable_host.id}.json", params: {} @@ -53,5 +104,26 @@ RSpec.describe Admin::EmbeddableHostsController do expect(UserHistory.where(acting_user_id: admin.id, action: UserHistory.actions[:embeddable_host_destroy]).exists?).to eq(true) end end + + shared_examples "embeddable host deletion not allowed" do + it "prevents deletion with a 404 response" do + delete "/admin/embeddable_hosts/#{embeddable_host.id}.json", params: {} + + 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 "embeddable host deletion not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "embeddable host deletion not allowed" + end end end diff --git a/spec/requests/admin/embedding_controller_spec.rb b/spec/requests/admin/embedding_controller_spec.rb index 722ba050af3..7f28786b837 100644 --- a/spec/requests/admin/embedding_controller_spec.rb +++ b/spec/requests/admin/embedding_controller_spec.rb @@ -1,7 +1,87 @@ # frozen_string_literal: true RSpec.describe Admin::EmbeddingController do - it "is a subclass of AdminController" do - expect(Admin::EmbeddingController < Admin::AdminController).to eq(true) + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } + + describe "#show" do + context 'when logged in as an admin' do + before { sign_in(admin) } + + it "returns embedding" do + get "/admin/customize/embedding.json" + + expect(response.status).to eq(200) + expect(response.parsed_body["embedding"]).to be_present + end + end + + shared_examples "embedding accessible" do + it "returns embedding" do + get "/admin/customize/embedding.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 "embedding accessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "embedding accessible" + end + end + + describe "#update" do + context 'when logged in as an admin' do + before { sign_in(admin) } + + it "updates embedding" do + put "/admin/customize/embedding.json", params: { + embedding: { + embed_by_username: "system", + embed_post_limit: 200 + } + } + + expect(response.status).to eq(200) + expect(response.parsed_body["embedding"]["embed_by_username"]).to eq("system") + expect(response.parsed_body["embedding"]["embed_post_limit"]).to eq(200) + end + end + + shared_examples "embedding updates not allowed" do + it "prevents updates with a 404 response" do + put "/admin/customize/embedding.json", params: { + embedding: { + embed_by_username: "system", + embed_post_limit: 200 + } + } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(response.parsed_body["embedding"]).to be_nil + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "embedding updates not allowed" + end + + context "when logged in as a moderator" do + before { sign_in(user) } + + include_examples "embedding updates not allowed" + end end end diff --git a/spec/requests/admin/emojis_controller_spec.rb b/spec/requests/admin/emojis_controller_spec.rb index fe7773a3787..b6849347cf4 100644 --- a/spec/requests/admin/emojis_controller_spec.rb +++ b/spec/requests/admin/emojis_controller_spec.rb @@ -2,139 +2,219 @@ RSpec.describe Admin::EmojisController do fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } fab!(:upload) { Fabricate(:upload) } - before do - sign_in(admin) - end - describe '#index' do - it "returns a list of custom emojis" do - CustomEmoji.create!(name: 'osama-test-emoji', upload: upload) - Emoji.clear_cache + context "when logged in as an admin" do + before { sign_in(admin) } - get "/admin/customize/emojis.json" - expect(response.status).to eq(200) + it "returns a list of custom emojis" do + CustomEmoji.create!(name: 'osama-test-emoji', upload: upload) + Emoji.clear_cache - json = response.parsed_body - expect(json[0]["name"]).to eq("osama-test-emoji") - expect(json[0]["url"]).to eq(upload.url) + get "/admin/customize/emojis.json" + expect(response.status).to eq(200) + + json = response.parsed_body + expect(json[0]["name"]).to eq("osama-test-emoji") + expect(json[0]["url"]).to eq(upload.url) + end + end + + shared_examples "custom emojis inaccessible" do + it "denies access with a 404 response" do + get "/admin/customize/emojis.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 "custom emojis inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "custom emojis inaccessible" end end describe "#create" do - describe 'when upload is invalid' do - it 'should publish the right error' do + context "when logged in as an admin" do + before { sign_in(admin) } - post "/admin/customize/emojis.json", params: { - name: 'test', - file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/fake.jpg") - } + context 'when upload is invalid' do + it 'should publish the right error' do - expect(response.status).to eq(422) - parsed = response.parsed_body - expect(parsed["errors"]).to eq([I18n.t('upload.images.size_not_found')]) + post "/admin/customize/emojis.json", params: { + name: 'test', + file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/fake.jpg") + } + + expect(response.status).to eq(422) + parsed = response.parsed_body + expect(parsed["errors"]).to eq([I18n.t('upload.images.size_not_found')]) + end end - end - describe 'when emoji name already exists' do - it 'should publish the right error' do - CustomEmoji.create!(name: 'test', upload: upload) + context 'when emoji name already exists' do + it 'should publish the right error' do + CustomEmoji.create!(name: 'test', upload: upload) + + post "/admin/customize/emojis.json", params: { + name: 'test', + file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") + } + + expect(response.status).to eq(422) + parsed = response.parsed_body + expect(parsed["errors"]).to eq([ + "Name #{I18n.t('activerecord.errors.models.custom_emoji.attributes.name.taken')}" + ]) + end + end + + it 'should allow an admin to add a custom emoji' do + Emoji.expects(:clear_cache) post "/admin/customize/emojis.json", params: { name: 'test', file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") } - expect(response.status).to eq(422) - parsed = response.parsed_body - expect(parsed["errors"]).to eq([ - "Name #{I18n.t('activerecord.errors.models.custom_emoji.attributes.name.taken')}" - ]) + custom_emoji = CustomEmoji.last + upload = custom_emoji.upload + + expect(upload.original_filename).to eq('logo.png') + + data = response.parsed_body + expect(response.status).to eq(200) + expect(data["errors"]).to eq(nil) + expect(data["name"]).to eq(custom_emoji.name) + expect(data["url"]).to eq(upload.url) + expect(custom_emoji.group).to eq(nil) + end + + it 'should allow an admin to add a custom emoji with a custom group' do + Emoji.expects(:clear_cache) + + post "/admin/customize/emojis.json", params: { + name: 'test', + group: 'Foo', + file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") + } + + custom_emoji = CustomEmoji.last + + data = response.parsed_body + expect(response.status).to eq(200) + expect(custom_emoji.group).to eq("foo") + end + + it 'should fix up the emoji name' do + Emoji.expects(:clear_cache).times(3) + + post "/admin/customize/emojis.json", params: { + name: 'test.png', + file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") + } + + custom_emoji = CustomEmoji.last + upload = custom_emoji.upload + + expect(upload.original_filename).to eq('logo.png') + expect(custom_emoji.name).to eq("test") + expect(response.status).to eq(200) + + post "/admin/customize/emojis.json", params: { + name: 'st&#* onk$', + file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") + } + + custom_emoji = CustomEmoji.last + expect(custom_emoji.name).to eq("st_onk_") + expect(response.status).to eq(200) + + post "/admin/customize/emojis.json", params: { + name: 'PaRTYpaRrot', + file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") + } + + custom_emoji = CustomEmoji.last + expect(custom_emoji.name).to eq("partyparrot") + expect(response.status).to eq(200) end end - it 'should allow an admin to add a custom emoji' do - Emoji.expects(:clear_cache) + shared_examples "custom emoji creation not allowed" do + it "prevents creation with a 404 response" do + post "/admin/customize/emojis.json", params: { + name: 'test', + file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") + } - post "/admin/customize/emojis.json", params: { - name: 'test', - file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") - } - - custom_emoji = CustomEmoji.last - upload = custom_emoji.upload - - expect(upload.original_filename).to eq('logo.png') - - data = response.parsed_body - expect(response.status).to eq(200) - expect(data["errors"]).to eq(nil) - expect(data["name"]).to eq(custom_emoji.name) - expect(data["url"]).to eq(upload.url) - expect(custom_emoji.group).to eq(nil) + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end - it 'should allow an admin to add a custom emoji with a custom group' do - Emoji.expects(:clear_cache) + context "when logged in as a moderator" do + before { sign_in(moderator) } - post "/admin/customize/emojis.json", params: { - name: 'test', - group: 'Foo', - file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") - } - - custom_emoji = CustomEmoji.last - - data = response.parsed_body - expect(response.status).to eq(200) - expect(custom_emoji.group).to eq("foo") + include_examples "custom emoji creation not allowed" end - it 'should fix up the emoji name' do - Emoji.expects(:clear_cache).times(3) + context "when logged in as a non-staff user" do + before { sign_in(user) } - post "/admin/customize/emojis.json", params: { - name: 'test.png', - file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") - } - - custom_emoji = CustomEmoji.last - upload = custom_emoji.upload - - expect(upload.original_filename).to eq('logo.png') - expect(custom_emoji.name).to eq("test") - expect(response.status).to eq(200) - - post "/admin/customize/emojis.json", params: { - name: 'st&#* onk$', - file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") - } - - custom_emoji = CustomEmoji.last - expect(custom_emoji.name).to eq("st_onk_") - expect(response.status).to eq(200) - - post "/admin/customize/emojis.json", params: { - name: 'PaRTYpaRrot', - file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") - } - - custom_emoji = CustomEmoji.last - expect(custom_emoji.name).to eq("partyparrot") - expect(response.status).to eq(200) + include_examples "custom emoji creation not allowed" end end describe '#destroy' do - it 'should allow an admin to delete a custom emoji' do - custom_emoji = CustomEmoji.create!(name: 'test', upload: upload) - Emoji.clear_cache + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'should allow an admin to delete a custom emoji' do + custom_emoji = CustomEmoji.create!(name: 'test', upload: upload) + Emoji.clear_cache + + expect do + delete "/admin/customize/emojis/#{custom_emoji.name}.json", + params: { name: 'test' } + end.to change { CustomEmoji.count }.by(-1) + end + end + + shared_examples "custom emoji deletion not allowed" do + it "prevents deletion with a 404 response" do + custom_emoji = CustomEmoji.create!(name: 'test', upload: upload) + Emoji.clear_cache - expect do delete "/admin/customize/emojis/#{custom_emoji.name}.json", params: { name: 'test' } - end.to change { CustomEmoji.count }.by(-1) + + 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 "custom emoji deletion not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "custom emoji deletion not allowed" end end end diff --git a/spec/requests/admin/groups_controller_spec.rb b/spec/requests/admin/groups_controller_spec.rb index 89ff772ea09..732f1f62465 100644 --- a/spec/requests/admin/groups_controller_spec.rb +++ b/spec/requests/admin/groups_controller_spec.rb @@ -2,17 +2,10 @@ RSpec.describe Admin::GroupsController do fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } fab!(:user) { Fabricate(:user) } fab!(:group) { Fabricate(:group) } - it 'is a subclass of StaffController' do - expect(Admin::UsersController < Admin::StaffController).to eq(true) - end - - before do - sign_in(admin) - end - describe '#create' do let(:group_params) do { @@ -27,359 +20,147 @@ RSpec.describe Admin::GroupsController do } end - it 'should work' do - post "/admin/groups.json", params: group_params + context "when logged in as an admin" do + before { sign_in(admin) } - expect(response.status).to eq(200) - - group = Group.last - - expect(group.name).to eq('testing') - expect(group.users).to contain_exactly(admin, user) - expect(group.allow_membership_requests).to eq(true) - expect(group.membership_request_template).to eq('Testing') - expect(group.members_visibility_level).to eq(Group.visibility_levels[:staff]) - end - - context "with custom_fields" do - before do - plugin = Plugin::Instance.new - plugin.register_editable_group_custom_field :test - end - - after do - DiscoursePluginRegistry.reset! - end - - it "only updates allowed user fields" do - params = group_params - params[:group].merge!(custom_fields: { test: :hello1, test2: :hello2 }) - - post "/admin/groups.json", params: params - - group = Group.last - - expect(response.status).to eq(200) - expect(group.custom_fields['test']).to eq('hello1') - expect(group.custom_fields['test2']).to be_blank - end - - it "is secure when there are no registered editable fields" do - DiscoursePluginRegistry.reset! - params = group_params - params[:group].merge!(custom_fields: { test: :hello1, test2: :hello2 }) - - post "/admin/groups.json", params: params - - group = Group.last - - expect(response.status).to eq(200) - expect(group.custom_fields['test']).to be_blank - expect(group.custom_fields['test2']).to be_blank - end - end - - context 'with Group.plugin_permitted_params' do - after do - DiscoursePluginRegistry.reset! - end - - it 'filter unpermitted params' do - params = group_params - params[:group].merge!(allow_unknown_sender_topic_replies: true) - - post "/admin/groups.json", params: params - expect(Group.last.allow_unknown_sender_topic_replies).to eq(false) - end - - it 'allows plugin to allow custom params' do - params = group_params - params[:group].merge!(allow_unknown_sender_topic_replies: true) - - plugin = Plugin::Instance.new - plugin.register_group_param :allow_unknown_sender_topic_replies - - post "/admin/groups.json", params: params - expect(Group.last.allow_unknown_sender_topic_replies).to eq(true) - end - end - end - - describe '#add_owners' do - it 'should work' do - put "/admin/groups/#{group.id}/owners.json", params: { - group: { - usernames: [user.username, admin.username].join(",") - } - } - - expect(response.status).to eq(200) - - response_body = response.parsed_body - - expect(response_body["usernames"]).to contain_exactly(user.username, admin.username) - - expect(group.group_users.where(owner: true).map(&:user)) - .to contain_exactly(user, admin) - end - - it 'returns not-found error when there is no group' do - group.destroy! - - put "/admin/groups/#{group.id}/owners.json", params: { - group: { - usernames: user.username - } - } - - expect(response.status).to eq(404) - end - - it 'does not allow adding owners to an automatic group' do - group.update!(automatic: true) - - expect do - put "/admin/groups/#{group.id}/owners.json", params: { - group: { - usernames: user.username - } - } - end.to_not change { group.group_users.count } - - expect(response.status).to eq(422) - expect(response.parsed_body["errors"]).to eq(["You cannot modify an automatic group"]) - end - - it 'does not notify users when the param is not present' do - put "/admin/groups/#{group.id}/owners.json", params: { - group: { - usernames: user.username - } - } - expect(response.status).to eq(200) - - topic = Topic.find_by( - title: I18n.t("system_messages.user_added_to_group_as_owner.subject_template", group_name: group.name), - archetype: "private_message" - ) - expect(topic.nil?).to eq(true) - end - - it 'notifies users when the param is present' do - put "/admin/groups/#{group.id}/owners.json", params: { - group: { - usernames: user.username, - notify_users: true - } - } - expect(response.status).to eq(200) - - topic = Topic.find_by( - title: I18n.t("system_messages.user_added_to_group_as_owner.subject_template", group_name: group.name), - archetype: "private_message" - ) - expect(topic.nil?).to eq(false) - expect(topic.topic_users.map(&:user_id)).to include(-1, user.id) - end - end - - describe '#remove_owner' do - let(:user2) { Fabricate(:user) } - let(:user3) { Fabricate(:user) } - - it 'should work' do - group.add_owner(user) - - delete "/admin/groups/#{group.id}/owners.json", params: { - user_id: user.id - } - - expect(response.status).to eq(200) - expect(group.group_users.where(owner: true)).to eq([]) - end - - it 'should work with multiple users' do - group.add_owner(user) - group.add_owner(user3) - - delete "/admin/groups/#{group.id}/owners.json", params: { - group: { - usernames: "#{user.username},#{user2.username},#{user3.username}" - } - } - - expect(response.status).to eq(200) - expect(group.group_users.where(owner: true)).to eq([]) - end - - it 'returns not-found error when there is no group' do - group.destroy! - - delete "/admin/groups/#{group.id}/owners.json", params: { - user_id: user.id - } - - expect(response.status).to eq(404) - end - - it 'does not allow removing owners from an automatic group' do - group.update!(automatic: true) - - delete "/admin/groups/#{group.id}/owners.json", params: { - user_id: user.id - } - - expect(response.status).to eq(422) - expect(response.parsed_body["errors"]).to eq(["You cannot modify an automatic group"]) - end - end - - describe "#set_primary" do - let(:user2) { Fabricate(:user) } - let(:user3) { Fabricate(:user) } - - it 'sets with multiple users' do - user2.update!(primary_group_id: group.id) - - put "/admin/groups/#{group.id}/primary.json", params: { - group: { usernames: "#{user.username},#{user2.username},#{user3.username}" }, - primary: "true" - } - - expect(response.status).to eq(200) - expect(User.where(primary_group_id: group.id).size).to eq(3) - end - - it 'unsets with multiple users' do - user.update!(primary_group_id: group.id) - user3.update!(primary_group_id: group.id) - - put "/admin/groups/#{group.id}/primary.json", params: { - group: { usernames: "#{user.username},#{user2.username},#{user3.username}" }, - primary: "false" - } - - expect(response.status).to eq(200) - expect(User.where(primary_group_id: group.id).size).to eq(0) - end - end - - describe "#destroy" do - it 'should return the right response for an invalid group_id' do - max_id = Group.maximum(:id).to_i - delete "/admin/groups/#{max_id + 1}.json" - expect(response.status).to eq(404) - end - - it 'logs when a group is destroyed' do - delete "/admin/groups/#{group.id}.json" - - history = UserHistory.where(acting_user: admin).last - - expect(history).to be_present - expect(history.details).to include("name: #{group.name}") - expect(history.details).to include("id: #{group.id}") - end - - it 'logs the grant_trust_level attribute' do - trust_level = TrustLevel[4] - group.update!(grant_trust_level: trust_level) - delete "/admin/groups/#{group.id}.json" - - history = UserHistory.where(acting_user: admin).last - - expect(history).to be_present - expect(history.details).to include("grant_trust_level: #{trust_level}") - expect(history.details).to include("name: #{group.name}") - end - - describe 'when group is automatic' do - it "returns the right response" do - group.update!(automatic: true) - - delete "/admin/groups/#{group.id}.json" - - expect(response.status).to eq(422) - expect(Group.find(group.id)).to eq(group) - end - end - - describe 'for a non automatic group' do - it "returns the right response" do - delete "/admin/groups/#{group.id}.json" - - expect(response.status).to eq(200) - expect(Group.find_by(id: group.id)).to eq(nil) - end - end - end - - describe '#automatic_membership_count' do - it 'returns count of users whose emails match the domain' do - Fabricate(:user, email: 'user1@somedomain.org') - Fabricate(:user, email: 'user1@somedomain.com') - Fabricate(:user, email: 'user1@notsomedomain.com') - group = Fabricate(:group) - - put "/admin/groups/automatic_membership_count.json", params: { - automatic_membership_email_domains: 'somedomain.org|somedomain.com', - id: group.id - } - expect(response.status).to eq(200) - expect(response.parsed_body["user_count"]).to eq(2) - end - - it "doesn't responde with 500 if domain is invalid" do - group = Fabricate(:group) - - put "/admin/groups/automatic_membership_count.json", params: { - automatic_membership_email_domains: '@somedomain.org|@somedomain.com', - id: group.id - } - expect(response.status).to eq(200) - expect(response.parsed_body["user_count"]).to eq(0) - end - end - - context "when moderators_manage_categories_and_groups is enabled" do - let(:group_params) do - { - group: { - name: 'testing-as-moderator', - usernames: [admin.username, user.username].join(","), - owner_usernames: [user.username].join(","), - allow_membership_requests: true, - membership_request_template: 'Testing', - members_visibility_level: Group.visibility_levels[:staff] - } - } - end - - before do - SiteSetting.moderators_manage_categories_and_groups = true - end - - context "when the user is a moderator" do - before do - user.update!(moderator: true) - sign_in(user) - end - - it 'should allow groups to be created' do + it 'should work' do post "/admin/groups.json", params: group_params expect(response.status).to eq(200) group = Group.last - expect(group.name).to eq('testing-as-moderator') + expect(group.name).to eq('testing') expect(group.users).to contain_exactly(admin, user) expect(group.allow_membership_requests).to eq(true) expect(group.membership_request_template).to eq('Testing') expect(group.members_visibility_level).to eq(Group.visibility_levels[:staff]) end - it 'should allow group owners to be added' do + context "with custom_fields" do + before do + plugin = Plugin::Instance.new + plugin.register_editable_group_custom_field :test + end + + after do + DiscoursePluginRegistry.reset! + end + + it "only updates allowed user fields" do + params = group_params + params[:group].merge!(custom_fields: { test: :hello1, test2: :hello2 }) + + post "/admin/groups.json", params: params + + group = Group.last + + expect(response.status).to eq(200) + expect(group.custom_fields['test']).to eq('hello1') + expect(group.custom_fields['test2']).to be_blank + end + + it "is secure when there are no registered editable fields" do + DiscoursePluginRegistry.reset! + params = group_params + params[:group].merge!(custom_fields: { test: :hello1, test2: :hello2 }) + + post "/admin/groups.json", params: params + + group = Group.last + + expect(response.status).to eq(200) + expect(group.custom_fields['test']).to be_blank + expect(group.custom_fields['test2']).to be_blank + end + end + + context 'with Group.plugin_permitted_params' do + after do + DiscoursePluginRegistry.reset! + end + + it 'filter unpermitted params' do + params = group_params + params[:group].merge!(allow_unknown_sender_topic_replies: true) + + post "/admin/groups.json", params: params + expect(Group.last.allow_unknown_sender_topic_replies).to eq(false) + end + + it 'allows plugin to allow custom params' do + params = group_params + params[:group].merge!(allow_unknown_sender_topic_replies: true) + + plugin = Plugin::Instance.new + plugin.register_group_param :allow_unknown_sender_topic_replies + + post "/admin/groups.json", params: params + expect(Group.last.allow_unknown_sender_topic_replies).to eq(true) + end + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + context "with moderators_manage_categories_and_groups enabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = true + end + + it "creates group" do + expect do + post "/admin/groups.json", params: group_params + end.to change { Group.count }.by(1) + + expect(response.status).to eq(200) + + group = Group.last + + expect(group.name).to eq('testing') + expect(group.users).to contain_exactly(admin, user) + expect(group.allow_membership_requests).to eq(true) + expect(group.membership_request_template).to eq('Testing') + expect(group.members_visibility_level).to eq(Group.visibility_levels[:staff]) + end + end + + context "with moderators_manage_categories_and_groups disabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = false + end + + it "prevents creation with a 403 response" do + expect do + post "/admin/groups.json", params: group_params + end.to_not change { Group.count } + + expect(response.status).to eq(403) + expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) + end + end + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + it "prevents creation with a 404 response" do + expect do + post "/admin/groups.json", params: group_params + end.to_not change { Group.count } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end + end + end + + describe '#add_owners' do + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'should work' do put "/admin/groups/#{group.id}/owners.json", params: { group: { usernames: [user.username, admin.username].join(",") @@ -396,7 +177,140 @@ RSpec.describe Admin::GroupsController do .to contain_exactly(user, admin) end - it 'should allow groups owners to be removed' do + it 'returns not-found error when there is no group' do + group.destroy! + + put "/admin/groups/#{group.id}/owners.json", params: { + group: { + usernames: user.username + } + } + + expect(response.status).to eq(404) + end + + it 'does not allow adding owners to an automatic group' do + group.update!(automatic: true) + + expect do + put "/admin/groups/#{group.id}/owners.json", params: { + group: { + usernames: user.username + } + } + end.to_not change { group.group_users.count } + + expect(response.status).to eq(422) + expect(response.parsed_body["errors"]).to eq(["You cannot modify an automatic group"]) + end + + it 'does not notify users when the param is not present' do + put "/admin/groups/#{group.id}/owners.json", params: { + group: { + usernames: user.username + } + } + expect(response.status).to eq(200) + + topic = Topic.find_by( + title: I18n.t("system_messages.user_added_to_group_as_owner.subject_template", group_name: group.name), + archetype: "private_message" + ) + expect(topic.nil?).to eq(true) + end + + it 'notifies users when the param is present' do + put "/admin/groups/#{group.id}/owners.json", params: { + group: { + usernames: user.username, + notify_users: true + } + } + expect(response.status).to eq(200) + + topic = Topic.find_by( + title: I18n.t("system_messages.user_added_to_group_as_owner.subject_template", group_name: group.name), + archetype: "private_message" + ) + expect(topic.nil?).to eq(false) + expect(topic.topic_users.map(&:user_id)).to include(-1, user.id) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + context "with moderators_manage_categories_and_groups enabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = true + end + + it "adds owners" do + put "/admin/groups/#{group.id}/owners.json", params: { + group: { + usernames: [user.username, admin.username, moderator.username].join(",") + } + } + + response_body = response.parsed_body + + expect(response.status).to eq(200) + expect(response_body["usernames"]).to contain_exactly( + user.username, + admin.username, + moderator.username + ) + expect(group.group_users.where(owner: true).map(&:user)) + .to contain_exactly(user, admin, moderator) + end + end + + context "with moderators_manage_categories_and_groups disabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = false + end + + it "prevents adding of owners with a 403 response" do + put "/admin/groups/#{group.id}/owners.json", params: { + group: { + usernames: [user.username, admin.username, moderator.username].join(",") + } + } + + expect(response.status).to eq(403) + expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) + expect(group.group_users.where(owner: true).map(&:user)) + .to be_empty + end + end + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + it "prevents adding of owners with a 404 response" do + put "/admin/groups/#{group.id}/owners.json", params: { + group: { + usernames: [user.username, admin.username].join(",") + } + } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(group.group_users.where(owner: true).map(&:user)) + .to be_empty + end + end + end + + describe '#remove_owner' do + let(:user2) { Fabricate(:user) } + let(:user3) { Fabricate(:user) } + + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'should work' do group.add_owner(user) delete "/admin/groups/#{group.id}/owners.json", params: { @@ -406,32 +320,23 @@ RSpec.describe Admin::GroupsController do expect(response.status).to eq(200) expect(group.group_users.where(owner: true)).to eq([]) end - end - context "when the user is not a moderator or admin" do - before do - user.update!(moderator: false, admin: false) - sign_in(user) - end + it 'should work with multiple users' do + group.add_owner(user) + group.add_owner(user3) - it 'should not allow groups to be created' do - post "/admin/groups.json", params: group_params - - expect(response.status).to eq(404) - end - - it 'should not allow group owners to be added' do - put "/admin/groups/#{group.id}/owners.json", params: { + delete "/admin/groups/#{group.id}/owners.json", params: { group: { - usernames: [user.username, admin.username].join(",") + usernames: "#{user.username},#{user2.username},#{user3.username}" } } - expect(response.status).to eq(404) + expect(response.status).to eq(200) + expect(group.group_users.where(owner: true)).to eq([]) end - it 'should not allow groups owners to be removed' do - group.add_owner(user) + it 'returns not-found error when there is no group' do + group.destroy! delete "/admin/groups/#{group.id}/owners.json", params: { user_id: user.id @@ -439,6 +344,323 @@ RSpec.describe Admin::GroupsController do expect(response.status).to eq(404) end + + it 'does not allow removing owners from an automatic group' do + group.update!(automatic: true) + + delete "/admin/groups/#{group.id}/owners.json", params: { + user_id: user.id + } + + expect(response.status).to eq(422) + expect(response.parsed_body["errors"]).to eq(["You cannot modify an automatic group"]) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + context "with moderators_manage_categories_and_groups enabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = true + end + + it "removes owner" do + group.add_owner(user) + + delete "/admin/groups/#{group.id}/owners.json", params: { + user_id: user.id + } + + expect(response.status).to eq(200) + expect(group.group_users.where(owner: true)).to eq([]) + end + end + + context "with moderators_manage_categories_and_groups disabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = false + end + + it "prevents owner removal with a 403 response" do + group.add_owner(user) + + delete "/admin/groups/#{group.id}/owners.json", params: { + user_id: user.id + } + + expect(response.status).to eq(403) + expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) + expect(group.group_users.where(owner: true).map(&:user)).to contain_exactly(user) + end + end + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + it "prevents owner removal with a 404 response" do + group.add_owner(user) + + delete "/admin/groups/#{group.id}/owners.json", params: { + user_id: user.id + } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(group.group_users.where(owner: true).map(&:user)).to contain_exactly(user) + end + end + end + + describe "#set_primary" do + let(:user2) { Fabricate(:user) } + let(:user3) { Fabricate(:user) } + + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'sets with multiple users' do + user2.update!(primary_group_id: group.id) + + put "/admin/groups/#{group.id}/primary.json", params: { + group: { usernames: "#{user.username},#{user2.username},#{user3.username}" }, + primary: "true" + } + + expect(response.status).to eq(200) + expect(User.where(primary_group_id: group.id).size).to eq(3) + end + + it 'unsets with multiple users' do + user.update!(primary_group_id: group.id) + user3.update!(primary_group_id: group.id) + + put "/admin/groups/#{group.id}/primary.json", params: { + group: { usernames: "#{user.username},#{user2.username},#{user3.username}" }, + primary: "false" + } + + expect(response.status).to eq(200) + expect(User.where(primary_group_id: group.id).size).to eq(0) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + context "with moderators_manage_categories_and_groups enabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = true + end + + it "sets multiple primary users" do + user2.update!(primary_group_id: group.id) + + put "/admin/groups/#{group.id}/primary.json", params: { + group: { usernames: "#{user.username},#{user2.username},#{user3.username}" }, + primary: "true" + } + + expect(response.status).to eq(200) + expect(User.where(primary_group_id: group.id).size).to eq(3) + end + end + + context "with moderators_manage_categories_and_groups disabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = false + end + + it "sets multiple primary users" do + user2.update!(primary_group_id: group.id) + + put "/admin/groups/#{group.id}/primary.json", params: { + group: { usernames: "#{user.username},#{user2.username},#{user3.username}" }, + primary: "true" + } + + expect(response.status).to eq(200) + expect(User.where(primary_group_id: group.id).size).to eq(3) + end + end + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + it "prevents setting of primary user with a 404 response" do + user2.update!(primary_group_id: group.id) + + put "/admin/groups/#{group.id}/primary.json", params: { + group: { usernames: "#{user.username},#{user2.username},#{user3.username}" }, + primary: "true" + } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(User.where(primary_group_id: group.id).size).to eq(1) + end + end + end + + describe "#destroy" do + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'should return the right response for an invalid group_id' do + max_id = Group.maximum(:id).to_i + delete "/admin/groups/#{max_id + 1}.json" + expect(response.status).to eq(404) + end + + it 'logs when a group is destroyed' do + delete "/admin/groups/#{group.id}.json" + + history = UserHistory.where(acting_user: admin).last + + expect(history).to be_present + expect(history.details).to include("name: #{group.name}") + expect(history.details).to include("id: #{group.id}") + end + + it 'logs the grant_trust_level attribute' do + trust_level = TrustLevel[4] + group.update!(grant_trust_level: trust_level) + delete "/admin/groups/#{group.id}.json" + + history = UserHistory.where(acting_user: admin).last + + expect(history).to be_present + expect(history.details).to include("grant_trust_level: #{trust_level}") + expect(history.details).to include("name: #{group.name}") + end + + context "when group is automatic" do + it "returns the right response" do + group.update!(automatic: true) + + delete "/admin/groups/#{group.id}.json" + + expect(response.status).to eq(422) + expect(Group.find(group.id)).to eq(group) + end + end + + context "when group is not automatic" do + it "returns the right response" do + delete "/admin/groups/#{group.id}.json" + + expect(response.status).to eq(200) + expect(Group.find_by(id: group.id)).to eq(nil) + end + end + end + + shared_examples "group deletion not allowed" do + it "prevents deletion with a 404 response" do + expect do + delete "/admin/groups/#{group.id}.json" + end.not_to change { Group.count } + + 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) } + + context "with moderators_manage_categories_and_groups enabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = true + end + + include_examples "group deletion not allowed" + end + + context "with moderators_manage_categories_and_groups disabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = false + end + + include_examples "group deletion not allowed" + end + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "group deletion not allowed" + end + end + + describe '#automatic_membership_count' do + context "when logged in as admin" do + before { sign_in(admin) } + + it 'returns count of users whose emails match the domain' do + Fabricate(:user, email: 'user1@somedomain.org') + Fabricate(:user, email: 'user1@somedomain.com') + Fabricate(:user, email: 'user1@notsomedomain.com') + group = Fabricate(:group) + + put "/admin/groups/automatic_membership_count.json", params: { + automatic_membership_email_domains: 'somedomain.org|somedomain.com', + id: group.id + } + expect(response.status).to eq(200) + expect(response.parsed_body["user_count"]).to eq(2) + end + + it "doesn't responde with 500 if domain is invalid" do + group = Fabricate(:group) + + put "/admin/groups/automatic_membership_count.json", params: { + automatic_membership_email_domains: '@somedomain.org|@somedomain.com', + id: group.id + } + expect(response.status).to eq(200) + expect(response.parsed_body["user_count"]).to eq(0) + end + end + + shared_examples "automatic membership count inaccessible" do + it "denies access with a 404 response" do + put "/admin/groups/automatic_membership_count.json", params: { + automatic_membership_email_domains: 'somedomain.org|somedomain.com', + id: group.id + } + + 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) } + + context "with moderators_manage_categories_and_groups enabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = true + end + + include_examples "automatic membership count inaccessible" + end + + context "with moderators_manage_categories_and_groups disabled" do + before do + SiteSetting.moderators_manage_categories_and_groups = false + end + + include_examples "automatic membership count inaccessible" + end + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "automatic membership count inaccessible" end end end diff --git a/spec/requests/admin/impersonate_controller_spec.rb b/spec/requests/admin/impersonate_controller_spec.rb index a95527a4c1d..b60edd3c5ae 100644 --- a/spec/requests/admin/impersonate_controller_spec.rb +++ b/spec/requests/admin/impersonate_controller_spec.rb @@ -1,28 +1,48 @@ # frozen_string_literal: true RSpec.describe Admin::ImpersonateController do + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } + fab!(:another_admin) { Fabricate(:admin) } - it "is a subclass of AdminController" do - expect(Admin::ImpersonateController < Admin::AdminController).to eq(true) - end + describe '#index' do + context "when logged in as an admin" do + before { sign_in(admin) } - context 'while logged in as an admin' do - fab!(:admin) { Fabricate(:admin) } - fab!(:user) { Fabricate(:user) } - fab!(:another_admin) { Fabricate(:admin) } - - before do - sign_in(admin) - end - - describe '#index' do it 'returns success' do get "/admin/impersonate.json" + expect(response.status).to eq(200) end end - describe '#create' do + shared_examples "impersonation inaccessible" do + it "denies access with a 404 response" do + get "/admin/impersonate.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 "impersonation inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "impersonation inaccessible" + end + end + + describe '#create' do + context "when logged in as an admin" do + before { sign_in(admin) } + it 'requires a username_or_email parameter' do post "/admin/impersonate.json" expect(response.status).to eq(400) @@ -58,5 +78,32 @@ RSpec.describe Admin::ImpersonateController do end end end + + shared_examples "impersonation not allowed" do + it "prevents impersonation with a with 404 response" do + expect do + post "/admin/impersonate.json", params: { username_or_email: user.username } + end.not_to change { UserHistory.where(action: UserHistory.actions[:impersonate]).count } + + expect(response.status).to eq(404) + expect(session[:current_user_id]).to eq(current_user.id) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "impersonation not allowed" do + let(:current_user) { moderator } + end + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "impersonation not allowed" do + let(:current_user) { user } + end + end end end diff --git a/spec/requests/admin/permalinks_controller_spec.rb b/spec/requests/admin/permalinks_controller_spec.rb index 56cc9ec5bd6..8ef11d4e330 100644 --- a/spec/requests/admin/permalinks_controller_spec.rb +++ b/spec/requests/admin/permalinks_controller_spec.rb @@ -1,109 +1,160 @@ # frozen_string_literal: true RSpec.describe Admin::PermalinksController do - - it "is a subclass of AdminController" do - expect(Admin::PermalinksController < Admin::AdminController).to eq(true) - end - fab!(:admin) { Fabricate(:admin) } - - before do - sign_in(admin) - end + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } describe '#index' do - it 'filters url' do - Fabricate(:permalink, url: "/forum/23") - Fabricate(:permalink, url: "/forum/98") - Fabricate(:permalink, url: "/discuss/topic/45") - Fabricate(:permalink, url: "/discuss/topic/76") + context "when logged in as an admin" do + before { sign_in(admin) } - get "/admin/permalinks.json", params: { filter: "topic" } + it 'filters url' do + Fabricate(:permalink, url: "/forum/23") + Fabricate(:permalink, url: "/forum/98") + Fabricate(:permalink, url: "/discuss/topic/45") + Fabricate(:permalink, url: "/discuss/topic/76") - expect(response.status).to eq(200) - result = response.parsed_body - expect(result.length).to eq(2) + get "/admin/permalinks.json", params: { filter: "topic" } + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result.length).to eq(2) + end + + it 'filters external url' do + Fabricate(:permalink, external_url: "http://google.com") + Fabricate(:permalink, external_url: "http://wikipedia.org") + Fabricate(:permalink, external_url: "http://www.discourse.org") + Fabricate(:permalink, external_url: "http://try.discourse.org") + + get "/admin/permalinks.json", params: { filter: "discourse" } + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result.length).to eq(2) + end + + it 'filters url and external url both' do + Fabricate(:permalink, url: "/forum/23", external_url: "http://google.com") + Fabricate(:permalink, url: "/discourse/98", external_url: "http://wikipedia.org") + Fabricate(:permalink, url: "/discuss/topic/45", external_url: "http://discourse.org") + Fabricate(:permalink, url: "/discuss/topic/76", external_url: "http://try.discourse.org") + + get "/admin/permalinks.json", params: { filter: "discourse" } + + expect(response.status).to eq(200) + result = response.parsed_body + expect(result.length).to eq(3) + end end - it 'filters external url' do - Fabricate(:permalink, external_url: "http://google.com") - Fabricate(:permalink, external_url: "http://wikipedia.org") - Fabricate(:permalink, external_url: "http://www.discourse.org") - Fabricate(:permalink, external_url: "http://try.discourse.org") + shared_examples "permalinks inaccessible" do + it "denies access with a 404 response" do + get "/admin/permalinks.json", params: { filter: "topic" } - get "/admin/permalinks.json", params: { filter: "discourse" } - - expect(response.status).to eq(200) - result = response.parsed_body - expect(result.length).to eq(2) + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end - it 'filters url and external url both' do - Fabricate(:permalink, url: "/forum/23", external_url: "http://google.com") - Fabricate(:permalink, url: "/discourse/98", external_url: "http://wikipedia.org") - Fabricate(:permalink, url: "/discuss/topic/45", external_url: "http://discourse.org") - Fabricate(:permalink, url: "/discuss/topic/76", external_url: "http://try.discourse.org") + context "when logged in as a moderator" do + before { sign_in(moderator) } - get "/admin/permalinks.json", params: { filter: "discourse" } + include_examples "permalinks inaccessible" + end - expect(response.status).to eq(200) - result = response.parsed_body - expect(result.length).to eq(3) + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "permalinks inaccessible" end end describe "#create" do - it "works for topics" do - topic = Fabricate(:topic) + context "when logged in as an admin" do + before { sign_in(admin) } - post "/admin/permalinks.json", params: { - url: "/topics/771", - permalink_type: "topic_id", - permalink_type_value: topic.id - } + it "works for topics" do + topic = Fabricate(:topic) - expect(response.status).to eq(200) - expect(Permalink.last).to have_attributes(url: "topics/771", topic_id: topic.id, post_id: nil, category_id: nil, tag_id: nil) + post "/admin/permalinks.json", params: { + url: "/topics/771", + permalink_type: "topic_id", + permalink_type_value: topic.id + } + + expect(response.status).to eq(200) + expect(Permalink.last).to have_attributes(url: "topics/771", topic_id: topic.id, post_id: nil, category_id: nil, tag_id: nil) + end + + it "works for posts" do + some_post = Fabricate(:post) + + post "/admin/permalinks.json", params: { + url: "/topics/771/8291", + permalink_type: "post_id", + permalink_type_value: some_post.id + } + + expect(response.status).to eq(200) + expect(Permalink.last).to have_attributes(url: "topics/771/8291", topic_id: nil, post_id: some_post.id, category_id: nil, tag_id: nil) + end + + it "works for categories" do + category = Fabricate(:category) + + post "/admin/permalinks.json", params: { + url: "/forums/11", + permalink_type: "category_id", + permalink_type_value: category.id + } + + expect(response.status).to eq(200) + expect(Permalink.last).to have_attributes(url: "forums/11", topic_id: nil, post_id: nil, category_id: category.id, tag_id: nil) + end + + it "works for tags" do + tag = Fabricate(:tag) + + post "/admin/permalinks.json", params: { + url: "/forums/12", + permalink_type: "tag_name", + permalink_type_value: tag.name + } + + expect(response.status).to eq(200) + expect(Permalink.last).to have_attributes(url: "forums/12", topic_id: nil, post_id: nil, category_id: nil, tag_id: tag.id) + end end - it "works for posts" do - some_post = Fabricate(:post) + shared_examples "permalink creation not allowed" do + it "prevents creation with a 404 response" do + topic = Fabricate(:topic) - post "/admin/permalinks.json", params: { - url: "/topics/771/8291", - permalink_type: "post_id", - permalink_type_value: some_post.id - } + expect do + post "/admin/permalinks.json", params: { + url: "/topics/771", + permalink_type: "topic_id", + permalink_type_value: topic.id + } + end.not_to change { Permalink.count } - expect(response.status).to eq(200) - expect(Permalink.last).to have_attributes(url: "topics/771/8291", topic_id: nil, post_id: some_post.id, category_id: nil, tag_id: nil) + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end - it "works for categories" do - category = Fabricate(:category) + context "when logged in as a moderator" do + before { sign_in(moderator) } - post "/admin/permalinks.json", params: { - url: "/forums/11", - permalink_type: "category_id", - permalink_type_value: category.id - } - - expect(response.status).to eq(200) - expect(Permalink.last).to have_attributes(url: "forums/11", topic_id: nil, post_id: nil, category_id: category.id, tag_id: nil) + include_examples "permalink creation not allowed" end - it "works for tags" do - tag = Fabricate(:tag) + context "when logged in as a non-staff user" do + before { sign_in(user) } - post "/admin/permalinks.json", params: { - url: "/forums/12", - permalink_type: "tag_name", - permalink_type_value: tag.name - } - - expect(response.status).to eq(200) - expect(Permalink.last).to have_attributes(url: "forums/12", topic_id: nil, post_id: nil, category_id: nil, tag_id: tag.id) + include_examples "permalink creation not allowed" end end end diff --git a/spec/requests/admin/plugins_controller_spec.rb b/spec/requests/admin/plugins_controller_spec.rb index fe83c428f63..de2b8225030 100644 --- a/spec/requests/admin/plugins_controller_spec.rb +++ b/spec/requests/admin/plugins_controller_spec.rb @@ -1,20 +1,42 @@ # frozen_string_literal: true RSpec.describe Admin::PluginsController do + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } - it "is a subclass of StaffController" do - expect(Admin::PluginsController < Admin::StaffController).to eq(true) - end + describe "#index" do + context "while logged in as an admin" do + before { sign_in(admin) } - context "while logged in as an admin" do - before do - sign_in(Fabricate(:admin)) + it "returns plugins" do + get "/admin/plugins.json" + + expect(response.status).to eq(200) + expect(response.parsed_body.has_key?('plugins')).to eq(true) + end end - it 'should return JSON' do - get "/admin/plugins.json" - expect(response.status).to eq(200) - expect(response.parsed_body.has_key?('plugins')).to eq(true) + context "when logged in as a moderator" do + before { sign_in(moderator) } + + it "returns plugins" do + get "/admin/plugins.json" + + expect(response.status).to eq(200) + expect(response.parsed_body.has_key?('plugins')).to eq(true) + 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/plugins.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end end end diff --git a/spec/requests/admin/reports_controller_spec.rb b/spec/requests/admin/reports_controller_spec.rb index 41c21946fbe..71d5f52cb83 100644 --- a/spec/requests/admin/reports_controller_spec.rb +++ b/spec/requests/admin/reports_controller_spec.rb @@ -1,19 +1,14 @@ # frozen_string_literal: true RSpec.describe Admin::ReportsController do - it "is a subclass of StaffController" do - expect(Admin::ReportsController < Admin::StaffController).to eq(true) - end + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } - context 'while logged in as an admin' do - fab!(:admin) { Fabricate(:admin) } - fab!(:user) { Fabricate(:user) } + describe '#bulk' do + context "when logged in as an admin" do + before { sign_in(admin) } - before do - sign_in(admin) - end - - describe '#bulk' do context "with valid params" do it "renders the reports as JSON" do Fabricate(:topic) @@ -66,7 +61,45 @@ RSpec.describe Admin::ReportsController do end end - describe '#show' do + context "when logged in as a moderator" do + before { sign_in(moderator) } + + it "returns report" do + Fabricate(:topic) + + get "/admin/reports/bulk.json", params: { + reports: { + topics: { limit: 10 }, + likes: { limit: 10 } + } + } + + expect(response.status).to eq(200) + expect(response.parsed_body["reports"].count).to eq(2) + 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/reports/bulk.json", params: { + reports: { + topics: { limit: 10 }, + not_found: { limit: 10 } + } + } + + 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 invalid id form" do let(:invalid_id) { "!!&asdfasdf" } @@ -131,5 +164,29 @@ RSpec.describe Admin::ReportsController do end end end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + it "returns report" do + Fabricate(:topic) + + get "/admin/reports/topics.json" + + expect(response.status).to eq(200) + expect(response.parsed_body["report"]["total"]).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/reports/topics.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end + end end end diff --git a/spec/requests/admin/robots_txt_controller_spec.rb b/spec/requests/admin/robots_txt_controller_spec.rb index f39b369d3e0..fd9464c3add 100644 --- a/spec/requests/admin/robots_txt_controller_spec.rb +++ b/spec/requests/admin/robots_txt_controller_spec.rb @@ -1,31 +1,123 @@ # frozen_string_literal: true RSpec.describe Admin::RobotsTxtController do - it "is a subclass of AdminController" do - expect(described_class < Admin::AdminController).to eq(true) - end - fab!(:admin) { Fabricate(:admin) } fab!(:moderator) { Fabricate(:moderator) } fab!(:user) { Fabricate(:user) } - context "when logged in as a non-admin user" do - shared_examples "access_forbidden" do - it "can't see #show" do + describe "#show" do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "returns default content if there are no overrides" do get "/admin/customize/robots.json" - expect(response.status).to eq(404) + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["robots_txt"]).to be_present + expect(json["overridden"]).to eq(false) end - it "can't perform #update" do - put "/admin/customize/robots.json", params: { robots_txt: "adasdasd" } + it "returns overridden content if there are overrides" do + SiteSetting.overridden_robots_txt = "something" + get "/admin/customize/robots.json" + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["robots_txt"]).to eq("something") + expect(json["overridden"]).to eq(true) + end + end + + shared_examples "robot.txt inaccessible" do + it "denies access with a 404 response" do + get "/admin/customize/robots.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 "robot.txt inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "robot.txt inaccessible" + end + end + + describe "#update" do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "overrides the site's default robots.txt" do + put "/admin/customize/robots.json", params: { robots_txt: "new_content" } + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["robots_txt"]).to eq("new_content") + expect(json["overridden"]).to eq(true) + expect(SiteSetting.overridden_robots_txt).to eq("new_content") + + get "/robots.txt" + expect(response.body).to include("new_content") + end + + it "requires `robots_txt` param to be present" do + SiteSetting.overridden_robots_txt = "overridden_content" + put "/admin/customize/robots.json", params: { robots_txt: "" } + expect(response.status).to eq(400) + end + end + + shared_examples "robot.txt update not allowed" do + it "prevents updates with a 404 response" do + put "/admin/customize/robots.json", params: { robots_txt: "adasdasd" } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(SiteSetting.overridden_robots_txt).to eq("") end + end - it "can't perform #reset" do + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "robot.txt update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "robot.txt update not allowed" + end + end + + describe "#reset" do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "resets robots.txt file to the default version" do SiteSetting.overridden_robots_txt = "overridden_content" delete "/admin/customize/robots.json" + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["robots_txt"]).not_to include("overridden_content") + expect(json["overridden"]).to eq(false) + expect(SiteSetting.overridden_robots_txt).to eq("") + end + end + + shared_examples "robot.txt reset not allowed" do + it "prevents resets with a 404 response" do + SiteSetting.overridden_robots_txt = "overridden_content" + + delete "/admin/customize/robots.json" + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) expect(SiteSetting.overridden_robots_txt).to eq("overridden_content") end end @@ -33,70 +125,13 @@ RSpec.describe Admin::RobotsTxtController do context "when logged in as a moderator" do before { sign_in(moderator) } - include_examples "access_forbidden" + include_examples "robot.txt reset not allowed" end - context "when logged in as non-staff user" do - before { sign_in(user) } + context "when logged in as a non-staff user" do + before { sign_in(user) } - include_examples "access_forbidden" - end - end - - describe "#show" do - before { sign_in(admin) } - - it "returns default content if there are no overrides" do - get "/admin/customize/robots.json" - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["robots_txt"]).to be_present - expect(json["overridden"]).to eq(false) - end - - it "returns overridden content if there are overrides" do - SiteSetting.overridden_robots_txt = "something" - get "/admin/customize/robots.json" - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["robots_txt"]).to eq("something") - expect(json["overridden"]).to eq(true) - end - end - - describe "#update" do - before { sign_in(admin) } - - it "overrides the site's default robots.txt" do - put "/admin/customize/robots.json", params: { robots_txt: "new_content" } - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["robots_txt"]).to eq("new_content") - expect(json["overridden"]).to eq(true) - expect(SiteSetting.overridden_robots_txt).to eq("new_content") - - get "/robots.txt" - expect(response.body).to include("new_content") - end - - it "requires `robots_txt` param to be present" do - SiteSetting.overridden_robots_txt = "overridden_content" - put "/admin/customize/robots.json", params: { robots_txt: "" } - expect(response.status).to eq(400) - end - end - - describe "#reset" do - before { sign_in(admin) } - - it "resets robots.txt file to the default version" do - SiteSetting.overridden_robots_txt = "overridden_content" - delete "/admin/customize/robots.json" - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["robots_txt"]).not_to include("overridden_content") - expect(json["overridden"]).to eq(false) - expect(SiteSetting.overridden_robots_txt).to eq("") + include_examples "robot.txt reset not allowed" end end end diff --git a/spec/requests/admin/screened_emails_controller_spec.rb b/spec/requests/admin/screened_emails_controller_spec.rb index 8d9fc482700..da2b177d507 100644 --- a/spec/requests/admin/screened_emails_controller_spec.rb +++ b/spec/requests/admin/screened_emails_controller_spec.rb @@ -1,21 +1,78 @@ # frozen_string_literal: true RSpec.describe Admin::ScreenedEmailsController do - it "is a subclass of StaffController" do - expect(Admin::ScreenedEmailsController < Admin::StaffController).to eq(true) - end + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } + fab!(:screened_email) { Fabricate(:screened_email) } describe '#index' do - before do - sign_in(Fabricate(:admin)) + shared_examples "screened emails accessible" do + it "returns screened emails" do + get "/admin/logs/screened_emails.json" + + expect(response.status).to eq(200) + json = response.parsed_body + expect(json.size).to eq(1) + end end - it 'returns JSON' do - Fabricate(:screened_email) - get "/admin/logs/screened_emails.json" - expect(response.status).to eq(200) - json = response.parsed_body - expect(json.size).to eq(1) + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "screened emails accessible" + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "screened emails accessible" + 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/logs/screened_emails.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end + end + end + + describe "#destroy" do + shared_examples "screened email deletion possible" do + it "deletes screened email" do + expect do + delete "/admin/logs/screened_emails/#{screened_email.id}.json" + end.to change { ScreenedEmail.count }.by(-1) + + expect(response.status).to eq(200) + end + end + + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "screened email deletion possible" + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "screened email deletion possible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + it "prevents deletion with a 404 response" do + delete "/admin/logs/screened_emails/#{screened_email.id}.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end end end diff --git a/spec/requests/admin/screened_ip_addresses_controller_spec.rb b/spec/requests/admin/screened_ip_addresses_controller_spec.rb index f6f2a068805..3073b509793 100644 --- a/spec/requests/admin/screened_ip_addresses_controller_spec.rb +++ b/spec/requests/admin/screened_ip_addresses_controller_spec.rb @@ -1,47 +1,65 @@ # frozen_string_literal: true RSpec.describe Admin::ScreenedIpAddressesController do - - it "is a subclass of StaffController" do - expect(Admin::ScreenedIpAddressesController < Admin::StaffController).to eq(true) - end - fab!(:admin) { Fabricate(:admin) } - - before do - sign_in(admin) - end + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } describe '#index' do - it 'filters screened ip addresses' do - Fabricate(:screened_ip_address, ip_address: "1.2.3.4") - Fabricate(:screened_ip_address, ip_address: "1.2.3.5") - Fabricate(:screened_ip_address, ip_address: "1.2.3.6") - Fabricate(:screened_ip_address, ip_address: "4.5.6.7") - Fabricate(:screened_ip_address, ip_address: "5.0.0.0/8") + shared_examples "screened ip addresses accessible" do + it 'filters screened ip addresses' do + Fabricate(:screened_ip_address, ip_address: "1.2.3.4") + Fabricate(:screened_ip_address, ip_address: "1.2.3.5") + Fabricate(:screened_ip_address, ip_address: "1.2.3.6") + Fabricate(:screened_ip_address, ip_address: "4.5.6.7") + Fabricate(:screened_ip_address, ip_address: "5.0.0.0/8") - get "/admin/logs/screened_ip_addresses.json", params: { filter: "1.2.*" } + get "/admin/logs/screened_ip_addresses.json", params: { filter: "1.2.*" } - expect(response.status).to eq(200) - expect(response.parsed_body.map { |record| record["ip_address"] }) - .to contain_exactly("1.2.3.4", "1.2.3.5", "1.2.3.6") + expect(response.status).to eq(200) + expect(response.parsed_body.map { |record| record["ip_address"] }) + .to contain_exactly("1.2.3.4", "1.2.3.5", "1.2.3.6") - get "/admin/logs/screened_ip_addresses.json", params: { filter: "4.5.6.7" } + get "/admin/logs/screened_ip_addresses.json", params: { filter: "4.5.6.7" } - expect(response.status).to eq(200) - expect(response.parsed_body.map { |record| record["ip_address"] }) - .to contain_exactly("4.5.6.7") + expect(response.status).to eq(200) + expect(response.parsed_body.map { |record| record["ip_address"] }) + .to contain_exactly("4.5.6.7") - get "/admin/logs/screened_ip_addresses.json", params: { filter: "5.0.0.1" } + get "/admin/logs/screened_ip_addresses.json", params: { filter: "5.0.0.1" } - expect(response.status).to eq(200) - expect(response.parsed_body.map { |record| record["ip_address"] }) - .to contain_exactly("5.0.0.0/8") + expect(response.status).to eq(200) + expect(response.parsed_body.map { |record| record["ip_address"] }) + .to contain_exactly("5.0.0.0/8") - get "/admin/logs/screened_ip_addresses.json", params: { filter: "6.0.0.1" } + get "/admin/logs/screened_ip_addresses.json", params: { filter: "6.0.0.1" } - expect(response.status).to eq(200) - expect(response.parsed_body).to be_blank + expect(response.status).to eq(200) + expect(response.parsed_body).to be_blank + end + end + + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "screened ip addresses accessible" + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "screened ip addresses accessible" + 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/logs/screened_ip_addresses.json", params: { filter: "1.2.*" } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end end end diff --git a/spec/requests/admin/screened_urls_controller_spec.rb b/spec/requests/admin/screened_urls_controller_spec.rb index a4d5cb2d176..c7db4dba0b9 100644 --- a/spec/requests/admin/screened_urls_controller_spec.rb +++ b/spec/requests/admin/screened_urls_controller_spec.rb @@ -1,21 +1,43 @@ # frozen_string_literal: true RSpec.describe Admin::ScreenedUrlsController do - it "is a subclass of StaffController" do - expect(Admin::ScreenedUrlsController < Admin::StaffController).to eq(true) - end + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } + fab!(:screened_url) { Fabricate(:screened_url) } describe '#index' do - before do - sign_in(Fabricate(:admin)) + shared_examples "screened urls accessible" do + it "returns screened urls" do + get "/admin/logs/screened_urls.json" + + expect(response.status).to eq(200) + json = response.parsed_body + expect(json.size).to eq(1) + end end - it 'returns JSON' do - Fabricate(:screened_url) - get "/admin/logs/screened_urls.json" - expect(response.status).to eq(200) - json = response.parsed_body - expect(json.size).to eq(1) + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "screened urls accessible" + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "screened urls accessible" + 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/logs/screened_urls.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end end end diff --git a/spec/requests/admin/search_logs_spec.rb b/spec/requests/admin/search_logs_spec.rb index 7692d528d71..caab13f5793 100644 --- a/spec/requests/admin/search_logs_spec.rb +++ b/spec/requests/admin/search_logs_spec.rb @@ -6,99 +6,89 @@ RSpec.describe Admin::SearchLogsController do fab!(:user) { Fabricate(:user) } before do - SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1') + SearchLog.log(term: "ruby", search_type: :header, ip_address: "127.0.0.1") end after do SearchLog.clear_debounce_cache! end - it "is a subclass of StaffController" do - expect(Admin::SearchLogsController < Admin::StaffController).to eq(true) - end - describe "#index" do - it "raises an error if you aren't logged in" do - get '/admin/logs/search_logs.json' - expect(response.status).to eq(404) + shared_examples "search logs accessible" do + it "returns search logs" do + get '/admin/logs/search_logs.json' + + expect(response.status).to eq(200) + + json = response.parsed_body + expect(json[0]["term"]).to eq("ruby") + expect(json[0]["searches"]).to eq(1) + expect(json[0]["ctr"]).to eq(0) + end end - it "raises an error if you aren't an admin" do - sign_in(user) - get '/admin/logs/search_logs.json' - expect(response.status).to eq(404) + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "search logs accessible" end - it "should work if you are an admin" do - sign_in(admin) - get '/admin/logs/search_logs.json' + context "when logged in as a moderator" do + before { sign_in(moderator) } - expect(response.status).to eq(200) - - json = response.parsed_body - expect(json[0]['term']).to eq('ruby') - expect(json[0]['searches']).to eq(1) - expect(json[0]['ctr']).to eq(0) + include_examples "search logs accessible" end - it "should work if you are a moderator" do - sign_in(moderator) - get "/admin/logs/search_logs.json" + context "when logged in as a non-staff user" do + before { sign_in(user) } - expect(response.status).to eq(200) + it "denies access with a 404 response" do + get "/admin/logs/search_logs.json" - json = response.parsed_body - expect(json[0]["term"]).to eq("ruby") - expect(json[0]["searches"]).to eq(1) - expect(json[0]["ctr"]).to eq(0) + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end end describe "#term" do - it "raises an error if you aren't logged in" do - get '/admin/logs/search_logs/term.json', params: { - term: "ruby" - } + shared_examples "search log term accessible" do + it "returns search log term" do + get '/admin/logs/search_logs/term.json', params: { + term: "ruby" + } - expect(response.status).to eq(404) + expect(response.status).to eq(200) + + json = response.parsed_body + expect(json['term']['type']).to eq('search_log_term') + expect(json['term']['search_result']).to be_present + end end - it "raises an error if you aren't an admin" do - sign_in(user) + context "when logged in as an admin" do + before { sign_in(admin) } - get '/admin/logs/search_logs/term.json', params: { - term: "ruby" - } - - expect(response.status).to eq(404) + include_examples "search log term accessible" end - it "should work if you are an admin" do - sign_in(admin) + context "when logged in as a moderator" do + before { sign_in(moderator) } - get '/admin/logs/search_logs/term.json', params: { - term: "ruby" - } - - expect(response.status).to eq(200) - - json = response.parsed_body - expect(json['term']['type']).to eq('search_log_term') - expect(json['term']['search_result']).to be_present + include_examples "search log term accessible" end - it "should work if you are a moderator" do - sign_in(moderator) + context "when logged in as a non-staff user" do + before { sign_in(user) } - get "/admin/logs/search_logs/term.json", params: { - term: "ruby" - } + it "denies access with a 404 response" do + get "/admin/logs/search_logs/term.json", params: { + term: "ruby" + } - expect(response.status).to eq(200) - - json = response.parsed_body - expect(json["term"]["type"]).to eq("search_log_term") - expect(json["term"]["search_result"]).to be_present + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end end end diff --git a/spec/requests/admin/site_settings_controller_spec.rb b/spec/requests/admin/site_settings_controller_spec.rb index 8837fadf492..64fc9b6c6b4 100644 --- a/spec/requests/admin/site_settings_controller_spec.rb +++ b/spec/requests/admin/site_settings_controller_spec.rb @@ -1,21 +1,17 @@ # frozen_string_literal: true RSpec.describe Admin::SiteSettingsController do + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } - it "is a subclass of AdminController" do - expect(Admin::SiteSettingsController < Admin::AdminController).to eq(true) - end + describe "#index" do + context "when logged in as an admin" do + before { sign_in(admin) } - context 'while logged in as an admin' do - fab!(:admin) { Fabricate(:admin) } - - before do - sign_in(admin) - end - - describe '#index' do - it 'returns valid info' do + it "returns valid info" do get "/admin/site_settings.json" + expect(response.status).to eq(200) json = response.parsed_body expect(json["site_settings"].length).to be > 100 @@ -28,12 +24,205 @@ RSpec.describe Admin::SiteSettingsController do end end - describe '#update' do - before do - SiteSetting.setting(:test_setting, "default") - SiteSetting.setting(:test_upload, "", type: :upload) - SiteSetting.refresh! + shared_examples "site settings inaccessible" do + it "denies access with a 404 response" do + get "/admin/site_settings.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 "site settings inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "site settings inaccessible" + end + end + + describe "#user_count" do + fab!(:staged_user) { Fabricate(:staged) } + let(:tracking) { NotificationLevels.all[:tracking] } + + before do + SiteSetting.setting(:test_setting, "default") + SiteSetting.setting(:test_upload, "", type: :upload) + SiteSetting.refresh! + end + + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'should return correct user count for default categories change' do + category_id = Fabricate(:category).id + + put "/admin/site_settings/default_categories_watching/user_count.json", params: { + default_categories_watching: category_id + } + + expect(response.parsed_body["user_count"]).to eq(User.real.where(staged: false).count) + + CategoryUser.create!(category_id: category_id, notification_level: tracking, user: user) + + put "/admin/site_settings/default_categories_watching/user_count.json", params: { + default_categories_watching: category_id + } + + expect(response.parsed_body["user_count"]).to eq(User.real.where(staged: false).count - 1) + + SiteSetting.setting(:default_categories_watching, "") + end + + it 'should return correct user count for default tags change' do + tag = Fabricate(:tag) + + put "/admin/site_settings/default_tags_watching/user_count.json", params: { + default_tags_watching: tag.name + } + + expect(response.parsed_body["user_count"]).to eq(User.real.where(staged: false).count) + + TagUser.create!(tag_id: tag.id, notification_level: tracking, user: user) + + put "/admin/site_settings/default_tags_watching/user_count.json", params: { + default_tags_watching: tag.name + } + + expect(response.parsed_body["user_count"]).to eq(User.real.where(staged: false).count - 1) + + SiteSetting.setting(:default_tags_watching, "") + end + + context "for sidebar defaults" do + it 'returns the right count for the default_sidebar_categories site setting' do + category = Fabricate(:category) + + put "/admin/site_settings/default_sidebar_categories/user_count.json", params: { + default_sidebar_categories: "#{category.id}" + } + + expect(response.status).to eq(200) + expect(response.parsed_body["user_count"]).to eq(User.real.not_staged.count) + end + + it 'returns the right count for the default_sidebar_tags site setting' do + tag = Fabricate(:tag) + + put "/admin/site_settings/default_sidebar_tags/user_count.json", params: { + default_sidebar_tags: "#{tag.name}" + } + + expect(response.status).to eq(200) + expect(response.parsed_body["user_count"]).to eq(User.real.not_staged.count) + end + end + + context "with user options" do + def expect_user_count(site_setting_name:, user_setting_name:, current_site_setting_value:, new_site_setting_value:, + current_user_setting_value: nil, new_user_setting_value: nil) + + current_user_setting_value ||= current_site_setting_value + new_user_setting_value ||= new_site_setting_value + + SiteSetting.public_send("#{site_setting_name}=", current_site_setting_value) + UserOption.human_users.update_all(user_setting_name => current_user_setting_value) + user_count = User.human_users.count + + # Correctly counts users when all of them have default value + put "/admin/site_settings/#{site_setting_name}/user_count.json", params: { + site_setting_name => new_site_setting_value + } + expect(response.parsed_body["user_count"]).to eq(user_count) + + # Correctly counts users when one of them already has new value + user.user_option.update!(user_setting_name => new_user_setting_value) + put "/admin/site_settings/#{site_setting_name}/user_count.json", params: { + site_setting_name => new_site_setting_value + } + expect(response.parsed_body["user_count"]).to eq(user_count - 1) + + # Correctly counts users when site setting value has been changed + SiteSetting.public_send("#{site_setting_name}=", new_site_setting_value) + put "/admin/site_settings/#{site_setting_name}/user_count.json", params: { + site_setting_name => current_site_setting_value + } + expect(response.parsed_body["user_count"]).to eq(1) + end + + it "should return correct user count for boolean setting" do + expect_user_count( + site_setting_name: "default_other_external_links_in_new_tab", + user_setting_name: "external_links_in_new_tab", + current_site_setting_value: false, + new_site_setting_value: true + ) + end + + it "should return correct user count for 'text_size_key'" do + expect_user_count( + site_setting_name: "default_text_size", + user_setting_name: "text_size_key", + current_site_setting_value: "normal", + new_site_setting_value: "larger", + current_user_setting_value: UserOption.text_sizes[:normal], + new_user_setting_value: UserOption.text_sizes[:larger] + ) + end + + it "should return correct user count for 'title_count_mode_key'" do + expect_user_count( + site_setting_name: "default_title_count_mode", + user_setting_name: "title_count_mode_key", + current_site_setting_value: "notifications", + new_site_setting_value: "contextual", + current_user_setting_value: UserOption.title_count_modes[:notifications], + new_user_setting_value: UserOption.title_count_modes[:contextual] + ) + end + end + end + + shared_examples "user counts inaccessible" do + it "denies access with a 404 response" do + category_id = Fabricate(:category).id + + put "/admin/site_settings/default_categories_watching/user_count.json", params: { + default_categories_watching: category_id + } + + 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 counts inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "user counts inaccessible" + end + end + + describe '#update' do + before do + SiteSetting.setting(:test_setting, "default") + SiteSetting.setting(:test_upload, "", type: :upload) + SiteSetting.refresh! + end + + context "when logged in as an admin" do + before { sign_in(admin) } it 'sets the value when the param is present' do put "/admin/site_settings/test_setting.json", params: { @@ -70,7 +259,7 @@ RSpec.describe Admin::SiteSettingsController do expect(SiteSetting.test_setting).to eq('') end - describe 'default user options' do + context "with default user options" do let!(:user1) { Fabricate(:user) } let!(:user2) { Fabricate(:user) } @@ -154,7 +343,7 @@ RSpec.describe Admin::SiteSettingsController do end end - describe 'default categories' do + context "with default categories" do fab!(:user1) { Fabricate(:user) } fab!(:user2) { Fabricate(:user) } fab!(:staged_user) { Fabricate(:staged) } @@ -219,7 +408,7 @@ RSpec.describe Admin::SiteSettingsController do end end - describe 'default tags' do + context "with default tags" do fab!(:user1) { Fabricate(:user) } fab!(:user2) { Fabricate(:user) } fab!(:staged_user) { Fabricate(:staged) } @@ -258,141 +447,7 @@ RSpec.describe Admin::SiteSettingsController do end end - describe '#user_count' do - fab!(:user) { Fabricate(:user) } - fab!(:staged_user) { Fabricate(:staged) } - let(:tracking) { NotificationLevels.all[:tracking] } - - it 'should return correct user count for default categories change' do - category_id = Fabricate(:category).id - - put "/admin/site_settings/default_categories_watching/user_count.json", params: { - default_categories_watching: category_id - } - - expect(response.parsed_body["user_count"]).to eq(User.real.where(staged: false).count) - - CategoryUser.create!(category_id: category_id, notification_level: tracking, user: user) - - put "/admin/site_settings/default_categories_watching/user_count.json", params: { - default_categories_watching: category_id - } - - expect(response.parsed_body["user_count"]).to eq(User.real.where(staged: false).count - 1) - - SiteSetting.setting(:default_categories_watching, "") - end - - it 'should return correct user count for default tags change' do - tag = Fabricate(:tag) - - put "/admin/site_settings/default_tags_watching/user_count.json", params: { - default_tags_watching: tag.name - } - - expect(response.parsed_body["user_count"]).to eq(User.real.where(staged: false).count) - - TagUser.create!(tag_id: tag.id, notification_level: tracking, user: user) - - put "/admin/site_settings/default_tags_watching/user_count.json", params: { - default_tags_watching: tag.name - } - - expect(response.parsed_body["user_count"]).to eq(User.real.where(staged: false).count - 1) - - SiteSetting.setting(:default_tags_watching, "") - end - - context "for sidebar defaults" do - it 'returns the right count for the default_sidebar_categories site setting' do - category = Fabricate(:category) - - put "/admin/site_settings/default_sidebar_categories/user_count.json", params: { - default_sidebar_categories: "#{category.id}" - } - - expect(response.status).to eq(200) - expect(response.parsed_body["user_count"]).to eq(User.real.not_staged.count) - end - - it 'returns the right count for the default_sidebar_tags site setting' do - tag = Fabricate(:tag) - - put "/admin/site_settings/default_sidebar_tags/user_count.json", params: { - default_sidebar_tags: "#{tag.name}" - } - - expect(response.status).to eq(200) - expect(response.parsed_body["user_count"]).to eq(User.real.not_staged.count) - end - end - - context "with user options" do - def expect_user_count(site_setting_name:, user_setting_name:, current_site_setting_value:, new_site_setting_value:, - current_user_setting_value: nil, new_user_setting_value: nil) - - current_user_setting_value ||= current_site_setting_value - new_user_setting_value ||= new_site_setting_value - - SiteSetting.public_send("#{site_setting_name}=", current_site_setting_value) - UserOption.human_users.update_all(user_setting_name => current_user_setting_value) - user_count = User.human_users.count - - # Correctly counts users when all of them have default value - put "/admin/site_settings/#{site_setting_name}/user_count.json", params: { - site_setting_name => new_site_setting_value - } - expect(response.parsed_body["user_count"]).to eq(user_count) - - # Correctly counts users when one of them already has new value - user.user_option.update!(user_setting_name => new_user_setting_value) - put "/admin/site_settings/#{site_setting_name}/user_count.json", params: { - site_setting_name => new_site_setting_value - } - expect(response.parsed_body["user_count"]).to eq(user_count - 1) - - # Correctly counts users when site setting value has been changed - SiteSetting.public_send("#{site_setting_name}=", new_site_setting_value) - put "/admin/site_settings/#{site_setting_name}/user_count.json", params: { - site_setting_name => current_site_setting_value - } - expect(response.parsed_body["user_count"]).to eq(1) - end - - it "should return correct user count for boolean setting" do - expect_user_count( - site_setting_name: "default_other_external_links_in_new_tab", - user_setting_name: "external_links_in_new_tab", - current_site_setting_value: false, - new_site_setting_value: true - ) - end - - it "should return correct user count for 'text_size_key'" do - expect_user_count( - site_setting_name: "default_text_size", - user_setting_name: "text_size_key", - current_site_setting_value: "normal", - new_site_setting_value: "larger", - current_user_setting_value: UserOption.text_sizes[:normal], - new_user_setting_value: UserOption.text_sizes[:larger] - ) - end - - it "should return correct user count for 'title_count_mode_key'" do - expect_user_count( - site_setting_name: "default_title_count_mode", - user_setting_name: "title_count_mode_key", - current_site_setting_value: "notifications", - new_site_setting_value: "contextual", - current_user_setting_value: UserOption.title_count_modes[:notifications], - new_user_setting_value: UserOption.title_count_modes[:contextual] - ) - end - end - end - - describe 'upload site settings' do + context "with upload site settings" do it 'can remove the site setting' do SiteSetting.test_upload = Fabricate(:upload) @@ -467,5 +522,28 @@ RSpec.describe Admin::SiteSettingsController do expect(response.status).to eq(422) end end + + shared_examples "site setting update not allowed" do + it "prevents updates with a 404 response" do + put "/admin/site_settings/test_setting.json", params: { + test_setting: 'hello' + } + + 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 "site setting update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "site setting update not allowed" + end end end diff --git a/spec/requests/admin/site_texts_controller_spec.rb b/spec/requests/admin/site_texts_controller_spec.rb index 579e1c56752..de85e4826ce 100644 --- a/spec/requests/admin/site_texts_controller_spec.rb +++ b/spec/requests/admin/site_texts_controller_spec.rb @@ -2,6 +2,7 @@ RSpec.describe Admin::SiteTextsController do fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } fab!(:user) { Fabricate(:user) } let(:default_locale) { I18n.locale } @@ -10,40 +11,10 @@ RSpec.describe Admin::SiteTextsController do I18n.reload! end - it "is a subclass of AdminController" do - expect(Admin::SiteTextsController < Admin::AdminController).to eq(true) - end + describe '#index' do + context "when logged in as an admin" do + before { sign_in(admin) } - context "when not logged in as an admin" do - it "raises an error if you aren't logged in" do - put '/admin/customize/site_texts/some_key.json', params: { - site_text: { value: 'foo' }, locale: default_locale - } - - expect(response.status).to eq(404) - end - - it "raises an error if you aren't an admin" do - sign_in(user) - - put "/admin/customize/site_texts/some_key.json", params: { - site_text: { value: 'foo' }, locale: default_locale - } - expect(response.status).to eq(404) - - put "/admin/customize/reseed.json", params: { - category_ids: [], topic_ids: [] - } - expect(response.status).to eq(404) - end - end - - context "when logged in as admin" do - before do - sign_in(admin) - end - - describe '#index' do it 'returns json' do get "/admin/customize/site_texts.json", params: { q: 'title', locale: default_locale } expect(response.status).to eq(200) @@ -232,7 +203,32 @@ RSpec.describe Admin::SiteTextsController do end end - describe '#show' do + shared_examples "site texts inaccessible" do + it "denies access with a 404 response" do + get "/admin/customize/site_texts.json", params: { q: 'title', locale: default_locale } + + 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 "site texts inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "site texts inaccessible" + end + end + + describe '#show' do + context "when logged in as an admin" do + before { sign_in(admin) } + it 'returns a site text for a key that exists' do get "/admin/customize/site_texts/js.topic.list.json", params: { locale: default_locale } expect(response.status).to eq(200) @@ -373,7 +369,32 @@ RSpec.describe Admin::SiteTextsController do end end - describe '#update & #revert' do + shared_examples "site text inaccessible" do + it "denies access with a 404 response" do + get "/admin/customize/site_texts/js.topic.list.json", params: { locale: default_locale } + + 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 "site text inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "site text inaccessible" + end + end + + describe '#update & #revert' do + context "when logged in as an admin" do + before { sign_in(admin) } + it "returns 'not found' when an unknown key is used" do put '/admin/customize/site_texts/some_key.json', params: { site_text: { value: 'foo', locale: default_locale } @@ -551,26 +572,53 @@ RSpec.describe Admin::SiteTextsController do end end - context "when reseeding" do - before do - staff_category = Fabricate( - :category, - name: "Staff EN", - user: Discourse.system_user - ) - SiteSetting.staff_category_id = staff_category.id + shared_examples "site text update not allowed" do + it "prevents updates with a 404 response" do + put "/admin/customize/site_texts/js.emoji_picker.animals_%26_nature.json", params: { + site_text: { value: 'foo', locale: default_locale } + } - guidelines_topic = Fabricate( - :topic, - title: "The English Guidelines", - category: @staff_category, - user: Discourse.system_user - ) - Fabricate(:post, topic: guidelines_topic, user: Discourse.system_user) - SiteSetting.guidelines_topic_id = guidelines_topic.id + 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 "site text update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "site text update not allowed" + end + end + + context "when reseeding" do + before do + staff_category = Fabricate( + :category, + name: "Staff EN", + user: Discourse.system_user + ) + SiteSetting.staff_category_id = staff_category.id + + guidelines_topic = Fabricate( + :topic, + title: "The English Guidelines", + category: @staff_category, + user: Discourse.system_user + ) + Fabricate(:post, topic: guidelines_topic, user: Discourse.system_user) + SiteSetting.guidelines_topic_id = guidelines_topic.id + end + + describe '#get_reseed_options' do + context "when logged in as an admin" do + before { sign_in(admin) } - describe '#get_reseed_options' do it 'returns correct json' do get "/admin/customize/reseed.json" expect(response.status).to eq(200) @@ -587,7 +635,32 @@ RSpec.describe Admin::SiteTextsController do end end - describe '#reseed' do + shared_examples "reseed options inaccessible" do + it "denies access with a 404 response" do + get "/admin/customize/reseed.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 "reseed options inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "reseed options inaccessible" + end + end + + describe '#reseed' do + context "when logged in as an admin" do + before { sign_in(admin) } + it 'reseeds categories and topics' do SiteSetting.default_locale = :de @@ -601,6 +674,30 @@ RSpec.describe Admin::SiteTextsController do expect(Topic.find(SiteSetting.guidelines_topic_id).title).to eq(I18n.t("guidelines_topic.title", locale: :de)) end end + + shared_examples "reseed not allowed" do + it "prevents reseeds with a 404 response" do + post "/admin/customize/reseed.json", params: { + category_ids: ["staff_category_id"], + topic_ids: ["guidelines_topic_id"] + } + + 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 "reseed not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "reseed not allowed" + end end end end diff --git a/spec/requests/admin/staff_action_logs_controller_spec.rb b/spec/requests/admin/staff_action_logs_controller_spec.rb index e59ae0fca6f..873fa8218d3 100644 --- a/spec/requests/admin/staff_action_logs_controller_spec.rb +++ b/spec/requests/admin/staff_action_logs_controller_spec.rb @@ -1,99 +1,146 @@ # frozen_string_literal: true RSpec.describe Admin::StaffActionLogsController do - it "is a subclass of StaffController" do - expect(Admin::StaffActionLogsController < Admin::StaffController).to eq(true) - end - fab!(:admin) { Fabricate(:admin) } - - before do - sign_in(admin) - end + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } describe '#index' do - it 'generates logs' do - topic = Fabricate(:topic) - StaffActionLogger.new(Discourse.system_user).log_topic_delete_recover(topic, "delete_topic") + shared_examples "staff action logs accessible" do + it 'returns logs' do + topic = Fabricate(:topic) + StaffActionLogger.new(Discourse.system_user).log_topic_delete_recover(topic, "delete_topic") - get "/admin/logs/staff_action_logs.json", params: { action_id: UserHistory.actions[:delete_topic] } - - json = response.parsed_body - expect(response.status).to eq(200) - - expect(json["staff_action_logs"].length).to eq(1) - expect(json["staff_action_logs"][0]["action_name"]).to eq("delete_topic") - - expect(json["extras"]["user_history_actions"]).to include( - "id" => 'delete_topic', "action_id" => UserHistory.actions[:delete_topic] - ) - end - - it 'generates logs with pages' do - 1.upto(4).each do |idx| - StaffActionLogger.new(Discourse.system_user).log_site_setting_change("title", "value #{idx - 1}", "value #{idx}") - end - - get "/admin/logs/staff_action_logs.json", params: { limit: 3 } - - json = response.parsed_body - expect(response.status).to eq(200) - expect(json["staff_action_logs"].length).to eq(3) - expect(json["staff_action_logs"][0]["new_value"]).to eq("value 4") - - get "/admin/logs/staff_action_logs.json", params: { limit: 3, page: 1 } - - json = response.parsed_body - expect(response.status).to eq(200) - expect(json["staff_action_logs"].length).to eq(1) - expect(json["staff_action_logs"][0]["new_value"]).to eq("value 1") - end - - context 'when staff actions are extended' do - let(:plugin_extended_action) { :confirmed_ham } - before { UserHistory.stubs(:staff_actions).returns([plugin_extended_action]) } - after { UserHistory.unstub(:staff_actions) } - - it 'Uses the custom_staff id' do - get "/admin/logs/staff_action_logs.json", params: {} + get "/admin/logs/staff_action_logs.json", params: { action_id: UserHistory.actions[:delete_topic] } json = response.parsed_body - action = json['extras']['user_history_actions'].first + expect(response.status).to eq(200) - expect(action['id']).to eq plugin_extended_action.to_s - expect(action['action_id']).to eq UserHistory.actions[:custom_staff] + expect(json["staff_action_logs"].length).to eq(1) + expect(json["staff_action_logs"][0]["action_name"]).to eq("delete_topic") + + expect(json["extras"]["user_history_actions"]).to include( + "id" => 'delete_topic', "action_id" => UserHistory.actions[:delete_topic] + ) + end + end + + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "staff action logs accessible" + + it 'generates logs with pages' do + 1.upto(4).each do |idx| + StaffActionLogger.new(Discourse.system_user).log_site_setting_change("title", "value #{idx - 1}", "value #{idx}") + end + + get "/admin/logs/staff_action_logs.json", params: { limit: 3 } + + json = response.parsed_body + expect(response.status).to eq(200) + expect(json["staff_action_logs"].length).to eq(3) + expect(json["staff_action_logs"][0]["new_value"]).to eq("value 4") + + get "/admin/logs/staff_action_logs.json", params: { limit: 3, page: 1 } + + json = response.parsed_body + expect(response.status).to eq(200) + expect(json["staff_action_logs"].length).to eq(1) + expect(json["staff_action_logs"][0]["new_value"]).to eq("value 1") + end + + context 'when staff actions are extended' do + let(:plugin_extended_action) { :confirmed_ham } + before { UserHistory.stubs(:staff_actions).returns([plugin_extended_action]) } + after { UserHistory.unstub(:staff_actions) } + + it 'Uses the custom_staff id' do + get "/admin/logs/staff_action_logs.json", params: {} + + json = response.parsed_body + action = json['extras']['user_history_actions'].first + + expect(action['id']).to eq plugin_extended_action.to_s + expect(action['action_id']).to eq UserHistory.actions[:custom_staff] + end + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "staff action logs accessible" + 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/logs/staff_action_logs.json", params: { action_id: UserHistory.actions[:delete_topic] } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) end end end describe '#diff' do - it 'can generate diffs for theme changes' do - theme = Fabricate(:theme) - theme.set_field(target: :mobile, name: :scss, value: 'body {.up}') - theme.set_field(target: :common, name: :scss, value: 'omit-dupe') + shared_examples "theme diffs accessible" do + it 'generates diffs for theme changes' do + theme = Fabricate(:theme) + theme.set_field(target: :mobile, name: :scss, value: 'body {.up}') + theme.set_field(target: :common, name: :scss, value: 'omit-dupe') - original_json = ThemeSerializer.new(theme, root: false).to_json + original_json = ThemeSerializer.new(theme, root: false).to_json - theme.set_field(target: :mobile, name: :scss, value: 'body {.down}') + theme.set_field(target: :mobile, name: :scss, value: 'body {.down}') - record = StaffActionLogger.new(Discourse.system_user) - .log_theme_change(original_json, theme) + record = StaffActionLogger.new(Discourse.system_user) + .log_theme_change(original_json, theme) - get "/admin/logs/staff_action_logs/#{record.id}/diff.json" - expect(response.status).to eq(200) + get "/admin/logs/staff_action_logs/#{record.id}/diff.json" + expect(response.status).to eq(200) - parsed = response.parsed_body - expect(parsed["side_by_side"]).to include("up") - expect(parsed["side_by_side"]).to include("down") + parsed = response.parsed_body + expect(parsed["side_by_side"]).to include("up") + expect(parsed["side_by_side"]).to include("down") - expect(parsed["side_by_side"]).not_to include("omit-dupe") + expect(parsed["side_by_side"]).not_to include("omit-dupe") + end end - it 'is not erroring when current value is empty' do - theme = Fabricate(:theme) - StaffActionLogger.new(admin).log_theme_destroy(theme) - get "/admin/logs/staff_action_logs/#{UserHistory.last.id}/diff.json" - expect(response.status).to eq(200) + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "theme diffs accessible" + + it 'is not erroring when current value is empty' do + theme = Fabricate(:theme) + StaffActionLogger.new(admin).log_theme_destroy(theme) + get "/admin/logs/staff_action_logs/#{UserHistory.last.id}/diff.json" + expect(response.status).to eq(200) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "theme diffs accessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + it "denies access with a 404 response" do + theme = Fabricate(:theme) + StaffActionLogger.new(admin).log_theme_destroy(theme) + + get "/admin/logs/staff_action_logs/#{UserHistory.last.id}/diff.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end end end end diff --git a/spec/requests/admin/themes_controller_spec.rb b/spec/requests/admin/themes_controller_spec.rb index f61a50ecd93..ab48564bac0 100644 --- a/spec/requests/admin/themes_controller_spec.rb +++ b/spec/requests/admin/themes_controller_spec.rb @@ -2,14 +2,8 @@ RSpec.describe Admin::ThemesController do fab!(:admin) { Fabricate(:admin) } - - it "is a subclass of AdminController" do - expect(Admin::ThemesController < Admin::AdminController).to eq(true) - end - - before do - sign_in(admin) - end + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } let! :repo do setup_git_repo( @@ -28,13 +22,38 @@ RSpec.describe Admin::ThemesController do end describe '#generate_key_pair' do - it 'can generate key pairs' do - post "/admin/themes/generate_key_pair.json" - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["private_key"]).to eq(nil) - expect(json["public_key"]).to include("ssh-rsa ") - expect(Discourse.redis.get("ssh_key_#{json["public_key"]}")).not_to eq(nil) + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'can generate key pairs' do + post "/admin/themes/generate_key_pair.json" + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["private_key"]).to eq(nil) + expect(json["public_key"]).to include("ssh-rsa ") + expect(Discourse.redis.get("ssh_key_#{json["public_key"]}")).not_to eq(nil) + end + end + + shared_examples "key pair generation not allowed" do + it "prevents key pair generation with a 404 response" do + post "/admin/themes/generate_key_pair.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 "key pair generation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "key pair generation not allowed" end end @@ -45,64 +64,116 @@ RSpec.describe Admin::ThemesController do Rack::Test::UploadedFile.new(file) end - it 'can create a theme upload' do - post "/admin/themes/upload_asset.json", params: { file: upload } - expect(response.status).to eq(201) + context "when logged in as an admin" do + before { sign_in(admin) } - upload = Upload.find_by(original_filename: filename) - - expect(upload.id).not_to be_nil - expect(response.parsed_body["upload_id"]).to eq(upload.id) - end - - context "when trying to upload an existing file" do - let(:uploaded_file) { Upload.find_by(original_filename: filename) } - let(:response_json) { response.parsed_body } - - before do + it 'can create a theme upload' do post "/admin/themes/upload_asset.json", params: { file: upload } expect(response.status).to eq(201) + + upload = Upload.find_by(original_filename: filename) + + expect(upload.id).not_to be_nil + expect(response.parsed_body["upload_id"]).to eq(upload.id) end - it "reuses the original upload" do - expect(response.status).to eq(201) - expect(response_json["upload_id"]).to eq(uploaded_file.id) + context "when trying to upload an existing file" do + let(:uploaded_file) { Upload.find_by(original_filename: filename) } + let(:response_json) { response.parsed_body } + + before do + post "/admin/themes/upload_asset.json", params: { file: upload } + expect(response.status).to eq(201) + end + + it "reuses the original upload" do + expect(response.status).to eq(201) + expect(response_json["upload_id"]).to eq(uploaded_file.id) + end end end + + shared_examples "theme asset upload not allowed" do + it "prevents theme asset upload with a 404 response" do + expect do + post "/admin/themes/upload_asset.json", params: { file: upload } + end.not_to change { Upload.count } + + 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 "theme asset upload not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "theme asset upload not allowed" + end end describe '#export' do - it "exports correctly" do - theme = Fabricate(:theme, name: "Awesome Theme") - theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') - theme.set_field(target: :desktop, name: :after_header, value: 'test') - theme.set_field(target: :extra_js, name: "discourse/controller/blah", value: 'console.log("test");') - theme.save! + context "when logged in as an admin" do + before { sign_in(admin) } - get "/admin/customize/themes/#{theme.id}/export" - expect(response.status).to eq(200) + it "exports correctly" do + theme = Fabricate(:theme, name: "Awesome Theme") + theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') + theme.set_field(target: :desktop, name: :after_header, value: 'test') + theme.set_field(target: :extra_js, name: "discourse/controller/blah", value: 'console.log("test");') + theme.save! - # Save the output in a temp file (automatically cleaned up) - file = Tempfile.new('archive.zip') - file.write(response.body) - file.rewind - uploaded_file = Rack::Test::UploadedFile.new(file.path, "application/zip") + get "/admin/customize/themes/#{theme.id}/export" + expect(response.status).to eq(200) - # Now import it again - expect do - post "/admin/themes/import.json", params: { theme: uploaded_file } - expect(response.status).to eq(201) - end.to change { Theme.count }.by (1) + # Save the output in a temp file (automatically cleaned up) + file = Tempfile.new('archive.zip') + file.write(response.body) + file.rewind + uploaded_file = Rack::Test::UploadedFile.new(file.path, "application/zip") - json = response.parsed_body + # Now import it again + expect do + post "/admin/themes/import.json", params: { theme: uploaded_file } + expect(response.status).to eq(201) + end.to change { Theme.count }.by (1) - expect(json["theme"]["name"]).to eq("Awesome Theme") - expect(json["theme"]["theme_fields"].length).to eq(3) + json = response.parsed_body + + expect(json["theme"]["name"]).to eq("Awesome Theme") + expect(json["theme"]["theme_fields"].length).to eq(3) + end + end + + shared_examples "theme export not allowed" do + it "prevents theme export with a 404 response" do + theme = Fabricate(:theme, name: "Awesome Theme") + + get "/admin/customize/themes/#{theme.id}/export" + + expect(response.status).to eq(404) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "theme export not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "theme export not allowed" end end describe '#import' do - let(:theme_json_file) do Rack::Test::UploadedFile.new(file_from_fixtures("sam-s-simple-theme.dcstyle.json", "json"), "application/json") end @@ -115,591 +186,749 @@ RSpec.describe Admin::ThemesController do file_from_fixtures("logo.png") end - context 'when theme allowlist mode is enabled' do - before do - global_setting :allowed_theme_repos, "https://github.com/discourse/discourse-brand-header.git" + context "when logged in as an admin" do + before { sign_in(admin) } + + context 'when theme allowlist mode is enabled' do + before do + global_setting :allowed_theme_repos, "https://github.com/discourse/discourse-brand-header.git" + end + + it "allows allowlisted imports" do + expect(Theme.allowed_remote_theme_ids.length).to eq(0) + + post "/admin/themes/import.json", params: { + remote: ' https://github.com/discourse/discourse-brand-header.git ' + } + + expect(Theme.allowed_remote_theme_ids.length).to eq(1) + expect(response.status).to eq(201) + end + + it "prevents adding disallowed themes" do + RemoteTheme.stubs(:import_theme) + remote = ' https://bad.com/discourse/discourse-brand-header.git ' + + post "/admin/themes/import.json", params: { remote: remote } + + expect(response.status).to eq(403) + expect(response.parsed_body['errors']).to include(I18n.t("themes.import_error.not_allowed_theme", { repo: remote.strip })) + end + + it "bans json file import" do + post "/admin/themes/import.json", params: { theme: theme_json_file } + expect(response.status).to eq(403) + end end - it "allows allowlisted imports" do - expect(Theme.allowed_remote_theme_ids.length).to eq(0) - + it 'can import a theme from Git' do + RemoteTheme.stubs(:import_theme) post "/admin/themes/import.json", params: { remote: ' https://github.com/discourse/discourse-brand-header.git ' } expect(response.status).to eq(201) - expect(Theme.allowed_remote_theme_ids.length).to eq(1) end - it "prevents adding disallowed themes" do - RemoteTheme.stubs(:import_theme) - remote = ' https://bad.com/discourse/discourse-brand-header.git ' + it 'can lookup a private key by public key' do + Discourse.redis.setex('ssh_key_abcdef', 1.hour, 'rsa private key') - post "/admin/themes/import.json", params: { remote: remote } + ThemeStore::GitImporter.any_instance.stubs(:import!) + RemoteTheme.stubs(:extract_theme_info).returns( + 'name' => 'discourse-brand-header', + 'component' => true + ) + RemoteTheme.any_instance.stubs(:update_from_remote) - expect(response.status).to eq(403) - expect(response.parsed_body['errors']).to include(I18n.t("themes.import_error.not_allowed_theme", { repo: remote.strip })) + post '/admin/themes/import.json', params: { + remote: ' https://github.com/discourse/discourse-brand-header.git ', + public_key: 'abcdef', + } + + expect(RemoteTheme.last.private_key).to eq('rsa private key') + + expect(response.status).to eq(201) end - it "bans json file import" do + it 'imports a theme' do post "/admin/themes/import.json", params: { theme: theme_json_file } - expect(response.status).to eq(403) + expect(response.status).to eq(201) + + json = response.parsed_body + + expect(json["theme"]["name"]).to eq("Sam's Simple Theme") + expect(json["theme"]["theme_fields"].length).to eq(2) + expect(json["theme"]["auto_update"]).to eq(false) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) end - end - it 'can import a theme from Git' do - RemoteTheme.stubs(:import_theme) - post "/admin/themes/import.json", params: { - remote: ' https://github.com/discourse/discourse-brand-header.git ' - } + it 'can fail if theme is not accessible' do + post "/admin/themes/import.json", params: { + remote: 'git@github.com:discourse/discourse-inexistent-theme.git' + } - expect(response.status).to eq(201) - end + expect(response.status).to eq(422) + expect(response.parsed_body["errors"]).to contain_exactly(I18n.t("themes.import_error.git")) + end - it 'can lookup a private key by public key' do - Discourse.redis.setex('ssh_key_abcdef', 1.hour, 'rsa private key') + it 'can force install theme' do + post "/admin/themes/import.json", params: { + remote: 'git@github.com:discourse/discourse-inexistent-theme.git', + force: true + } - ThemeStore::GitImporter.any_instance.stubs(:import!) - RemoteTheme.stubs(:extract_theme_info).returns( - 'name' => 'discourse-brand-header', - 'component' => true - ) - RemoteTheme.any_instance.stubs(:update_from_remote) + expect(response.status).to eq(201) + expect(response.parsed_body["theme"]["name"]).to eq("discourse-inexistent-theme") + end - post '/admin/themes/import.json', params: { - remote: ' https://github.com/discourse/discourse-brand-header.git ', - public_key: 'abcdef', - } + it 'fails to import with an error if uploads are not allowed' do + SiteSetting.theme_authorized_extensions = "nothing" - expect(RemoteTheme.last.private_key).to eq('rsa private key') - - expect(response.status).to eq(201) - end - - it 'should not be able to import a theme by moderator' do - sign_in(Fabricate(:moderator)) - - post "/admin/themes/import.json", params: { theme: theme_json_file } - expect(response.status).to eq(404) - end - - it 'imports a theme' do - post "/admin/themes/import.json", params: { theme: theme_json_file } - expect(response.status).to eq(201) - - json = response.parsed_body - - expect(json["theme"]["name"]).to eq("Sam's Simple Theme") - expect(json["theme"]["theme_fields"].length).to eq(2) - expect(json["theme"]["auto_update"]).to eq(false) - expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) - end - - it 'can fail if theme is not accessible' do - post "/admin/themes/import.json", params: { - remote: 'git@github.com:discourse/discourse-inexistent-theme.git' - } - - expect(response.status).to eq(422) - expect(response.parsed_body["errors"]).to contain_exactly(I18n.t("themes.import_error.git")) - end - - it 'can force install theme' do - post "/admin/themes/import.json", params: { - remote: 'git@github.com:discourse/discourse-inexistent-theme.git', - force: true - } - - expect(response.status).to eq(201) - expect(response.parsed_body["theme"]["name"]).to eq("discourse-inexistent-theme") - end - - it 'fails to import with an error if uploads are not allowed' do - SiteSetting.theme_authorized_extensions = "nothing" - - expect do - post "/admin/themes/import.json", params: { theme: theme_archive } - end.to change { Theme.count }.by (0) - - expect(response.status).to eq(422) - end - - it 'imports a theme from an archive' do - _existing_theme = Fabricate(:theme, name: "Header Icons") - - expect do - post "/admin/themes/import.json", params: { theme: theme_archive } - end.to change { Theme.count }.by (1) - expect(response.status).to eq(201) - json = response.parsed_body - - expect(json["theme"]["name"]).to eq("Header Icons") - expect(json["theme"]["theme_fields"].length).to eq(5) - expect(json["theme"]["auto_update"]).to eq(false) - expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) - end - - it 'updates an existing theme from an archive by name' do - # Old theme CLI method, remove Jan 2020 - _existing_theme = Fabricate(:theme, name: "Header Icons") - - expect do - post "/admin/themes/import.json", params: { bundle: theme_archive } - end.to change { Theme.count }.by (0) - expect(response.status).to eq(201) - json = response.parsed_body - - expect(json["theme"]["name"]).to eq("Header Icons") - expect(json["theme"]["theme_fields"].length).to eq(5) - expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) - end - - it 'updates an existing theme from an archive by id' do - # Used by theme CLI - _existing_theme = Fabricate(:theme, name: "Header Icons") - other_existing_theme = Fabricate(:theme, name: "Some other name") - - messages = MessageBus.track_publish do expect do - post "/admin/themes/import.json", params: { bundle: theme_archive, theme_id: other_existing_theme.id } + post "/admin/themes/import.json", params: { theme: theme_archive } end.to change { Theme.count }.by (0) + + expect(response.status).to eq(422) end - expect(response.status).to eq(201) - json = response.parsed_body - # Ensure only one refresh message is sent. - # More than 1 is wasteful, and can trigger unusual race conditions in the client - # If this test fails, it probably means `theme.save` is being called twice - check any 'autosave' relations - file_change_messages = messages.filter { |m| m[:channel] == "/file-change" } - expect(file_change_messages.count).to eq(1) + it 'imports a theme from an archive' do + _existing_theme = Fabricate(:theme, name: "Header Icons") - expect(json["theme"]["name"]).to eq("Some other name") - expect(json["theme"]["id"]).to eq(other_existing_theme.id) - expect(json["theme"]["theme_fields"].length).to eq(5) - expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + expect do + post "/admin/themes/import.json", params: { theme: theme_archive } + end.to change { Theme.count }.by (1) + expect(response.status).to eq(201) + json = response.parsed_body + + expect(json["theme"]["name"]).to eq("Header Icons") + expect(json["theme"]["theme_fields"].length).to eq(5) + expect(json["theme"]["auto_update"]).to eq(false) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end + + it 'updates an existing theme from an archive by name' do + # Old theme CLI method, remove Jan 2020 + _existing_theme = Fabricate(:theme, name: "Header Icons") + + expect do + post "/admin/themes/import.json", params: { bundle: theme_archive } + end.to change { Theme.count }.by (0) + expect(response.status).to eq(201) + json = response.parsed_body + + expect(json["theme"]["name"]).to eq("Header Icons") + expect(json["theme"]["theme_fields"].length).to eq(5) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end + + it 'updates an existing theme from an archive by id' do + # Used by theme CLI + _existing_theme = Fabricate(:theme, name: "Header Icons") + other_existing_theme = Fabricate(:theme, name: "Some other name") + + messages = MessageBus.track_publish do + expect do + post "/admin/themes/import.json", params: { bundle: theme_archive, theme_id: other_existing_theme.id } + end.to change { Theme.count }.by (0) + end + expect(response.status).to eq(201) + json = response.parsed_body + + # Ensure only one refresh message is sent. + # More than 1 is wasteful, and can trigger unusual race conditions in the client + # If this test fails, it probably means `theme.save` is being called twice - check any 'autosave' relations + file_change_messages = messages.filter { |m| m[:channel] == "/file-change" } + expect(file_change_messages.count).to eq(1) + + expect(json["theme"]["name"]).to eq("Some other name") + expect(json["theme"]["id"]).to eq(other_existing_theme.id) + expect(json["theme"]["theme_fields"].length).to eq(5) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end + + it 'creates a new theme when id specified as nil' do + # Used by theme CLI + existing_theme = Fabricate(:theme, name: "Header Icons") + + expect do + post "/admin/themes/import.json", params: { bundle: theme_archive, theme_id: nil } + end.to change { Theme.count }.by (1) + expect(response.status).to eq(201) + json = response.parsed_body + + expect(json["theme"]["name"]).to eq("Header Icons") + expect(json["theme"]["id"]).not_to eq(existing_theme.id) + expect(json["theme"]["theme_fields"].length).to eq(5) + expect(json["theme"]["auto_update"]).to eq(false) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end end - it 'creates a new theme when id specified as nil' do - # Used by theme CLI - existing_theme = Fabricate(:theme, name: "Header Icons") + shared_examples "theme import not allowed" do + it "prevents theme import with a 404 response" do + post "/admin/themes/import.json", params: { theme: theme_json_file } - expect do - post "/admin/themes/import.json", params: { bundle: theme_archive, theme_id: nil } - end.to change { Theme.count }.by (1) - expect(response.status).to eq(201) - json = response.parsed_body + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + end + end - expect(json["theme"]["name"]).to eq("Header Icons") - expect(json["theme"]["id"]).not_to eq(existing_theme.id) - expect(json["theme"]["theme_fields"].length).to eq(5) - expect(json["theme"]["auto_update"]).to eq(false) - expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "theme import not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "theme import not allowed" end end describe '#index' do - it 'correctly returns themes' do - ColorScheme.destroy_all - Theme.destroy_all + context "when logged in as an admin" do + before { sign_in(admin) } - theme = Fabricate(:theme) - theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') - theme.set_field(target: :desktop, name: :after_header, value: 'test') + it 'correctly returns themes' do + ColorScheme.destroy_all + Theme.destroy_all - theme.remote_theme = RemoteTheme.new( - remote_url: 'awesome.git', - remote_version: '7', - local_version: '8', - remote_updated_at: Time.zone.now - ) + theme = Fabricate(:theme) + theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') + theme.set_field(target: :desktop, name: :after_header, value: 'test') - theme.save! + theme.remote_theme = RemoteTheme.new( + remote_url: 'awesome.git', + remote_version: '7', + local_version: '8', + remote_updated_at: Time.zone.now + ) - # this will get serialized as well - ColorScheme.create_from_base(name: "test", colors: []) + theme.save! - get "/admin/themes.json" + # this will get serialized as well + ColorScheme.create_from_base(name: "test", colors: []) - expect(response.status).to eq(200) + get "/admin/themes.json" - json = response.parsed_body + expect(response.status).to eq(200) - expect(json["extras"]["color_schemes"].length).to eq(1) - theme_json = json["themes"].find { |t| t["id"] == theme.id } - expect(theme_json["theme_fields"].length).to eq(2) - expect(theme_json["remote_theme"]["remote_version"]).to eq("7") + json = response.parsed_body + + expect(json["extras"]["color_schemes"].length).to eq(1) + theme_json = json["themes"].find { |t| t["id"] == theme.id } + expect(theme_json["theme_fields"].length).to eq(2) + expect(theme_json["remote_theme"]["remote_version"]).to eq("7") + end + end + + shared_examples "themes inaccessible" do + it "denies access with a 404 response" do + get "/admin/themes.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 "themes inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "themes inaccessible" end end describe '#create' do - it 'creates a theme' do - post "/admin/themes.json", params: { - theme: { - name: 'my test name', - theme_fields: [name: 'scss', target: 'common', value: 'body{color: red;}'] + context "when logged in as an admin" do + before { sign_in(admin) } + + it 'creates a theme' do + post "/admin/themes.json", params: { + theme: { + name: 'my test name', + theme_fields: [name: 'scss', target: 'common', value: 'body{color: red;}'] + } } - } - expect(response.status).to eq(201) + expect(response.status).to eq(201) - json = response.parsed_body + json = response.parsed_body - expect(json["theme"]["theme_fields"].length).to eq(1) - expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + expect(json["theme"]["theme_fields"].length).to eq(1) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end + end + + shared_examples "theme creation not allowed" do + it "prevents creation with a 404 response" do + expect do + post "/admin/themes.json", params: { + theme: { + name: 'my test name', + theme_fields: [name: 'scss', target: 'common', value: 'body{color: red;}'] + } + } + end.not_to change { Theme.count } + + 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 "theme creation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "theme creation not allowed" end end describe '#update' do let!(:theme) { Fabricate(:theme) } - it 'returns the right response when an invalid id is given' do - put "/admin/themes/99999.json" + context "when logged in as an admin" do + before { sign_in(admin) } - expect(response.status).to eq(400) - end + it 'returns the right response when an invalid id is given' do + put "/admin/themes/99999.json" - it 'can change default theme' do - SiteSetting.default_theme_id = -1 - - put "/admin/themes/#{theme.id}.json", params: { - id: theme.id, theme: { default: true } - } - - expect(response.status).to eq(200) - expect(SiteSetting.default_theme_id).to eq(theme.id) - end - - it 'can unset default theme' do - SiteSetting.default_theme_id = theme.id - - put "/admin/themes/#{theme.id}.json", params: { - theme: { default: false } - } - - expect(response.status).to eq(200) - expect(SiteSetting.default_theme_id).to eq(-1) - end - - context 'when theme allowlist mode is enabled' do - before do - global_setting :allowed_theme_repos, " https://magic.com/repo.git, https://x.com/git" + expect(response.status).to eq(400) end - it 'unconditionally bans theme_fields from updating' do + it 'can change default theme' do + SiteSetting.default_theme_id = -1 + + put "/admin/themes/#{theme.id}.json", params: { + id: theme.id, theme: { default: true } + } + + expect(response.status).to eq(200) + expect(SiteSetting.default_theme_id).to eq(theme.id) + end + + it 'can unset default theme' do + SiteSetting.default_theme_id = theme.id + + put "/admin/themes/#{theme.id}.json", params: { + theme: { default: false } + } + + expect(response.status).to eq(200) + expect(SiteSetting.default_theme_id).to eq(-1) + end + + context 'when theme allowlist mode is enabled' do + before do + global_setting :allowed_theme_repos, " https://magic.com/repo.git, https://x.com/git" + end + + it 'unconditionally bans theme_fields from updating' do + r = RemoteTheme.create!(remote_url: "https://magic.com/repo.git") + theme.update!(remote_theme_id: r.id) + + put "/admin/themes/#{theme.id}.json", params: { + theme: { + name: 'my test name', + theme_fields: [ + { name: 'scss', target: 'common', value: '' }, + { name: 'scss', target: 'desktop', value: 'body{color: blue;}' }, + ] + } + } + + expect(response.status).to eq(403) + end + end + + it 'updates a theme' do + theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') + theme.save + + child_theme = Fabricate(:theme, component: true) + + upload = UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(Discourse.system_user.id) + + put "/admin/themes/#{theme.id}.json", params: { + theme: { + child_theme_ids: [child_theme.id], + name: 'my test name', + theme_fields: [ + { name: 'scss', target: 'common', value: '' }, + { name: 'scss', target: 'desktop', value: 'body{color: blue;}' }, + { name: 'bob', target: 'common', value: '', type_id: 2, upload_id: upload.id }, + ] + } + } + + expect(response.status).to eq(200) + + json = response.parsed_body + + fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] } + + expect(fields[0]["value"]).to eq('') + expect(fields[0]["upload_id"]).to eq(upload.id) + expect(fields[1]["value"]).to eq('body{color: blue;}') + expect(fields.length).to eq(2) + expect(json["theme"]["child_themes"].length).to eq(1) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end + + it 'prevents theme update when using ember css selectors' do + child_theme = Fabricate(:theme, component: true) + + put "/admin/themes/#{theme.id}.json", params: { + theme: { + child_theme_ids: [child_theme.id], + name: 'my test name', + theme_fields: [ + { name: 'scss', target: 'common', value: '' }, + { name: 'scss', target: 'desktop', value: '.ember-view{color: blue;}' }, + ] + } + } + + expect(response.status).to eq(200) + + json = response.parsed_body + + fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] } + expect(fields[0]["error"]).to eq(I18n.t("themes.ember_selector_error")) + + put "/admin/themes/#{theme.id}.json", params: { + theme: { + child_theme_ids: [child_theme.id], + name: 'my test name', + theme_fields: [ + { name: 'scss', target: 'common', value: '' }, + { name: 'scss', target: 'desktop', value: '#ember392{color: blue;}' }, + ] + } + } + + expect(response.status).to eq(200) + json = response.parsed_body + + fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] } + expect(fields[0]["error"]).to eq(I18n.t("themes.ember_selector_error")) + end + + it 'blocks remote theme fields from being locally edited' do r = RemoteTheme.create!(remote_url: "https://magic.com/repo.git") theme.update!(remote_theme_id: r.id) put "/admin/themes/#{theme.id}.json", params: { theme: { - name: 'my test name', theme_fields: [ { name: 'scss', target: 'common', value: '' }, - { name: 'scss', target: 'desktop', value: 'body{color: blue;}' }, + { name: 'header', target: 'common', value: 'filename.jpg', upload_id: 4 } ] } } expect(response.status).to eq(403) end - end - it 'updates a theme' do - theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') - theme.save + it 'allows zip-imported theme fields to be locally edited' do + r = RemoteTheme.create!(remote_url: "") + theme.update!(remote_theme_id: r.id) - child_theme = Fabricate(:theme, component: true) - - upload = UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(Discourse.system_user.id) - - put "/admin/themes/#{theme.id}.json", params: { - theme: { - child_theme_ids: [child_theme.id], - name: 'my test name', - theme_fields: [ - { name: 'scss', target: 'common', value: '' }, - { name: 'scss', target: 'desktop', value: 'body{color: blue;}' }, - { name: 'bob', target: 'common', value: '', type_id: 2, upload_id: upload.id }, - ] - } - } - - expect(response.status).to eq(200) - - json = response.parsed_body - - fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] } - - expect(fields[0]["value"]).to eq('') - expect(fields[0]["upload_id"]).to eq(upload.id) - expect(fields[1]["value"]).to eq('body{color: blue;}') - expect(fields.length).to eq(2) - expect(json["theme"]["child_themes"].length).to eq(1) - expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) - end - - it 'prevents theme update when using ember css selectors' do - child_theme = Fabricate(:theme, component: true) - - put "/admin/themes/#{theme.id}.json", params: { - theme: { - child_theme_ids: [child_theme.id], - name: 'my test name', - theme_fields: [ - { name: 'scss', target: 'common', value: '' }, - { name: 'scss', target: 'desktop', value: '.ember-view{color: blue;}' }, - ] - } - } - - expect(response.status).to eq(200) - - json = response.parsed_body - - fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] } - expect(fields[0]["error"]).to eq(I18n.t("themes.ember_selector_error")) - - put "/admin/themes/#{theme.id}.json", params: { - theme: { - child_theme_ids: [child_theme.id], - name: 'my test name', - theme_fields: [ - { name: 'scss', target: 'common', value: '' }, - { name: 'scss', target: 'desktop', value: '#ember392{color: blue;}' }, - ] - } - } - - expect(response.status).to eq(200) - json = response.parsed_body - - fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] } - expect(fields[0]["error"]).to eq(I18n.t("themes.ember_selector_error")) - end - - it 'blocks remote theme fields from being locally edited' do - r = RemoteTheme.create!(remote_url: "https://magic.com/repo.git") - theme.update!(remote_theme_id: r.id) - - put "/admin/themes/#{theme.id}.json", params: { - theme: { - theme_fields: [ - { name: 'scss', target: 'common', value: '' }, - { name: 'header', target: 'common', value: 'filename.jpg', upload_id: 4 } - ] - } - } - - expect(response.status).to eq(403) - end - - it 'allows zip-imported theme fields to be locally edited' do - r = RemoteTheme.create!(remote_url: "") - theme.update!(remote_theme_id: r.id) - - put "/admin/themes/#{theme.id}.json", params: { - theme: { - theme_fields: [ - { name: 'scss', target: 'common', value: '' }, - { name: 'header', target: 'common', value: 'filename.jpg', upload_id: 4 } - ] - } - } - - expect(response.status).to eq(200) - end - - it 'updates a child theme' do - child_theme = Fabricate(:theme, component: true) - put "/admin/themes/#{child_theme.id}.json", params: { - theme: { - parent_theme_ids: [theme.id], - } - } - expect(child_theme.parent_themes).to eq([theme]) - end - - it 'can update translations' do - theme.set_field(target: :translations, name: :en, value: { en: { somegroup: { somestring: "defaultstring" } } }.deep_stringify_keys.to_yaml) - theme.save! - - put "/admin/themes/#{theme.id}.json", params: { - theme: { - translations: { - "somegroup.somestring" => "overriddenstring" + put "/admin/themes/#{theme.id}.json", params: { + theme: { + theme_fields: [ + { name: 'scss', target: 'common', value: '' }, + { name: 'header', target: 'common', value: 'filename.jpg', upload_id: 4 } + ] } } - } - # Response correct - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["theme"]["translations"][0]["value"]).to eq("overriddenstring") + expect(response.status).to eq(200) + end - # Database correct - theme.reload - expect(theme.theme_translation_overrides.count).to eq(1) - expect(theme.theme_translation_overrides.first.translation_key).to eq("somegroup.somestring") - - # Set back to default - put "/admin/themes/#{theme.id}.json", params: { - theme: { - translations: { - "somegroup.somestring" => "defaultstring" + it 'updates a child theme' do + child_theme = Fabricate(:theme, component: true) + put "/admin/themes/#{child_theme.id}.json", params: { + theme: { + parent_theme_ids: [theme.id], } } - } - # Response correct - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["theme"]["translations"][0]["value"]).to eq("defaultstring") + expect(child_theme.parent_themes).to eq([theme]) + end - # Database correct - theme.reload - expect(theme.theme_translation_overrides.count).to eq(0) - end + it 'can update translations' do + theme.set_field(target: :translations, name: :en, value: { en: { somegroup: { somestring: "defaultstring" } } }.deep_stringify_keys.to_yaml) + theme.save! - it 'checking for updates saves the remote_theme record' do - theme.remote_theme = RemoteTheme.create!(remote_url: "http://discourse.org", remote_version: "a", local_version: "a", commits_behind: 0) - theme.save! - ThemeStore::GitImporter.any_instance.stubs(:import!) - ThemeStore::GitImporter.any_instance.stubs(:commits_since).returns(["b", 1]) - - put "/admin/themes/#{theme.id}.json", params: { - theme: { - remote_check: true + put "/admin/themes/#{theme.id}.json", params: { + theme: { + translations: { + "somegroup.somestring" => "overriddenstring" + } + } } - } - theme.reload - expect(theme.remote_theme.remote_version).to eq("b") - expect(theme.remote_theme.commits_behind).to eq(1) - end - it 'can disable component' do - child = Fabricate(:theme, component: true) + # Response correct + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["theme"]["translations"][0]["value"]).to eq("overriddenstring") - put "/admin/themes/#{child.id}.json", params: { - theme: { - enabled: false + # Database correct + theme.reload + expect(theme.theme_translation_overrides.count).to eq(1) + expect(theme.theme_translation_overrides.first.translation_key).to eq("somegroup.somestring") + + # Set back to default + put "/admin/themes/#{theme.id}.json", params: { + theme: { + translations: { + "somegroup.somestring" => "defaultstring" + } + } } - } - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["theme"]["enabled"]).to eq(false) - expect(UserHistory.where( - context: child.id.to_s, - action: UserHistory.actions[:disable_theme_component] - ).size).to eq(1) - expect(json["theme"]["disabled_by"]["id"]).to eq(admin.id) - end + # Response correct + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["theme"]["translations"][0]["value"]).to eq("defaultstring") - it "enabling/disabling a component creates the correct staff action log" do - child = Fabricate(:theme, component: true) - UserHistory.destroy_all + # Database correct + theme.reload + expect(theme.theme_translation_overrides.count).to eq(0) + end - put "/admin/themes/#{child.id}.json", params: { - theme: { - enabled: false + it 'checking for updates saves the remote_theme record' do + theme.remote_theme = RemoteTheme.create!(remote_url: "http://discourse.org", remote_version: "a", local_version: "a", commits_behind: 0) + theme.save! + ThemeStore::GitImporter.any_instance.stubs(:import!) + ThemeStore::GitImporter.any_instance.stubs(:commits_since).returns(["b", 1]) + + put "/admin/themes/#{theme.id}.json", params: { + theme: { + remote_check: true + } } - } - expect(response.status).to eq(200) + theme.reload + expect(theme.remote_theme.remote_version).to eq("b") + expect(theme.remote_theme.commits_behind).to eq(1) + end - expect(UserHistory.where( - context: child.id.to_s, - action: UserHistory.actions[:disable_theme_component] - ).size).to eq(1) - expect(UserHistory.where( - context: child.id.to_s, - action: UserHistory.actions[:enable_theme_component] - ).size).to eq(0) + it 'can disable component' do + child = Fabricate(:theme, component: true) - put "/admin/themes/#{child.id}.json", params: { - theme: { - enabled: true + put "/admin/themes/#{child.id}.json", params: { + theme: { + enabled: false + } } - } - expect(response.status).to eq(200) - json = response.parsed_body + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["theme"]["enabled"]).to eq(false) + expect(UserHistory.where( + context: child.id.to_s, + action: UserHistory.actions[:disable_theme_component] + ).size).to eq(1) + expect(json["theme"]["disabled_by"]["id"]).to eq(admin.id) + end - expect(UserHistory.where( - context: child.id.to_s, - action: UserHistory.actions[:disable_theme_component] - ).size).to eq(1) - expect(UserHistory.where( - context: child.id.to_s, - action: UserHistory.actions[:enable_theme_component] - ).size).to eq(1) + it "enabling/disabling a component creates the correct staff action log" do + child = Fabricate(:theme, component: true) + UserHistory.destroy_all - expect(json["theme"]["disabled_by"]).to eq(nil) - expect(json["theme"]["enabled"]).to eq(true) + put "/admin/themes/#{child.id}.json", params: { + theme: { + enabled: false + } + } + expect(response.status).to eq(200) + + expect(UserHistory.where( + context: child.id.to_s, + action: UserHistory.actions[:disable_theme_component] + ).size).to eq(1) + expect(UserHistory.where( + context: child.id.to_s, + action: UserHistory.actions[:enable_theme_component] + ).size).to eq(0) + + put "/admin/themes/#{child.id}.json", params: { + theme: { + enabled: true + } + } + expect(response.status).to eq(200) + json = response.parsed_body + + expect(UserHistory.where( + context: child.id.to_s, + action: UserHistory.actions[:disable_theme_component] + ).size).to eq(1) + expect(UserHistory.where( + context: child.id.to_s, + action: UserHistory.actions[:enable_theme_component] + ).size).to eq(1) + + expect(json["theme"]["disabled_by"]).to eq(nil) + expect(json["theme"]["enabled"]).to eq(true) + end + + it 'handles import errors on update' do + theme.create_remote_theme!(remote_url: "https://example.com/repository") + theme.save! + + # RemoteTheme is extensively tested, and setting up the test scaffold is a large overhead + # So use a stub here to test the controller + RemoteTheme.any_instance.stubs(:update_from_remote).raises(RemoteTheme::ImportError.new("error message")) + put "/admin/themes/#{theme.id}.json", params: { + theme: { remote_update: true } + } + expect(response.status).to eq(422) + expect(response.parsed_body["errors"].first).to eq("error message") + end + + it 'returns the right error message' do + theme.update!(component: true) + + put "/admin/themes/#{theme.id}.json", params: { + theme: { default: true } + } + + expect(response.status).to eq(400) + expect(response.parsed_body["errors"].first).to include(I18n.t("themes.errors.component_no_default")) + end + + it 'prevents converting the default theme to a component' do + SiteSetting.default_theme_id = theme.id + + put "/admin/themes/#{theme.id}.json", params: { + theme: { component: true } + } + + # should this error message be localized? InvalidParameters :component + expect(response.status).to eq(400) + expect(response.parsed_body["errors"].first).to include('component') + end end - it 'handles import errors on update' do - theme.create_remote_theme!(remote_url: "https://example.com/repository") - theme.save! + shared_examples "theme update not allowed" do + it "prevents updates with a 404 response" do + SiteSetting.default_theme_id = -1 - # RemoteTheme is extensively tested, and setting up the test scaffold is a large overhead - # So use a stub here to test the controller - RemoteTheme.any_instance.stubs(:update_from_remote).raises(RemoteTheme::ImportError.new("error message")) - put "/admin/themes/#{theme.id}.json", params: { - theme: { remote_update: true } - } - expect(response.status).to eq(422) - expect(response.parsed_body["errors"].first).to eq("error message") + put "/admin/themes/#{theme.id}.json", params: { + id: theme.id, theme: { default: true } + } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(SiteSetting.default_theme_id).not_to eq(theme.id) + end end - it 'returns the right error message' do - theme.update!(component: true) + context "when logged in as a moderator" do + before { sign_in(moderator) } - put "/admin/themes/#{theme.id}.json", params: { - theme: { default: true } - } - - expect(response.status).to eq(400) - expect(response.parsed_body["errors"].first).to include(I18n.t("themes.errors.component_no_default")) + include_examples "theme update not allowed" end - it 'prevents converting the default theme to a component' do - SiteSetting.default_theme_id = theme.id + context "when logged in as a non-staff user" do + before { sign_in(user) } - put "/admin/themes/#{theme.id}.json", params: { - theme: { component: true } - } - - # should this error message be localized? InvalidParameters :component - expect(response.status).to eq(400) - expect(response.parsed_body["errors"].first).to include('component') + include_examples "theme update not allowed" end end describe '#destroy' do let!(:theme) { Fabricate(:theme) } - it 'returns the right response when an invalid id is given' do - delete "/admin/themes/9999.json" + context "when logged in as an admin" do + before { sign_in(admin) } - expect(response.status).to eq(400) + it 'returns the right response when an invalid id is given' do + delete "/admin/themes/9999.json" + + expect(response.status).to eq(400) + end + + it "deletes the field's javascript cache" do + theme.set_field(target: :common, name: :header, value: '') + theme.save! + + javascript_cache = theme.theme_fields.find_by(target_id: Theme.targets[:common], name: :header).javascript_cache + expect(javascript_cache).to_not eq(nil) + + delete "/admin/themes/#{theme.id}.json" + + expect(response.status).to eq(204) + expect { theme.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { javascript_cache.reload }.to raise_error(ActiveRecord::RecordNotFound) + end end - it "deletes the field's javascript cache" do - theme.set_field(target: :common, name: :header, value: '') - theme.save! + shared_examples "theme deletion not allowed" do + it "prevent deletion with a 404 response" do + delete "/admin/themes/#{theme.id}.json" - javascript_cache = theme.theme_fields.find_by(target_id: Theme.targets[:common], name: :header).javascript_cache - expect(javascript_cache).to_not eq(nil) + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(theme.reload).to be_present + end + end - delete "/admin/themes/#{theme.id}.json" + context "when logged in as a moderator" do + before { sign_in(moderator) } - expect(response.status).to eq(204) - expect { theme.reload }.to raise_error(ActiveRecord::RecordNotFound) - expect { javascript_cache.reload }.to raise_error(ActiveRecord::RecordNotFound) + include_examples "theme deletion not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "theme deletion not allowed" end end describe '#preview' do - it "should return the right response when an invalid id is given" do - get "/admin/themes/9999/preview.json" + context "when logged in as an admin" do + before { sign_in(admin) } - expect(response.status).to eq(400) + it "should return the right response when an invalid id is given" do + get "/admin/themes/9999/preview.json" + + expect(response.status).to eq(400) + end + end + + shared_examples "theme previews inaccessible" do + it "denies access with a 404 response" do + theme = Fabricate(:theme) + + get "/admin/themes/#{theme.id}/preview.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 "theme previews inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "theme previews inaccessible" end end @@ -711,30 +940,61 @@ RSpec.describe Admin::ThemesController do theme.save! end - it "should update a theme setting" do - put "/admin/themes/#{theme.id}/setting.json", params: { - name: "bg", - value: "green" - } + context "when logged in as an admin" do + before { sign_in(admin) } - expect(response.status).to eq(200) - expect(response.parsed_body["bg"]).to eq("green") + it "should update a theme setting" do + put "/admin/themes/#{theme.id}/setting.json", params: { + name: "bg", + value: "green" + } - theme.reload - expect(theme.cached_settings[:bg]).to eq("green") - user_history = UserHistory.last + expect(response.status).to eq(200) + expect(response.parsed_body["bg"]).to eq("green") - expect(user_history.action).to eq( - UserHistory.actions[:change_theme_setting] - ) + theme.reload + expect(theme.cached_settings[:bg]).to eq("green") + user_history = UserHistory.last + + expect(user_history.action).to eq( + UserHistory.actions[:change_theme_setting] + ) + end + + it "should clear a theme setting" do + put "/admin/themes/#{theme.id}/setting.json", params: { name: "bg" } + theme.reload + + expect(response.status).to eq(200) + expect(theme.cached_settings[:bg]).to eq("") + end end - it "should clear a theme setting" do - put "/admin/themes/#{theme.id}/setting.json", params: { name: "bg" } - theme.reload + shared_examples "theme update not allowed" do + it "prevents updates with a 404 response" do + put "/admin/themes/#{theme.id}/setting.json", params: { + name: "bg", + value: "green" + } - expect(response.status).to eq(200) - expect(theme.cached_settings[:bg]).to eq("") + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + + theme.reload + expect(theme.cached_settings[:bg]).to eq("red") + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "theme update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "theme update not allowed" end end end diff --git a/spec/requests/admin/user_fields_controller_spec.rb b/spec/requests/admin/user_fields_controller_spec.rb index 8949a46c39d..450c689cee1 100644 --- a/spec/requests/admin/user_fields_controller_spec.rb +++ b/spec/requests/admin/user_fields_controller_spec.rb @@ -1,18 +1,14 @@ # frozen_string_literal: true RSpec.describe Admin::UserFieldsController do - it "is a subclass of AdminController" do - expect(Admin::UserFieldsController < Admin::AdminController).to eq(true) - end + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } - context "when logged in" do - fab!(:admin) { Fabricate(:admin) } + describe '#create' do + context "when logged in as an admin" do + before { sign_in(admin) } - before do - sign_in(admin) - end - - describe '#create' do it "creates a user field" do expect { post "/admin/customize/user_fields.json", params: { @@ -41,8 +37,37 @@ RSpec.describe Admin::UserFieldsController do end end - describe '#index' do - fab!(:user_field) { Fabricate(:user_field) } + shared_examples "user field creation not allowed" do + it "prevents creation with a 404 response" do + expect do + post "/admin/customize/user_fields.json", params: { + user_field: { name: 'hello', description: 'hello desc', field_type: 'text' } + } + end.not_to change { UserField.count } + + 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 field creation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "user field creation not allowed" + end + end + + describe '#index' do + fab!(:user_field) { Fabricate(:user_field) } + + context "when logged in as an admin" do + before { sign_in(admin) } it "returns a list of user fields" do get "/admin/customize/user_fields.json" @@ -52,8 +77,34 @@ RSpec.describe Admin::UserFieldsController do end end - describe '#destroy' do - fab!(:user_field) { Fabricate(:user_field) } + shared_examples "user fields inaccessible" do + it "denies access with a 404 response" do + get "/admin/customize/user_fields.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(response.parsed_body['user_fields']).to be_nil + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "user fields inaccessible" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "user fields inaccessible" + end + end + + describe '#destroy' do + fab!(:user_field) { Fabricate(:user_field) } + + context "when logged in as an admin" do + before { sign_in(admin) } it "deletes the user field" do expect { @@ -63,8 +114,35 @@ RSpec.describe Admin::UserFieldsController do end end - describe '#update' do - fab!(:user_field) { Fabricate(:user_field) } + shared_examples "user field deletion not allowed" do + it "prevents deletion with a 404 response" do + expect do + delete "/admin/customize/user_fields/#{user_field.id}.json" + end.not_to change { UserField.count } + + 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 field deletion not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "user field deletion not allowed" + end + end + + describe '#update' do + fab!(:user_field) { Fabricate(:user_field) } + + context "when logged in as an admin" do + before { sign_in(admin) } it "updates the user field" do put "/admin/customize/user_fields/#{user_field.id}.json", params: { @@ -138,5 +216,34 @@ RSpec.describe Admin::UserFieldsController do }.to change { DirectoryColumn.count }.by(-1) end end + + shared_examples "user field update not allowed" do + it "prevents updates with a 404 response" do + user_field.reload + original_name = user_field.name + + put "/admin/customize/user_fields/#{user_field.id}.json", params: { + user_field: { name: 'fraggle', field_type: 'confirm', description: 'muppet' } + } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + + user_field.reload + expect(user_field.name).to eq(original_name) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "user field update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "user field update not allowed" + end end end diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb index 1fc3bb70b4d..e547da8d6ad 100644 --- a/spec/requests/admin/users_controller_spec.rb +++ b/spec/requests/admin/users_controller_spec.rb @@ -5,136 +5,225 @@ require 'rotp' RSpec.describe Admin::UsersController do fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } fab!(:user) { Fabricate(:user) } fab!(:coding_horror) { Fabricate(:coding_horror) } - it 'is a subclass of StaffController' do - expect(Admin::UsersController < Admin::StaffController).to eq(true) - end - - before do - sign_in(admin) - end - describe '#index' do - 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 logged in as an admin" do + before { sign_in(admin) } - context 'when showing emails' do - it "returns email for all the users" do - get "/admin/users/list.json", params: { show_emails: "true" } + it 'returns success with JSON' do + get "/admin/users/list.json" expect(response.status).to eq(200) - data = response.parsed_body - data.each do |user| - expect(user["email"]).to be_present + 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 do |user| + expect(user["email"]).to be_present + end + 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" - 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) + expect(response.parsed_body).to be_present end + 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) + 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 'with an existing user' do - it 'returns success' do - get "/admin/users/#{user.id}.json" - expect(response.status).to eq(200) + 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 - 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') + 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 'with a non-existing user' do - it 'returns 404 error' do - get "/admin/users/0.json" + 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 + 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 '#approve' do let(:evil_trout) { Fabricate(:evil_trout) } + before do SiteSetting.must_approve_users = true end - it "raises an error when the user doesn't have permission" do - sign_in(user) - put "/admin/users/#{evil_trout.id}/approve.json" - expect(response.status).to eq(404) - evil_trout.reload - expect(evil_trout.approved).to eq(false) + 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 - it "will create 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 + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "user approval possible" 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) + 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 do SiteSetting.must_approve_users = true end - let(:evil_trout) { Fabricate(:evil_trout) } + 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 "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) + 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 - it "won't approve the user when not allowed" do - sign_in(user) - put "/admin/users/approve-bulk.json", params: { users: [evil_trout.id] } - expect(response.status).to eq(404) - evil_trout.reload - expect(evil_trout.approved).to eq(false) + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "bulk user approval possible" 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) + 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 @@ -146,211 +235,263 @@ RSpec.describe Admin::UsersController do post_id: created_post.id } end - it "works properly" do - expect(user).not_to be_suspended + shared_examples "suspension of active user possible" do + it "suspends user" do + expect(user).not_to be_suspended - expect 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(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 + + context "when logged in as an admin" do + before { sign_in(admin) } + + shared_examples "suspension of active user possible" + + 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" } - end.not_to change { Jobs::CriticalUserEmail.jobs.size } - expect(response.status).to eq(200) + put "/admin/users/#{user.id}/suspend.json", params: { + suspend_until: 5.hours.from_now, + reason: "because I said so too" + } - 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 - - 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: FreedomPatches::Rails4.time_ago_in_words(user.suspend_record.created_at, true, scope: :'datetime.distance_in_words_verbose') + expect(response.status).to eq(409) + expect(response.parsed_body["message"]).to eq( + I18n.t( + "user.already_suspended", + staff: admin.username, + time_ago: FreedomPatches::Rails4.time_ago_in_words(user.suspend_record.created_at, true, scope: :'datetime.distance_in_words_verbose') + ) ) - ) - end + 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 + 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 + 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 - context "with an associated post" do - it "can have an associated post" do - put "/admin/users/#{user.id}/suspend.json", params: suspend_params + 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.post_id).to eq(created_post.id) + expect(log).to be_present + expect(log.details).to match(/short reason/) + expect(log.details).to match(/long reason/) 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 + 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) - 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 + put "/admin/users/#{user.id}/suspend.json", params: suspend_params 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 + user.reload + expect(user).to be_suspended - 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) + post "/bookmarks.json", params: { + post_id: Fabricate(:post).id + }, headers: { HTTP_API_KEY: api_key.key } + expect(response.status).to eq(403) 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" - } + context "when logged in as a moderator" do + before { sign_in(moderator) } - 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/) + include_examples "suspension of active user possible" 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) + context "when logged in as a non-staff user" do + before { sign_in(user) } - put "/admin/users/#{user.id}/suspend.json", params: suspend_params - expect(response.status).to eq(200) + 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 } - user.reload - expect(user).to be_suspended + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) - post "/bookmarks.json", params: { - post_id: Fabricate(:post).id - }, headers: { HTTP_API_KEY: api_key.key } - expect(response.status).to eq(403) + 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 '#revoke_admin' do fab!(:another_admin) { Fabricate(:admin) } - it 'raises an error unless the user can revoke access' do - sign_in(user) - put "/admin/users/#{another_admin.id}/revoke_admin.json" - expect(response.status).to eq(404) - another_admin.reload - expect(another_admin.admin).to eq(true) + 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 - 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) + 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 - it 'returns detailed user schema' do - put "/admin/users/#{another_admin.id}/revoke_admin.json" - 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) + 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 @@ -361,325 +502,464 @@ RSpec.describe Admin::UsersController do Discourse.redis.flushdb end - it "returns a 404 when the acting user doesn't have permission" do - sign_in(user) - put "/admin/users/#{another_user.id}/grant_admin.json" - expect(response.status).to eq(404) - expect(AdminConfirmation.exists_for?(another_user.id)).to eq(false) + 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 - it "returns a 404 when the acting user doesn't have permission even if they have 2FA enabled" do - Fabricate(:user_second_factor_totp, user: user) - sign_in(user) - put "/admin/users/#{another_user.id}/grant_admin.json" - expect(response.status).to eq(404) - expect(AdminConfirmation.exists_for?(another_user.id)).to eq(false) + shared_examples "admin grants not allowed" do + context "with 2FA enabled" do + before do + Fabricate(:user_second_factor_totp, user: user) + end + + 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 - it "returns a 404 if the username doesn't exist" do - put "/admin/users/123123/grant_admin.json" - expect(response.status).to eq(404) + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "admin grants not allowed" 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 + context "when logged in as a non-staff user" do + before { sign_in(user) } - 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) + include_examples "admin grants not allowed" end end describe '#add_group' do fab!(:group) { Fabricate(:group) } - it 'adds the user to the group' do - post "/admin/users/#{user.id}/groups.json", params: { - group_id: group.id - } + context "when logged in as an admin" do + before { sign_in(admin) } - 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 + it 'adds the user to the group' 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"]) + 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 - 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" + context "when logged in as an admin" do + before { sign_in(admin) } - expect(response.status).to eq(200) - expect(user.reload.primary_group).to eq(nil) + 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 - it 'returns not-found error when there is no group' do - delete "/admin/users/#{user.id}/groups/9090.json" + 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) - expect(response.status).to eq(404) + 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 - it 'does not allow removing owners from an automatic group' do - group = Fabricate(:group, users: [user], automatic: true) + context "when logged in as a moderator" do + before { sign_in(moderator) } - delete "/admin/users/#{user.id}/groups/#{group.id}.json" + include_examples "removing user from groups not allowed" + end - expect(response.status).to eq(422) - expect(response.parsed_body["errors"]).to eq(["You cannot modify an automatic group"]) + 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) { + fab!(:another_user) do coding_horror.update!(created_at: 1.month.ago) coding_horror - } - - it "raises an error when the user doesn't have permission" do - sign_in(user) - put "/admin/users/#{another_user.id}/trust_level.json" - expect(response.status).to eq(404) end - it "returns a 404 if the username doesn't exist" do - put "/admin/users/123123/trust_level.json" - expect(response.status).to eq(404) + 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 - it "upgrades the user's trust level" do - put "/admin/users/#{another_user.id}/trust_level.json", params: { level: 2 } + context "when logged in as an admin" do + let(:acting_user) { admin } - expect(response.status).to eq(200) - another_user.reload - expect(another_user.trust_level).to eq(2) + before { sign_in(admin) } - expect(UserHistory.where( - target_user: another_user, - acting_user: admin, - action: UserHistory.actions[:change_trust_level] - ).count).to eq(1) + include_examples "trust level updates possible" 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]) + context "when logged in as a moderator" do + let(:acting_user) { moderator } - put "/admin/users/#{another_user.id}/trust_level.json", params: { - level: TrustLevel[0] - } + before { sign_in(moderator) } - 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]) + 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 } - it "raises an error when the user doesn't have permission" do - sign_in(user) - put "/admin/users/#{another_user.id}/grant_moderation.json" - expect(response.status).to eq(404) - end + 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 } - }) do - put "/admin/users/#{another_user.id}/grant_moderation.json" + 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 - expect(response.status).to eq(200) - another_user.reload - expect(another_user.moderator).to eq(true) + 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 } + }) do + put "/admin/users/#{another_user.id}/grant_moderation.json" + end + + 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 - it 'returns detailed user schema' do - put "/admin/users/#{another_user.id}/grant_moderation.json" - expect(response.parsed_body['can_be_merged']).to eq(false) - expect(response.parsed_body['can_be_anonymized']).to eq(false) + 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!(:moderator) { Fabricate(:moderator) } + fab!(:another_moderator) { Fabricate(:moderator) } - it 'raises an error unless the user can revoke access' do - sign_in(user) - put "/admin/users/#{moderator.id}/revoke_moderation.json" - expect(response.status).to eq(404) - moderator.reload - expect(moderator.moderator).to eq(true) + 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 - it 'updates the moderator flag' do - put "/admin/users/#{moderator.id}/revoke_moderation.json" - expect(response.status).to eq(200) - moderator.reload - expect(moderator.moderator).to eq(false) + 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 - it 'returns detailed user schema' do - put "/admin/users/#{moderator.id}/revoke_moderation.json" - expect(response.parsed_body['can_be_merged']).to eq(true) - expect(response.parsed_body['can_be_anonymized']).to eq(true) + 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 @@ -688,426 +968,655 @@ RSpec.describe Admin::UsersController do fab!(:another_user) { coding_horror } fab!(:another_group) { Fabricate(:group, title: 'New') } - it "raises an error when the user doesn't have permission" do - sign_in(user) - put "/admin/users/#{another_user.id}/primary_group.json" - expect(response.status).to eq(404) - another_user.reload - expect(another_user.primary_group_id).to eq(nil) + 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 - it "returns a 404 if the user doesn't exist" do - put "/admin/users/123123/primary_group.json" - expect(response.status).to eq(404) + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "primary group updates possible" 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 - } + context "when logged in as a moderator" do + before { sign_in(moderator) } - expect(response.status).to eq(200) - another_user.reload - expect(another_user.primary_group_id).to eq(group.id) + include_examples "primary group updates possible" 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 - } + context "when logged in as a non-staff user" do + before { sign_in(user) } - expect(response.status).to eq(200) - another_user.reload - expect(another_user.primary_group_id).to eq(nil) - end + 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 + } - 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') + 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) } - 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" + 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) - 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 post" do + let(:topic) { Fabricate(:topic, user: delete_me) } + let!(:post) { Fabricate(:post, topic: topic, user: delete_me) } - context "when user has reviewable flagged post which was handled" do - let!(:reviewable) { Fabricate(:reviewable_flagged_post, created_by: admin, target_created_by: delete_me, target: post, topic: topic, status: 4) } + 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 - it "deletes the user record" do - delete "/admin/users/#{delete_me.id}.json", params: { delete_posts: true, delete_as_spammer: true } + 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 - end - end - it "blocks the e-mail if block_email param is is true" do - user_emails = delete_me.user_emails.pluck(:email) + context "when user has reviewable flagged post which was handled" do + let!(:reviewable) { Fabricate(:reviewable_flagged_post, created_by: admin, target_created_by: delete_me, target: post, topic: topic, status: 4) } - 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) } + 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 urls if block_url param is true" do - delete "/admin/users/#{delete_me.id}.json", params: { delete_posts: true, block_urls: true } + 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(ScreenedUrl.exists?(url: @urls)).to eq(true) + expect(ScreenedEmail.exists?(email: user_emails)).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 } + 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(ScreenedUrl.exists?(url: @urls)).to eq(false) + expect(ScreenedEmail.exists?(email: user_emails)).to eq(false) end - it "does not block the urls by default" do - delete "/admin/users/#{delete_me.id}.json", params: { delete_posts: true, block_urls: false } + 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(ScreenedUrl.exists?(url: @urls)).to eq(false) + 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, block_urls: false } + 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 - 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) + 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 '#activate' do fab!(:reg_user) { Fabricate(:inactive_user) } - 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) + 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 - it "should confirm email even when the tokens are expired" do - reg_user.email_tokens.update_all(confirmed: false, expired: true) + context "when logged in as an admin" do + before { sign_in(admin) } - reg_user.reload - expect(reg_user.email_confirmed?).to eq(false) + include_examples "user activation possible" + end - put "/admin/users/#{reg_user.id}/activate.json" - expect(response.status).to eq(200) + context "when logged in as a moderator" do + before { sign_in(moderator) } - reg_user.reload - expect(reg_user.email_confirmed?).to eq(true) + 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) } - 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) + 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) } - 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") + 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 - it "returns 404 when user_id does not exist" do - post "/admin/users/123123drink/log_out.json" - expect(response.status).to eq(404) + shared_examples "user log out not allowed" do + it "prevents loging 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) } - it "raises an error when the user doesn't have permission" do - sign_in(user) - put "/admin/users/#{reg_user.id}/silence.json" - expect(response.status).to eq(404) - reg_user.reload - expect(reg_user).not_to be_silenced - end + 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 "returns a 404 if the user doesn't exist" do + put "/admin/users/123123/silence.json" + expect(response.status).to eq(404) + end - it "punishes the user for spamming" do - put "/admin/users/#{reg_user.id}/silence.json" - 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 "punishes the user for spamming" do + put "/admin/users/#{reg_user.id}/silence.json" + 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) + it "can have an associated post" do + silence_post = Fabricate(:post, user: reg_user) - put "/admin/users/#{reg_user.id}/silence.json", params: { - 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: { - 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: { - message: "Email this to the user" + post_id: silence_post.id, + post_action: 'edit', + post_edit: "this is the new contents for the post" } - end.to change { Jobs::CriticalUserEmail.jobs.size }.by(1) + expect(response.status).to eq(200) - expect(response.status).to eq(200) - reg_user.reload - expect(reg_user).to be_silenced + 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: { + 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: { + 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: FreedomPatches::Rails4.time_ago_in_words(user.silenced_record.created_at, true, scope: :'datetime.distance_in_words_verbose') + ) + ) + end 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" - } + context "when logged in as a moderator" do + before { sign_in(moderator) } - put "/admin/users/#{user.id}/silence.json", params: { - silenced_till: 5.hours.from_now, - reason: "because I said so too" - } + it "silences user" do + put "/admin/users/#{reg_user.id}/silence.json" - expect(response.status).to eq(409) - expect(response.parsed_body["message"]).to eq( - I18n.t( - "user.already_silenced", - staff: admin.username, - time_ago: FreedomPatches::Rails4.time_ago_in_words(user.silenced_record.created_at, true, scope: :'datetime.distance_in_words_verbose') - ) - ) + expect(response.status).to eq(200) + reg_user.reload + expect(reg_user).to be_silenced + expect(reg_user.silenced_record).to be_present + 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) } - it "raises an error when the user doesn't have permission" do - sign_in(user) - put "/admin/users/#{reg_user.id}/unsilence.json" - expect(response.status).to eq(404) + 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 - it "returns a 403 if the user doesn't exist" do - put "/admin/users/123123/unsilence.json" - expect(response.status).to eq(404) + context "when logged in as an admin" do + before { sign_in(admin) } + + include_examples "unsilencing user possible" 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 + 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 - it "retrieves IP info" do - ip = "81.2.69.142" + 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") + 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: [6255148, 2635167, 2643743, 6269131], - hostname: "ip-81-2-69-142.example.com", - location: "London, England, United Kingdom", - region: "England", - latitude: 51.5142, - longitude: -0.0931, - ) + 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: [6255148, 2635167, 2643743, 6269131], + 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 - it "works" do - user_a = Fabricate(:user, ip_address: "42.42.42.42") - user_b = Fabricate(:user, ip_address: "42.42.42.42") + 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) + 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 @@ -1126,89 +1635,130 @@ RSpec.describe Admin::UsersController do sso.sso_secret = sso_secret end - 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" + context "when logged in as an admin" do + before { sign_in(admin) } - user = DiscourseConnect.parse(sso.payload, secure_session: read_secure_session).lookup_or_create_user + 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" - sso.name = "Bill" - sso.username = "Hokli$$!!" - sso.email = "bob2@bob.com" + user = DiscourseConnect.parse(sso.payload, secure_session: read_secure_session).lookup_or_create_user - post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) - expect(response.status).to eq(200) + sso.name = "Bill" + sso.username = "Hokli$$!!" + sso.email = "bob2@bob.com" - user.reload - expect(user.email).to eq("bob2@bob.com") - expect(user.name).to eq("Bill") - expect(user.username).to eq("Hokli") - 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) + 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 '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 - 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" + 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" - 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") + 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 - 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" + context "when logged in as a moderator" do + before { sign_in(moderator) } - 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"]) + include_examples "sso sync not allowed" 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')) + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "sso sync not allowed" end end @@ -1217,12 +1767,15 @@ RSpec.describe Admin::UsersController do let(:second_factor_backup) { user.generate_backup_codes } let(:security_key) { Fabricate(:user_security_key, user: user) } - describe 'as an admin' do + before do + second_factor + second_factor_backup + security_key + end + + context "when logged in as an admin" do before do sign_in(admin) - second_factor - second_factor_backup - security_key expect(user.reload.user_second_factors.totps.first).to eq(second_factor) end @@ -1275,73 +1828,147 @@ RSpec.describe Admin::UsersController do 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 - fab!(:moderator) { Fabricate(:moderator) } let(:logger) { StaffActionLogger.new(admin) } - it "doesn't allow moderators to clear a user's history" do - sign_in(moderator) - delete "/admin/users/#{user.id}/penalty_history.json" - expect(response.code).to eq("404") + 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 - def find_logs(action) - UserHistory.where(target_user_id: user.id, action: UserHistory.actions[action]) + 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 - 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) + context "when logged in as a moderator" do + before { sign_in(moderator) } - sign_in(admin) - 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 + 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 - describe 'when user is is invalid' do - it 'should return the right response' do - put "/admin/users/nothing/delete_posts_batch.json" + 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) - 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) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(response.parsed_body["posts_deleted"]).to be_nil end end end @@ -1351,45 +1978,147 @@ RSpec.describe Admin::UsersController do fab!(:topic) { Fabricate(:topic, user: user) } fab!(:first_post) { Fabricate(:post, topic: topic, user: user) } - 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 - } + context "when logged in as an admin" do + before { sign_in(admin) } - 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) + 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) { SingleSignOnRecord.create!(user_id: user.id, external_id: '12345', external_email: user.email, last_payload: '') } + fab!(:sso_record) do + SingleSignOnRecord.create!( + user_id: user.id, + external_id: '12345', + external_email: user.email, + last_payload: '' + ) + end - it "deletes the record" do + before do SiteSetting.discourse_connect_url = "https://www.example.com/sso" SiteSetting.enable_discourse_connect = true + end - delete "/admin/users/#{user.id}/sso_record.json" - expect(response.status).to eq(200) - expect(user.single_sign_on_record).to eq(nil) + 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 "#anonymize" 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 + 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 - 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') + 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 end diff --git a/spec/requests/admin/versions_controller_spec.rb b/spec/requests/admin/versions_controller_spec.rb index 60515532aa6..1d138641909 100644 --- a/spec/requests/admin/versions_controller_spec.rb +++ b/spec/requests/admin/versions_controller_spec.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true RSpec.describe Admin::VersionsController do + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } before do Jobs::VersionCheck.any_instance.stubs(:execute).returns(true) @@ -9,17 +12,8 @@ RSpec.describe Admin::VersionsController do DiscourseUpdates.stubs(:critical_updates_available?).returns(false) end - it "is a subclass of StaffController" do - expect(Admin::VersionsController < Admin::StaffController).to eq(true) - end - - context 'while logged in as an admin' do - fab!(:admin) { Fabricate(:admin) } - before do - sign_in(admin) - end - - describe 'show' do + describe "#show" do + shared_examples "version info accessible" do it 'should return the currently available version' do get "/admin/version_check.json" expect(response.status).to eq(200) @@ -34,5 +28,30 @@ RSpec.describe Admin::VersionsController do expect(json['installed_version']).to eq(Discourse::VERSION::STRING) end end + + context 'when logged in as admin' do + before { sign_in(admin) } + + include_examples "version info accessible" + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "version info accessible" + 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/version_check.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(response.parsed_body["latest_version"]).to be_nil + expect(response.parsed_body["installed_version"]).to be_nil + end + end end end diff --git a/spec/requests/admin/watched_words_controller_spec.rb b/spec/requests/admin/watched_words_controller_spec.rb index 24a153e11f3..80fbd2e32cc 100644 --- a/spec/requests/admin/watched_words_controller_spec.rb +++ b/spec/requests/admin/watched_words_controller_spec.rb @@ -6,10 +6,6 @@ RSpec.describe Admin::WatchedWordsController do fab!(:admin) { Fabricate(:admin) } fab!(:user) { Fabricate(:user) } - it "is a subclass of StaffController" do - expect(Admin::WatchedWordsController < Admin::StaffController).to eq(true) - end - describe '#destroy' do fab!(:watched_word) { Fabricate(:watched_word) } diff --git a/spec/requests/admin/web_hooks_controller_spec.rb b/spec/requests/admin/web_hooks_controller_spec.rb index ec1abb9fcf1..66713bf57ec 100644 --- a/spec/requests/admin/web_hooks_controller_spec.rb +++ b/spec/requests/admin/web_hooks_controller_spec.rb @@ -1,20 +1,15 @@ # frozen_string_literal: true RSpec.describe Admin::WebHooksController do + fab!(:web_hook) { Fabricate(:web_hook) } + fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } + fab!(:user) { Fabricate(:user) } - it 'is a subclass of AdminController' do - expect(Admin::WebHooksController < Admin::AdminController).to eq(true) - end + describe '#create' do + context "when logged in as admin" do + before { sign_in(admin) } - context 'while logged in as an admin' do - fab!(:web_hook) { Fabricate(:web_hook) } - fab!(:admin) { Fabricate(:admin) } - - before do - sign_in(admin) - end - - describe '#create' do it 'creates a webhook' do post "/admin/api/web_hooks.json", params: { web_hook: { @@ -58,7 +53,45 @@ RSpec.describe Admin::WebHooksController do end end - describe '#update' do + shared_examples "webhook creation not allowed" do + it "prevents creation with a 404 response" do + post "/admin/api/web_hooks.json", params: { + web_hook: { + payload_url: 'https://meta.discourse.org/', + content_type: 1, + secret: "a_secret_for_webhooks", + wildcard_web_hook: false, + active: true, + verify_certificate: true, + web_hook_event_type_ids: [1], + group_ids: [], + category_ids: [] + } + } + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(response.parsed_body["web_hook"]).to be_nil + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "webhook creation not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "webhook creation not allowed" + end + end + + describe '#update' do + context "when logged in as admin" do + before { sign_in(admin) } + it "logs webhook update" do put "/admin/api/web_hooks/#{web_hook.id}.json", params: { web_hook: { active: false, payload_url: "https://test.com" } @@ -71,7 +104,37 @@ RSpec.describe Admin::WebHooksController do end end - describe '#destroy' do + shared_examples "webhook update not allowed" do + it "prevents updates with a 404 response" do + current_payload_url = web_hook.payload_url + put "/admin/api/web_hooks/#{web_hook.id}.json", params: { + web_hook: { active: false, payload_url: "https://test.com" } + } + + web_hook.reload + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(web_hook.payload_url).to eq(current_payload_url) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "webhook update not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "webhook update not allowed" + end + end + + describe '#destroy' do + context "when logged in as admin" do + before { sign_in(admin) } + it "logs webhook destroy" do delete "/admin/api/web_hooks/#{web_hook.id}.json", params: { web_hook: { active: false, payload_url: "https://test.com" } @@ -82,7 +145,33 @@ RSpec.describe Admin::WebHooksController do end end - describe '#ping' do + shared_examples "webhook deletion not allowed" do + it "prevents deletion with a 404 response" do + delete "/admin/api/web_hooks/#{web_hook.id}.json" + + expect(response.status).to eq(404) + expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) + expect(web_hook.reload).to be_present + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + include_examples "webhook deletion not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "webhook deletion not allowed" + end + end + + describe '#ping' do + context "when logged in as admin" do + before { sign_in(admin) } + it 'enqueues the ping event' do expect do post "/admin/api/web_hooks/#{web_hook.id}/ping.json" @@ -95,62 +184,87 @@ RSpec.describe Admin::WebHooksController do end end - describe '#redeliver_event' do - let!(:web_hook_event) do - WebHookEvent.create!( - web_hook: web_hook, - payload: "abc", - headers: JSON.dump(aa: "1", bb: "2"), + shared_examples "webhook ping not allowed" do + it "fails to enqueue a ping with 404 response" do + expect do + post "/admin/api/web_hooks/#{web_hook.id}/ping.json" + end.not_to change { Jobs::EmitWebHookEvent.jobs.size } + + 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 "webhook ping not allowed" + end + + context "when logged in as a non-staff user" do + before { sign_in(user) } + + include_examples "webhook ping not allowed" + end + end + + describe '#redeliver_event' do + let!(:web_hook_event) do + WebHookEvent.create!( + web_hook: web_hook, + payload: "abc", + headers: JSON.dump(aa: "1", bb: "2"), + ) + end + + before { sign_in(admin) } + + it 'emits the web hook and updates the response headers and body' do + stub_request(:post, web_hook.payload_url) + .with(body: "abc", headers: { "aa" => 1, "bb" => 2 }) + .to_return( + status: 402, + body: "efg", + headers: { "Content-Type" => "application/json", "yoo" => "man" } ) - end + post "/admin/api/web_hooks/#{web_hook.id}/events/#{web_hook_event.id}/redeliver.json" + expect(response.status).to eq(200) - it 'emits the web hook and updates the response headers and body' do - stub_request(:post, web_hook.payload_url) - .with(body: "abc", headers: { "aa" => 1, "bb" => 2 }) - .to_return( - status: 402, - body: "efg", - headers: { "Content-Type" => "application/json", "yoo" => "man" } - ) + parsed_event = response.parsed_body["web_hook_event"] + expect(parsed_event["id"]).to eq(web_hook_event.id) + expect(parsed_event["status"]).to eq(402) + + expect(JSON.parse(parsed_event["headers"])).to eq({ "aa" => "1", "bb" => "2" }) + expect(parsed_event["payload"]).to eq("abc") + + expect(JSON.parse(parsed_event["response_headers"])).to eq({ "content-type" => "application/json", "yoo" => "man" }) + expect(parsed_event["response_body"]).to eq("efg") + end + + it "doesn't emit the web hook if the payload URL resolves to an internal IP" do + FinalDestination::TestHelper.stub_to_fail do post "/admin/api/web_hooks/#{web_hook.id}/events/#{web_hook_event.id}/redeliver.json" - expect(response.status).to eq(200) - - parsed_event = response.parsed_body["web_hook_event"] - expect(parsed_event["id"]).to eq(web_hook_event.id) - expect(parsed_event["status"]).to eq(402) - - expect(JSON.parse(parsed_event["headers"])).to eq({ "aa" => "1", "bb" => "2" }) - expect(parsed_event["payload"]).to eq("abc") - - expect(JSON.parse(parsed_event["response_headers"])).to eq({ "content-type" => "application/json", "yoo" => "man" }) - expect(parsed_event["response_body"]).to eq("efg") end + expect(response.status).to eq(200) - it "doesn't emit the web hook if the payload URL resolves to an internal IP" do - FinalDestination::TestHelper.stub_to_fail do - post "/admin/api/web_hooks/#{web_hook.id}/events/#{web_hook_event.id}/redeliver.json" - end - expect(response.status).to eq(200) + parsed_event = response.parsed_body["web_hook_event"] + expect(parsed_event["id"]).to eq(web_hook_event.id) + expect(parsed_event["response_headers"]).to eq({ error: I18n.t("webhooks.payload_url.blocked_or_internal") }.to_json) + expect(parsed_event["status"]).to eq(-1) + expect(parsed_event["response_body"]).to eq(nil) + end - parsed_event = response.parsed_body["web_hook_event"] - expect(parsed_event["id"]).to eq(web_hook_event.id) - expect(parsed_event["response_headers"]).to eq({ error: I18n.t("webhooks.payload_url.blocked_or_internal") }.to_json) - expect(parsed_event["status"]).to eq(-1) - expect(parsed_event["response_body"]).to eq(nil) + it "doesn't emit the web hook if the payload URL resolves to a blocked IP" do + FinalDestination::TestHelper.stub_to_fail do + post "/admin/api/web_hooks/#{web_hook.id}/events/#{web_hook_event.id}/redeliver.json" end + expect(response.status).to eq(200) - it "doesn't emit the web hook if the payload URL resolves to a blocked IP" do - FinalDestination::TestHelper.stub_to_fail do - post "/admin/api/web_hooks/#{web_hook.id}/events/#{web_hook_event.id}/redeliver.json" - end - expect(response.status).to eq(200) - - parsed_event = response.parsed_body["web_hook_event"] - expect(parsed_event["id"]).to eq(web_hook_event.id) - expect(parsed_event["response_headers"]).to eq({ error: I18n.t("webhooks.payload_url.blocked_or_internal") }.to_json) - expect(parsed_event["status"]).to eq(-1) - expect(parsed_event["response_body"]).to eq(nil) - end + parsed_event = response.parsed_body["web_hook_event"] + expect(parsed_event["id"]).to eq(web_hook_event.id) + expect(parsed_event["response_headers"]).to eq({ error: I18n.t("webhooks.payload_url.blocked_or_internal") }.to_json) + expect(parsed_event["status"]).to eq(-1) + expect(parsed_event["response_body"]).to eq(nil) end end end