about summary refs log tree commit diff
path: root/spec/controllers/admin
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2022-07-05 02:41:40 +0200
committerGitHub <noreply@github.com>2022-07-05 02:41:40 +0200
commit44b2ee3485ba0845e5910cefcb4b1e2f84f34470 (patch)
treecc91189c9b36aaf0a04d339455c6d238992753a9 /spec/controllers/admin
parent1b4054256f9d3302b44f71627a23bb0902578867 (diff)
Add customizable user roles (#18641)
* Add customizable user roles

* Various fixes and improvements

* Add migration for old settings and fix tootctl role management
Diffstat (limited to 'spec/controllers/admin')
-rw-r--r--spec/controllers/admin/account_moderation_notes_controller_spec.rb2
-rw-r--r--spec/controllers/admin/accounts_controller_spec.rb52
-rw-r--r--spec/controllers/admin/action_logs_controller_spec.rb2
-rw-r--r--spec/controllers/admin/base_controller_spec.rb7
-rw-r--r--spec/controllers/admin/change_email_controller_spec.rb2
-rw-r--r--spec/controllers/admin/confirmations_controller_spec.rb2
-rw-r--r--spec/controllers/admin/custom_emojis_controller_spec.rb2
-rw-r--r--spec/controllers/admin/dashboard_controller_spec.rb2
-rw-r--r--spec/controllers/admin/disputes/appeals_controller_spec.rb4
-rw-r--r--spec/controllers/admin/domain_blocks_controller_spec.rb2
-rw-r--r--spec/controllers/admin/email_domain_blocks_controller_spec.rb2
-rw-r--r--spec/controllers/admin/instances_controller_spec.rb8
-rw-r--r--spec/controllers/admin/invites_controller_spec.rb2
-rw-r--r--spec/controllers/admin/report_notes_controller_spec.rb2
-rw-r--r--spec/controllers/admin/reports_controller_spec.rb2
-rw-r--r--spec/controllers/admin/resets_controller_spec.rb2
-rw-r--r--spec/controllers/admin/roles_controller_spec.rb244
-rw-r--r--spec/controllers/admin/settings_controller_spec.rb2
-rw-r--r--spec/controllers/admin/statuses_controller_spec.rb2
-rw-r--r--spec/controllers/admin/tags_controller_spec.rb2
-rw-r--r--spec/controllers/admin/users/roles_controller.rb81
-rw-r--r--spec/controllers/admin/users/two_factor_authentications_controller_spec.rb (renamed from spec/controllers/admin/two_factor_authentications_controller_spec.rb)5
22 files changed, 367 insertions, 64 deletions
diff --git a/spec/controllers/admin/account_moderation_notes_controller_spec.rb b/spec/controllers/admin/account_moderation_notes_controller_spec.rb
index 410ce6543..d3f3263f8 100644
--- a/spec/controllers/admin/account_moderation_notes_controller_spec.rb
+++ b/spec/controllers/admin/account_moderation_notes_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
 RSpec.describe Admin::AccountModerationNotesController, type: :controller do
   render_views
 
-  let(:user) { Fabricate(:user, admin: true) }
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
   let(:target_account) { Fabricate(:account) }
 
   before do
diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 1779fb7c0..1bd51a0c8 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
   before { sign_in current_user, scope: :user }
 
   describe 'GET #index' do
-    let(:current_user) { Fabricate(:user, admin: true) }
+    let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
 
     around do |example|
       default_per_page = Account.default_per_page
@@ -60,7 +60,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
   end
 
   describe 'GET #show' do
-    let(:current_user) { Fabricate(:user, admin: true) }
+    let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
     let(:account) { Fabricate(:account) }
 
     it 'returns http success' do
@@ -72,15 +72,15 @@ RSpec.describe Admin::AccountsController, type: :controller do
   describe 'POST #memorialize' do
     subject { post :memorialize, params: { id: account.id } }
 
-    let(:current_user) { Fabricate(:user, admin: current_user_admin) }
+    let(:current_user) { Fabricate(:user, role: current_role) }
     let(:account) { user.account }
-    let(:user) { Fabricate(:user, admin: target_user_admin) }
+    let(:user) { Fabricate(:user, role: target_role) }
 
     context 'when user is admin' do
-      let(:current_user_admin) { true }
+      let(:current_role) { UserRole.find_by(name: 'Admin') }
 
       context 'when target user is admin' do
-        let(:target_user_admin) { true }
+        let(:target_role) { UserRole.find_by(name: 'Admin') }
 
         it 'fails to memorialize account' do
           is_expected.to have_http_status :forbidden
@@ -89,7 +89,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
       end
 
       context 'when target user is not admin' do
-        let(:target_user_admin) { false }
+        let(:target_role) { UserRole.find_by(name: 'Moderator') }
 
         it 'succeeds in memorializing account' do
           is_expected.to redirect_to admin_account_path(account.id)
@@ -99,10 +99,10 @@ RSpec.describe Admin::AccountsController, type: :controller do
     end
 
     context 'when user is not admin' do
-      let(:current_user_admin) { false }
+      let(:current_role) { UserRole.find_by(name: 'Moderator') }
 
       context 'when target user is admin' do
-        let(:target_user_admin) { true }
+        let(:target_role) { UserRole.find_by(name: 'Admin') }
 
         it 'fails to memorialize account' do
           is_expected.to have_http_status :forbidden
@@ -111,7 +111,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
       end
 
       context 'when target user is not admin' do
-        let(:target_user_admin) { false }
+        let(:target_role) { UserRole.find_by(name: 'Moderator') }
 
         it 'fails to memorialize account' do
           is_expected.to have_http_status :forbidden
@@ -124,12 +124,12 @@ RSpec.describe Admin::AccountsController, type: :controller do
   describe 'POST #enable' do
     subject { post :enable, params: { id: account.id } }
 
-    let(:current_user) { Fabricate(:user, admin: admin) }
+    let(:current_user) { Fabricate(:user, role: role) }
     let(:account) { user.account }
     let(:user) { Fabricate(:user, disabled: true) }
 
     context 'when user is admin' do
-      let(:admin) { true }
+      let(:role) { UserRole.find_by(name: 'Admin') }
 
       it 'succeeds in enabling account' do
         is_expected.to redirect_to admin_account_path(account.id)
@@ -138,7 +138,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
     end
 
     context 'when user is not admin' do
-      let(:admin) { false }
+      let(:role) { UserRole.everyone }
 
       it 'fails to enable account' do
         is_expected.to have_http_status :forbidden
@@ -150,19 +150,23 @@ RSpec.describe Admin::AccountsController, type: :controller do
   describe 'POST #redownload' do
     subject { post :redownload, params: { id: account.id } }
 
-    let(:current_user) { Fabricate(:user, admin: admin) }
-    let(:account) { Fabricate(:account) }
+    let(:current_user) { Fabricate(:user, role: role) }
+    let(:account) { Fabricate(:account, domain: 'example.com') }
+
+    before do
+      allow_any_instance_of(ResolveAccountService).to receive(:call)
+    end
 
     context 'when user is admin' do
-      let(:admin) { true }
+      let(:role) { UserRole.find_by(name: 'Admin') }
 
-      it 'succeeds in redownloadin' do
+      it 'succeeds in redownloading' do
         is_expected.to redirect_to admin_account_path(account.id)
       end
     end
 
     context 'when user is not admin' do
-      let(:admin) { false }
+      let(:role) { UserRole.everyone }
 
       it 'fails to redownload' do
         is_expected.to have_http_status :forbidden
@@ -173,11 +177,11 @@ RSpec.describe Admin::AccountsController, type: :controller do
   describe 'POST #remove_avatar' do
     subject { post :remove_avatar, params: { id: account.id } }
 
-    let(:current_user) { Fabricate(:user, admin: admin) }
+    let(:current_user) { Fabricate(:user, role: role) }
     let(:account) { Fabricate(:account) }
 
     context 'when user is admin' do
-      let(:admin) { true }
+      let(:role) { UserRole.find_by(name: 'Admin') }
 
       it 'succeeds in removing avatar' do
         is_expected.to redirect_to admin_account_path(account.id)
@@ -185,7 +189,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
     end
 
     context 'when user is not admin' do
-      let(:admin) { false }
+      let(:role) { UserRole.everyone }
 
       it 'fails to remove avatar' do
         is_expected.to have_http_status :forbidden
@@ -196,12 +200,12 @@ RSpec.describe Admin::AccountsController, type: :controller do
   describe 'POST #unblock_email' do
     subject { post :unblock_email, params: { id: account.id } }
 
-    let(:current_user) { Fabricate(:user, admin: admin) }
+    let(:current_user) { Fabricate(:user, role: role) }
     let(:account) { Fabricate(:account, suspended: true) }
     let!(:email_block) { Fabricate(:canonical_email_block, reference_account: account) }
 
     context 'when user is admin' do
-      let(:admin) { true }
+      let(:role) { UserRole.find_by(name: 'Admin') }
 
       it 'succeeds in removing email blocks' do
         expect { subject }.to change { CanonicalEmailBlock.where(reference_account: account).count }.from(1).to(0)
@@ -214,7 +218,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
     end
 
     context 'when user is not admin' do
-      let(:admin) { false }
+      let(:role) { UserRole.everyone }
 
       it 'fails to remove avatar' do
         subject
diff --git a/spec/controllers/admin/action_logs_controller_spec.rb b/spec/controllers/admin/action_logs_controller_spec.rb
index 4720ed2e2..c1957258f 100644
--- a/spec/controllers/admin/action_logs_controller_spec.rb
+++ b/spec/controllers/admin/action_logs_controller_spec.rb
@@ -5,7 +5,7 @@ require 'rails_helper'
 describe Admin::ActionLogsController, type: :controller do
   describe 'GET #index' do
     it 'returns 200' do
-      sign_in Fabricate(:user, admin: true)
+      sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin'))
       get :index, params: { page: 1 }
 
       expect(response).to have_http_status(200)
diff --git a/spec/controllers/admin/base_controller_spec.rb b/spec/controllers/admin/base_controller_spec.rb
index 9ac833623..44be91951 100644
--- a/spec/controllers/admin/base_controller_spec.rb
+++ b/spec/controllers/admin/base_controller_spec.rb
@@ -5,13 +5,14 @@ require 'rails_helper'
 describe Admin::BaseController, type: :controller do
   controller do
     def success
+      authorize :dashboard, :index?
       render 'admin/reports/show'
     end
   end
 
   it 'requires administrator or moderator' do
     routes.draw { get 'success' => 'admin/base#success' }
-    sign_in(Fabricate(:user, admin: false, moderator: false))
+    sign_in(Fabricate(:user))
     get :success
 
     expect(response).to have_http_status(:forbidden)
@@ -19,14 +20,14 @@ describe Admin::BaseController, type: :controller do
 
   it 'renders admin layout as a moderator' do
     routes.draw { get 'success' => 'admin/base#success' }
-    sign_in(Fabricate(:user, moderator: true))
+    sign_in(Fabricate(:user, role: UserRole.find_by(name: 'Moderator')))
     get :success
     expect(response).to render_template layout: 'admin'
   end
 
   it 'renders admin layout as an admin' do
     routes.draw { get 'success' => 'admin/base#success' }
-    sign_in(Fabricate(:user, admin: true))
+    sign_in(Fabricate(:user, role: UserRole.find_by(name: 'Admin')))
     get :success
     expect(response).to render_template layout: 'admin'
   end
diff --git a/spec/controllers/admin/change_email_controller_spec.rb b/spec/controllers/admin/change_email_controller_spec.rb
index e7f3f7c97..cf8a27d39 100644
--- a/spec/controllers/admin/change_email_controller_spec.rb
+++ b/spec/controllers/admin/change_email_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
 RSpec.describe Admin::ChangeEmailsController, type: :controller do
   render_views
 
-  let(:admin) { Fabricate(:user, admin: true) }
+  let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
 
   before do
     sign_in admin
diff --git a/spec/controllers/admin/confirmations_controller_spec.rb b/spec/controllers/admin/confirmations_controller_spec.rb
index 5b4f7e925..6268903c4 100644
--- a/spec/controllers/admin/confirmations_controller_spec.rb
+++ b/spec/controllers/admin/confirmations_controller_spec.rb
@@ -4,7 +4,7 @@ RSpec.describe Admin::ConfirmationsController, type: :controller do
   render_views
 
   before do
-    sign_in Fabricate(:user, admin: true), scope: :user
+    sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
   end
 
   describe 'POST #create' do
diff --git a/spec/controllers/admin/custom_emojis_controller_spec.rb b/spec/controllers/admin/custom_emojis_controller_spec.rb
index a8d96948c..06cd0c22d 100644
--- a/spec/controllers/admin/custom_emojis_controller_spec.rb
+++ b/spec/controllers/admin/custom_emojis_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
 describe Admin::CustomEmojisController do
   render_views
 
-  let(:user) { Fabricate(:user, admin: true) }
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
 
   before do
     sign_in user, scope: :user
diff --git a/spec/controllers/admin/dashboard_controller_spec.rb b/spec/controllers/admin/dashboard_controller_spec.rb
index 7824854f9..6231a09a2 100644
--- a/spec/controllers/admin/dashboard_controller_spec.rb
+++ b/spec/controllers/admin/dashboard_controller_spec.rb
@@ -12,7 +12,7 @@ describe Admin::DashboardController, type: :controller do
         Admin::SystemCheck::Message.new(:rules_check, nil, admin_rules_path),
         Admin::SystemCheck::Message.new(:sidekiq_process_check, 'foo, bar'),
       ])
-      sign_in Fabricate(:user, admin: true)
+      sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin'))
     end
 
     it 'returns 200' do
diff --git a/spec/controllers/admin/disputes/appeals_controller_spec.rb b/spec/controllers/admin/disputes/appeals_controller_spec.rb
index 6a06f9406..712657791 100644
--- a/spec/controllers/admin/disputes/appeals_controller_spec.rb
+++ b/spec/controllers/admin/disputes/appeals_controller_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Admin::Disputes::AppealsController, type: :controller do
   end
 
   describe 'POST #approve' do
-    let(:current_user) { Fabricate(:user, admin: true) }
+    let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
 
     before do
       allow(UserMailer).to receive(:appeal_approved).and_return(double('email', deliver_later: nil))
@@ -35,7 +35,7 @@ RSpec.describe Admin::Disputes::AppealsController, type: :controller do
   end
 
   describe 'POST #reject' do
-    let(:current_user) { Fabricate(:user, admin: true) }
+    let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
 
     before do
       allow(UserMailer).to receive(:appeal_rejected).and_return(double('email', deliver_later: nil))
diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb
index ecc79292b..5c2dcd268 100644
--- a/spec/controllers/admin/domain_blocks_controller_spec.rb
+++ b/spec/controllers/admin/domain_blocks_controller_spec.rb
@@ -4,7 +4,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
   render_views
 
   before do
-    sign_in Fabricate(:user, admin: true), scope: :user
+    sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
   end
 
   describe 'GET #new' do
diff --git a/spec/controllers/admin/email_domain_blocks_controller_spec.rb b/spec/controllers/admin/email_domain_blocks_controller_spec.rb
index cf194579d..e9cef4a94 100644
--- a/spec/controllers/admin/email_domain_blocks_controller_spec.rb
+++ b/spec/controllers/admin/email_domain_blocks_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Admin::EmailDomainBlocksController, type: :controller do
   render_views
 
   before do
-    sign_in Fabricate(:user, admin: true), scope: :user
+    sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
   end
 
   describe 'GET #index' do
diff --git a/spec/controllers/admin/instances_controller_spec.rb b/spec/controllers/admin/instances_controller_spec.rb
index 53427b874..337f7a80c 100644
--- a/spec/controllers/admin/instances_controller_spec.rb
+++ b/spec/controllers/admin/instances_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
 RSpec.describe Admin::InstancesController, type: :controller do
   render_views
 
-  let(:current_user) { Fabricate(:user, admin: true) }
+  let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
 
   let!(:account)     { Fabricate(:account, domain: 'popular') }
   let!(:account2)    { Fabricate(:account, domain: 'popular') }
@@ -35,11 +35,11 @@ RSpec.describe Admin::InstancesController, type: :controller do
   describe 'DELETE #destroy' do
     subject { delete :destroy, params: { id: Instance.first.id } }
 
-    let(:current_user) { Fabricate(:user, admin: admin) }
+    let(:current_user) { Fabricate(:user, role: role) }
     let(:account) { Fabricate(:account) }
 
     context 'when user is admin' do
-      let(:admin) { true }
+      let(:role) { UserRole.find_by(name: 'Admin') }
 
       it 'succeeds in purging instance' do
         is_expected.to redirect_to admin_instances_path
@@ -47,7 +47,7 @@ RSpec.describe Admin::InstancesController, type: :controller do
     end
 
     context 'when user is not admin' do
-      let(:admin) { false }
+      let(:role) { nil }
 
       it 'fails to purge instance' do
         is_expected.to have_http_status :forbidden
diff --git a/spec/controllers/admin/invites_controller_spec.rb b/spec/controllers/admin/invites_controller_spec.rb
index 449a699e4..1fb488742 100644
--- a/spec/controllers/admin/invites_controller_spec.rb
+++ b/spec/controllers/admin/invites_controller_spec.rb
@@ -5,7 +5,7 @@ require 'rails_helper'
 describe Admin::InvitesController do
   render_views
 
-  let(:user) { Fabricate(:user, admin: true) }
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
 
   before do
     sign_in user, scope: :user
diff --git a/spec/controllers/admin/report_notes_controller_spec.rb b/spec/controllers/admin/report_notes_controller_spec.rb
index c0013f41a..fa7572d18 100644
--- a/spec/controllers/admin/report_notes_controller_spec.rb
+++ b/spec/controllers/admin/report_notes_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
 describe Admin::ReportNotesController do
   render_views
 
-  let(:user) { Fabricate(:user, admin: true) }
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
 
   before do
     sign_in user, scope: :user
diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb
index d421f0739..4cd1524bf 100644
--- a/spec/controllers/admin/reports_controller_spec.rb
+++ b/spec/controllers/admin/reports_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
 describe Admin::ReportsController do
   render_views
 
-  let(:user) { Fabricate(:user, admin: true) }
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
   before do
     sign_in user, scope: :user
   end
diff --git a/spec/controllers/admin/resets_controller_spec.rb b/spec/controllers/admin/resets_controller_spec.rb
index 28510b5af..aeb172318 100644
--- a/spec/controllers/admin/resets_controller_spec.rb
+++ b/spec/controllers/admin/resets_controller_spec.rb
@@ -5,7 +5,7 @@ describe Admin::ResetsController do
 
   let(:account) { Fabricate(:account) }
   before do
-    sign_in Fabricate(:user, admin: true), scope: :user
+    sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
   end
 
   describe 'POST #create' do
diff --git a/spec/controllers/admin/roles_controller_spec.rb b/spec/controllers/admin/roles_controller_spec.rb
index 8e0de73cb..8ff891205 100644
--- a/spec/controllers/admin/roles_controller_spec.rb
+++ b/spec/controllers/admin/roles_controller_spec.rb
@@ -3,31 +3,247 @@ require 'rails_helper'
 describe Admin::RolesController do
   render_views
 
-  let(:admin) { Fabricate(:user, admin: true) }
+  let(:permissions)  { UserRole::Flags::NONE }
+  let(:current_role) { UserRole.create(name: 'Foo', permissions: permissions, position: 10) }
+  let(:current_user) { Fabricate(:user, role: current_role) }
 
   before do
-    sign_in admin, scope: :user
+    sign_in current_user, scope: :user
   end
 
-  describe 'POST #promote' do
-    subject { post :promote, params: { account_id: user.account_id } }
+  describe 'GET #index' do
+    before do
+      get :index
+    end
+
+    context 'when user does not have permission to manage roles' do
+      it 'returns http forbidden' do
+        expect(response).to have_http_status(:forbidden)
+      end
+    end
 
-    let(:user) { Fabricate(:user, moderator: false, admin: false) }
+    context 'when user has permission to manage roles' do
+      let(:permissions) { UserRole::FLAGS[:manage_roles] }
 
-    it 'promotes user' do
-      expect(subject).to redirect_to admin_account_path(user.account_id)
-      expect(user.reload).to be_moderator
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
     end
   end
 
-  describe 'POST #demote' do
-    subject { post :demote, params: { account_id: user.account_id } }
+  describe 'GET #new' do
+    before do
+      get :new
+    end
+
+    context 'when user does not have permission to manage roles' do
+      it 'returns http forbidden' do
+        expect(response).to have_http_status(:forbidden)
+      end
+    end
+
+    context 'when user has permission to manage roles' do
+      let(:permissions) { UserRole::FLAGS[:manage_roles] }
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+    end
+  end
+
+  describe 'POST #create' do
+    let(:selected_position) { 1 }
+    let(:selected_permissions_as_keys) { %w(manage_roles) }
+
+    before do
+      post :create, params: { user_role: { name: 'Bar', position: selected_position, permissions_as_keys: selected_permissions_as_keys } }
+    end
+
+    context 'when user has permission to manage roles' do
+      let(:permissions) { UserRole::FLAGS[:manage_roles] }
+
+      context 'when new role\'s does not elevate above the user\'s role' do
+        let(:selected_position) { 1 }
+        let(:selected_permissions_as_keys) { %w(manage_roles) }
+
+        it 'redirects to roles page' do
+          expect(response).to redirect_to(admin_roles_path)
+        end
+
+        it 'creates new role' do
+          expect(UserRole.find_by(name: 'Bar')).to_not be_nil
+        end
+      end
+
+      context 'when new role\'s position is higher than user\'s role' do
+        let(:selected_position) { 100 }
+        let(:selected_permissions_as_keys) { %w(manage_roles) }
+
+        it 'renders new template' do
+          expect(response).to render_template(:new)
+        end
+
+        it 'does not create new role' do
+          expect(UserRole.find_by(name: 'Bar')).to be_nil
+        end
+      end
+
+      context 'when new role has permissions the user does not have' do
+        let(:selected_position) { 1 }
+        let(:selected_permissions_as_keys) { %w(manage_roles manage_users manage_reports) }
+
+        it 'renders new template' do
+          expect(response).to render_template(:new)
+        end
+
+        it 'does not create new role' do
+          expect(UserRole.find_by(name: 'Bar')).to be_nil
+        end
+      end
+
+      context 'when user has administrator permission' do
+        let(:permissions) { UserRole::FLAGS[:administrator] }
+
+        let(:selected_position) { 1 }
+        let(:selected_permissions_as_keys) { %w(manage_roles manage_users manage_reports) }
+
+        it 'redirects to roles page' do
+          expect(response).to redirect_to(admin_roles_path)
+        end
+
+        it 'creates new role' do
+          expect(UserRole.find_by(name: 'Bar')).to_not be_nil
+        end
+      end
+    end
+  end
+
+  describe 'GET #edit' do
+    let(:role_position) { 8 }
+    let(:role) { UserRole.create(name: 'Bar', permissions: UserRole::FLAGS[:manage_users], position: role_position) }
+
+    before do
+      get :edit, params: { id: role.id }
+    end
+
+    context 'when user does not have permission to manage roles' do
+      it 'returns http forbidden' do
+        expect(response).to have_http_status(:forbidden)
+      end
+    end
+
+    context 'when user has permission to manage roles' do
+      let(:permissions) { UserRole::FLAGS[:manage_roles] }
+
+      context 'when user outranks the role' do
+        it 'returns http success' do
+          expect(response).to have_http_status(:success)
+        end
+      end
+
+      context 'when role outranks user' do
+        let(:role_position) { current_role.position + 1 }
+
+        it 'returns http forbidden' do
+          expect(response).to have_http_status(:forbidden)
+        end
+      end
+    end
+  end
+
+  describe 'PUT #update' do
+    let(:role_position) { 8 }
+    let(:role_permissions) { UserRole::FLAGS[:manage_users] }
+    let(:role) { UserRole.create(name: 'Bar', permissions: role_permissions, position: role_position) }
+
+    let(:selected_position) { 8 }
+    let(:selected_permissions_as_keys) { %w(manage_users) }
+
+    before do
+      put :update, params: { id: role.id, user_role: { name: 'Baz', position: selected_position, permissions_as_keys: selected_permissions_as_keys } }
+    end
+
+    context 'when user does not have permission to manage roles' do
+      it 'returns http forbidden' do
+        expect(response).to have_http_status(:forbidden)
+      end
+
+      it 'does not update the role' do
+        expect(role.reload.name).to eq 'Bar'
+      end
+    end
+
+    context 'when user has permission to manage roles' do
+      let(:permissions) { UserRole::FLAGS[:manage_roles] }
+
+      context 'when role has permissions the user doesn\'t' do
+        it 'renders edit template' do
+          expect(response).to render_template(:edit)
+        end
+
+        it 'does not update the role' do
+          expect(role.reload.name).to eq 'Bar'
+        end
+      end
+
+      context 'when user has all permissions of the role' do
+        let(:permissions) { UserRole::FLAGS[:manage_roles] | UserRole::FLAGS[:manage_users] }
+
+        context 'when user outranks the role' do
+          it 'redirects to roles page' do
+            expect(response).to redirect_to(admin_roles_path)
+          end
+
+          it 'updates the role' do
+            expect(role.reload.name).to eq 'Baz'
+          end
+        end
+
+        context 'when role outranks user' do
+          let(:role_position) { current_role.position + 1 }
+
+          it 'returns http forbidden' do
+            expect(response).to have_http_status(:forbidden)
+          end
+
+          it 'does not update the role' do
+            expect(role.reload.name).to eq 'Bar'
+          end
+        end
+      end
+    end
+  end
+
+  describe 'DELETE #destroy' do
+    let(:role_position) { 8 }
+    let(:role) { UserRole.create(name: 'Bar', permissions: UserRole::FLAGS[:manage_users], position: role_position) }
+
+    before do
+      delete :destroy, params: { id: role.id }
+    end
+
+    context 'when user does not have permission to manage roles' do
+      it 'returns http forbidden' do
+        expect(response).to have_http_status(:forbidden)
+      end
+    end
+
+    context 'when user has permission to manage roles' do
+      let(:permissions) { UserRole::FLAGS[:manage_roles] }
+
+      context 'when user outranks the role' do
+        it 'redirects to roles page' do
+          expect(response).to redirect_to(admin_roles_path)
+        end
+      end
 
-    let(:user) { Fabricate(:user, moderator: true, admin: false) }
+      context 'when role outranks user' do
+        let(:role_position) { current_role.position + 1 }
 
-    it 'demotes user' do
-      expect(subject).to redirect_to admin_account_path(user.account_id)
-      expect(user.reload).not_to be_moderator
+        it 'returns http forbidden' do
+          expect(response).to have_http_status(:forbidden)
+        end
+      end
     end
   end
 end
diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb
index 6cf0ee20a..46749f76c 100644
--- a/spec/controllers/admin/settings_controller_spec.rb
+++ b/spec/controllers/admin/settings_controller_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Admin::SettingsController, type: :controller do
 
   describe 'When signed in as an admin' do
     before do
-      sign_in Fabricate(:user, admin: true), scope: :user
+      sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
     end
 
     describe 'GET #edit' do
diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb
index de32fd18e..227688e23 100644
--- a/spec/controllers/admin/statuses_controller_spec.rb
+++ b/spec/controllers/admin/statuses_controller_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
 describe Admin::StatusesController do
   render_views
 
-  let(:user) { Fabricate(:user, admin: true) }
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
   let(:account) { Fabricate(:account) }
   let!(:status) { Fabricate(:status, account: account) }
   let(:media_attached_status) { Fabricate(:status, account: account, sensitive: !sensitive) }
diff --git a/spec/controllers/admin/tags_controller_spec.rb b/spec/controllers/admin/tags_controller_spec.rb
index 85c801a9c..52fd09eb1 100644
--- a/spec/controllers/admin/tags_controller_spec.rb
+++ b/spec/controllers/admin/tags_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Admin::TagsController, type: :controller do
   render_views
 
   before do
-    sign_in Fabricate(:user, admin: true)
+    sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin'))
   end
 
   describe 'GET #show' do
diff --git a/spec/controllers/admin/users/roles_controller.rb b/spec/controllers/admin/users/roles_controller.rb
new file mode 100644
index 000000000..bd6a3fa67
--- /dev/null
+++ b/spec/controllers/admin/users/roles_controller.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+describe Admin::Users::RolesController do
+  render_views
+
+  let(:current_role) { UserRole.create(name: 'Foo', permissions: UserRole::FLAGS[:manage_roles], position: 10) }
+  let(:current_user) { Fabricate(:user, role: current_role) }
+
+  let(:previous_role) { nil }
+  let(:user) { Fabricate(:user, role: previous_role) }
+
+  before do
+    sign_in current_user, scope: :user
+  end
+
+  describe 'GET #show' do
+    before do
+      get :show, params: { user_id: user.id }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+
+    context 'when target user is higher ranked than current user' do
+      let(:previous_role) { UserRole.create(name: 'Baz', permissions: UserRole::FLAGS[:administrator], position: 100) }
+
+      it 'returns http forbidden' do
+        expect(response).to have_http_status(:forbidden)
+      end
+    end
+  end
+
+  describe 'PUT #update' do
+    let(:selected_role) { UserRole.create(name: 'Bar', permissions: permissions, position: position) }
+
+    before do
+      put :update, params: { user_id: user.id, user: { role_id: selected_role.id } }
+    end
+
+    context do
+      let(:permissions) { UserRole::FLAGS[:manage_roles] }
+      let(:position) { 1 }
+
+      it 'updates user role' do
+        expect(user.reload.role_id).to eq selected_role&.id
+      end
+
+      it 'redirects back to account page' do
+        expect(response).to redirect_to(admin_account_path(user.account_id))
+      end
+    end
+
+    context 'when selected role has higher position than current user\'s role' do
+      let(:permissions) { UserRole::FLAGS[:administrator] }
+      let(:position) { 100 }
+
+      it 'does not update user role' do
+        expect(user.reload.role_id).to eq previous_role&.id
+      end
+
+      it 'renders edit form' do
+        expect(response).to render_template(:show)
+      end
+    end
+
+    context 'when target user is higher ranked than current user' do
+      let(:previous_role) { UserRole.create(name: 'Baz', permissions: UserRole::FLAGS[:administrator], position: 100) }
+      let(:permissions) { UserRole::FLAGS[:manage_roles] }
+      let(:position) { 1 }
+
+      it 'does not update user role' do
+        expect(user.reload.role_id).to eq previous_role&.id
+      end
+
+      it 'returns http forbidden' do
+        expect(response).to have_http_status(:forbidden)
+      end
+    end
+  end
+end
diff --git a/spec/controllers/admin/two_factor_authentications_controller_spec.rb b/spec/controllers/admin/users/two_factor_authentications_controller_spec.rb
index c65095729..e56264ef6 100644
--- a/spec/controllers/admin/two_factor_authentications_controller_spec.rb
+++ b/spec/controllers/admin/users/two_factor_authentications_controller_spec.rb
@@ -1,12 +1,13 @@
 require 'rails_helper'
 require 'webauthn/fake_client'
 
-describe Admin::TwoFactorAuthenticationsController do
+describe Admin::Users::TwoFactorAuthenticationsController do
   render_views
 
   let(:user) { Fabricate(:user) }
+
   before do
-    sign_in Fabricate(:user, admin: true), scope: :user
+    sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
   end
 
   describe 'DELETE #destroy' do