From 0c59ef44b1eff236c5354136fd950c9326cd2b9e Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 08:48:10 -0400
Subject: [PATCH 01/19] Extend spec coverage for `Poll` model (#32500)

---
 spec/models/poll_spec.rb | 26 +++++++++++++++++++-------
 1 file changed, 19 insertions(+), 7 deletions(-)

diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb
index 736f3615d0..66f521ab3f 100644
--- a/spec/models/poll_spec.rb
+++ b/spec/models/poll_spec.rb
@@ -3,7 +3,7 @@
 require 'rails_helper'
 
 RSpec.describe Poll do
-  describe 'scopes' do
+  describe 'Scopes' do
     let(:status) { Fabricate(:status) }
     let(:attached_poll) { Fabricate(:poll, status: status) }
     let(:not_attached_poll) do
@@ -13,7 +13,7 @@ RSpec.describe Poll do
       end
     end
 
-    describe 'attached' do
+    describe '.attached' do
       it 'finds the correct records' do
         results = described_class.attached
 
@@ -21,7 +21,7 @@ RSpec.describe Poll do
       end
     end
 
-    describe 'unattached' do
+    describe '.unattached' do
       it 'finds the correct records' do
         results = described_class.unattached
 
@@ -30,11 +30,23 @@ RSpec.describe Poll do
     end
   end
 
-  describe 'validations' do
-    context 'when not valid' do
-      subject { Fabricate.build(:poll) }
+  describe '#reset_votes!' do
+    let(:poll) { Fabricate :poll, cached_tallies: [2, 3], votes_count: 5, voters_count: 5 }
+    let!(:vote) { Fabricate :poll_vote, poll: }
 
-      it { is_expected.to validate_presence_of(:expires_at) }
+    it 'resets vote data and deletes votes' do
+      expect { poll.reset_votes! }
+        .to change(poll, :cached_tallies).to([0, 0])
+        .and change(poll, :votes_count).to(0)
+        .and(change(poll, :voters_count).to(0))
+      expect { vote.reload }
+        .to raise_error(ActiveRecord::RecordNotFound)
     end
   end
+
+  describe 'Validations' do
+    subject { Fabricate.build(:poll) }
+
+    it { is_expected.to validate_presence_of(:expires_at) }
+  end
 end

From 2d008108a4d068283bd37bcf701916ea54c06603 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 08:54:56 -0400
Subject: [PATCH 02/19] Reduce factory creation (132 -> 40) in lib/vacuum/*
 specs (#32498)

---
 spec/lib/vacuum/access_tokens_vacuum_spec.rb  | 34 +++++++------------
 spec/lib/vacuum/backups_vacuum_spec.rb        | 13 +++----
 spec/lib/vacuum/feeds_vacuum_spec.rb          |  4 +--
 .../vacuum/media_attachments_vacuum_spec.rb   |  4 +--
 spec/lib/vacuum/preview_cards_vacuum_spec.rb  | 22 ++++++------
 spec/lib/vacuum/statuses_vacuum_spec.rb       | 22 +++++-------
 6 files changed, 41 insertions(+), 58 deletions(-)

diff --git a/spec/lib/vacuum/access_tokens_vacuum_spec.rb b/spec/lib/vacuum/access_tokens_vacuum_spec.rb
index 54760c41bd..8768f6b2dc 100644
--- a/spec/lib/vacuum/access_tokens_vacuum_spec.rb
+++ b/spec/lib/vacuum/access_tokens_vacuum_spec.rb
@@ -14,32 +14,24 @@ RSpec.describe Vacuum::AccessTokensVacuum do
     let!(:expired_access_grant) { Fabricate(:access_grant, expires_in: 59.minutes.to_i, created_at: 1.hour.ago) }
     let!(:active_access_grant) { Fabricate(:access_grant) }
 
-    before do
+    it 'deletes revoked/expired access tokens and revoked/expired grants, but preserves active tokens/grants' do
       subject.perform
-    end
 
-    it 'deletes revoked access tokens' do
-      expect { revoked_access_token.reload }.to raise_error ActiveRecord::RecordNotFound
-    end
+      expect { revoked_access_token.reload }
+        .to raise_error ActiveRecord::RecordNotFound
+      expect { expired_access_token.reload }
+        .to raise_error ActiveRecord::RecordNotFound
 
-    it 'deletes expired access tokens' do
-      expect { expired_access_token.reload }.to raise_error ActiveRecord::RecordNotFound
-    end
+      expect { revoked_access_grant.reload }
+        .to raise_error ActiveRecord::RecordNotFound
+      expect { expired_access_grant.reload }
+        .to raise_error ActiveRecord::RecordNotFound
 
-    it 'deletes revoked access grants' do
-      expect { revoked_access_grant.reload }.to raise_error ActiveRecord::RecordNotFound
-    end
+      expect { active_access_token.reload }
+        .to_not raise_error
 
-    it 'deletes expired access grants' do
-      expect { expired_access_grant.reload }.to raise_error ActiveRecord::RecordNotFound
-    end
-
-    it 'does not delete active access tokens' do
-      expect { active_access_token.reload }.to_not raise_error
-    end
-
-    it 'does not delete active access grants' do
-      expect { active_access_grant.reload }.to_not raise_error
+      expect { active_access_grant.reload }
+        .to_not raise_error
     end
   end
 end
diff --git a/spec/lib/vacuum/backups_vacuum_spec.rb b/spec/lib/vacuum/backups_vacuum_spec.rb
index 867dbe4020..4a025352cb 100644
--- a/spec/lib/vacuum/backups_vacuum_spec.rb
+++ b/spec/lib/vacuum/backups_vacuum_spec.rb
@@ -11,16 +11,13 @@ RSpec.describe Vacuum::BackupsVacuum do
     let!(:expired_backup) { Fabricate(:backup, created_at: (retention_period + 1.day).ago) }
     let!(:current_backup) { Fabricate(:backup) }
 
-    before do
+    it 'deletes backups past the retention period but preserves those within the period' do
       subject.perform
-    end
 
-    it 'deletes backups past the retention period' do
-      expect { expired_backup.reload }.to raise_error ActiveRecord::RecordNotFound
-    end
-
-    it 'does not delete backups within the retention period' do
-      expect { current_backup.reload }.to_not raise_error
+      expect { expired_backup.reload }
+        .to raise_error ActiveRecord::RecordNotFound
+      expect { current_backup.reload }
+        .to_not raise_error
     end
   end
 end
diff --git a/spec/lib/vacuum/feeds_vacuum_spec.rb b/spec/lib/vacuum/feeds_vacuum_spec.rb
index ede1e3c360..38459a558f 100644
--- a/spec/lib/vacuum/feeds_vacuum_spec.rb
+++ b/spec/lib/vacuum/feeds_vacuum_spec.rb
@@ -14,11 +14,11 @@ RSpec.describe Vacuum::FeedsVacuum do
       redis.zadd(feed_key_for(active_user), 1, 1)
       redis.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2)
       redis.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3)
-
-      subject.perform
     end
 
     it 'clears feeds of inactive users and lists' do
+      subject.perform
+
       expect(redis.zcard(feed_key_for(inactive_user))).to eq 0
       expect(redis.zcard(feed_key_for(active_user))).to eq 1
       expect(redis.exists?(feed_key_for(inactive_user, 'reblogs'))).to be false
diff --git a/spec/lib/vacuum/media_attachments_vacuum_spec.rb b/spec/lib/vacuum/media_attachments_vacuum_spec.rb
index 1039c36cea..f7749038cb 100644
--- a/spec/lib/vacuum/media_attachments_vacuum_spec.rb
+++ b/spec/lib/vacuum/media_attachments_vacuum_spec.rb
@@ -17,9 +17,9 @@ RSpec.describe Vacuum::MediaAttachmentsVacuum do
     let!(:old_unattached_media) { Fabricate(:media_attachment, account_id: nil, created_at: 10.days.ago) }
     let!(:new_unattached_media) { Fabricate(:media_attachment, account_id: nil, created_at: 1.hour.ago) }
 
-    before { subject.perform }
-
     it 'handles attachments based on metadata details' do
+      subject.perform
+
       expect(old_remote_media.reload.file) # Remote and past retention period
         .to be_blank
       expect(old_local_media.reload.file) # Local and past retention
diff --git a/spec/lib/vacuum/preview_cards_vacuum_spec.rb b/spec/lib/vacuum/preview_cards_vacuum_spec.rb
index 9dbdf0bc2f..caeedd3269 100644
--- a/spec/lib/vacuum/preview_cards_vacuum_spec.rb
+++ b/spec/lib/vacuum/preview_cards_vacuum_spec.rb
@@ -15,24 +15,22 @@ RSpec.describe Vacuum::PreviewCardsVacuum do
     before do
       old_preview_card.statuses << Fabricate(:status)
       new_preview_card.statuses << Fabricate(:status)
+    end
 
+    it 'handles preview card cleanup' do
       subject.perform
-    end
 
-    it 'deletes cache of preview cards last updated before the retention period' do
-      expect(old_preview_card.reload.image).to be_blank
-    end
+      expect(old_preview_card.reload.image) # last updated before retention period
+        .to be_blank
 
-    it 'does not delete cache of preview cards last updated within the retention period' do
-      expect(new_preview_card.reload.image).to_not be_blank
-    end
+      expect(new_preview_card.reload.image) # last updated within the retention period
+        .to_not be_blank
 
-    it 'does not delete attached preview cards' do
-      expect(new_preview_card.reload).to be_persisted
-    end
+      expect(new_preview_card.reload) # Keep attached preview cards
+        .to be_persisted
 
-    it 'does not delete orphaned preview cards in the retention period' do
-      expect(orphaned_preview_card.reload).to be_persisted
+      expect(orphaned_preview_card.reload) # keep orphaned cards in the retention period
+        .to be_persisted
     end
   end
 end
diff --git a/spec/lib/vacuum/statuses_vacuum_spec.rb b/spec/lib/vacuum/statuses_vacuum_spec.rb
index d5c0139506..1fff864879 100644
--- a/spec/lib/vacuum/statuses_vacuum_spec.rb
+++ b/spec/lib/vacuum/statuses_vacuum_spec.rb
@@ -15,24 +15,20 @@ RSpec.describe Vacuum::StatusesVacuum do
     let!(:local_status_old) { Fabricate(:status, created_at: (retention_period + 2.days).ago) }
     let!(:local_status_recent) { Fabricate(:status, created_at: (retention_period - 2.days).ago) }
 
-    before do
+    it 'deletes remote statuses past the retention period and keeps others' do
       subject.perform
-    end
 
-    it 'deletes remote statuses past the retention period' do
-      expect { remote_status_old.reload }.to raise_error ActiveRecord::RecordNotFound
-    end
+      expect { remote_status_old.reload }
+        .to raise_error ActiveRecord::RecordNotFound
 
-    it 'does not delete local statuses past the retention period' do
-      expect { local_status_old.reload }.to_not raise_error
-    end
+      expect { local_status_old.reload }
+        .to_not raise_error
 
-    it 'does not delete remote statuses within the retention period' do
-      expect { remote_status_recent.reload }.to_not raise_error
-    end
+      expect { remote_status_recent.reload }
+        .to_not raise_error
 
-    it 'does not delete local statuses within the retention period' do
-      expect { local_status_recent.reload }.to_not raise_error
+      expect { local_status_recent.reload }
+        .to_not raise_error
     end
   end
 end

From c292ed07fe986d309ba3cf8a70e71d28ed5a3d1f Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 09:09:25 -0400
Subject: [PATCH 03/19] Expand coverage for `Scheduler::IpCleanupScheduler`
 worker (#32499)

---
 spec/fabricators/ip_block_fabricator.rb       |  6 +++
 .../scheduler/ip_cleanup_scheduler_spec.rb    | 47 +++++++++++++++++--
 2 files changed, 50 insertions(+), 3 deletions(-)
 create mode 100644 spec/fabricators/ip_block_fabricator.rb

diff --git a/spec/fabricators/ip_block_fabricator.rb b/spec/fabricators/ip_block_fabricator.rb
new file mode 100644
index 0000000000..30c48b90c6
--- /dev/null
+++ b/spec/fabricators/ip_block_fabricator.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+Fabricator(:ip_block) do
+  severity { :sign_up_requires_approval }
+  ip { sequence(:ip) { |n| "10.0.0.#{n}" } }
+end
diff --git a/spec/workers/scheduler/ip_cleanup_scheduler_spec.rb b/spec/workers/scheduler/ip_cleanup_scheduler_spec.rb
index 7071fa6e98..98150aa5ef 100644
--- a/spec/workers/scheduler/ip_cleanup_scheduler_spec.rb
+++ b/spec/workers/scheduler/ip_cleanup_scheduler_spec.rb
@@ -5,9 +5,50 @@ require 'rails_helper'
 RSpec.describe Scheduler::IpCleanupScheduler do
   let(:worker) { described_class.new }
 
-  describe 'perform' do
-    it 'runs without error' do
-      expect { worker.perform }.to_not raise_error
+  describe '#perform' do
+    context 'with IP-related data past retention times' do
+      let!(:future_ip_block) { Fabricate :ip_block, expires_at: 1.week.from_now }
+      let!(:old_ip_block) { Fabricate :ip_block, expires_at: 1.week.ago }
+      let!(:session_past_retention) { Fabricate :session_activation, ip: '10.0.0.0', updated_at: 18.months.ago }
+      let!(:inactive_user) { Fabricate :user, current_sign_in_at: 18.months.ago, sign_up_ip: '10.0.0.0' }
+      let!(:old_login_activity) { Fabricate :login_activity, created_at: 18.months.ago }
+      let!(:old_token) { Fabricate :access_token, last_used_at: 18.months.ago, last_used_ip: '10.0.0.0' }
+
+      before { stub_const 'Scheduler::IpCleanupScheduler::SESSION_RETENTION_PERIOD', 10.years.to_i.seconds }
+
+      it 'deletes the expired block' do
+        expect { worker.perform }
+          .to_not raise_error
+        expect { old_ip_block.reload }
+          .to raise_error(ActiveRecord::RecordNotFound)
+        expect { old_login_activity.reload }
+          .to raise_error(ActiveRecord::RecordNotFound)
+        expect(session_past_retention.reload.ip)
+          .to be_nil
+        expect(inactive_user.reload.sign_up_ip)
+          .to be_nil
+        expect(old_token.reload.last_used_ip)
+          .to be_nil
+        expect(future_ip_block.reload)
+          .to be_present
+      end
+    end
+
+    context 'with old session data' do
+      let!(:new_activation) { Fabricate :session_activation, updated_at: 1.week.ago }
+      let!(:old_activation) { Fabricate :session_activation, updated_at: 1.month.ago }
+
+      before { stub_const 'Scheduler::IpCleanupScheduler::SESSION_RETENTION_PERIOD', 10.days.to_i.seconds }
+
+      it 'clears old sessions' do
+        expect { worker.perform }
+          .to_not raise_error
+
+        expect { old_activation.reload }
+          .to raise_error(ActiveRecord::RecordNotFound)
+        expect(new_activation.reload)
+          .to be_present
+      end
     end
   end
 end

From a72819660aa749388f36a90a9dff2889815b68d7 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 09:10:03 -0400
Subject: [PATCH 04/19] Reduce factory creation (48 -> 8) in `AP::Note`
 serializer spec (#32492)

---
 .../activitypub/note_serializer_spec.rb       | 31 +++++++++----------
 1 file changed, 14 insertions(+), 17 deletions(-)

diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb
index 285b241ee2..a6976193b2 100644
--- a/spec/serializers/activitypub/note_serializer_spec.rb
+++ b/spec/serializers/activitypub/note_serializer_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe ActivityPub::NoteSerializer do
   let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
   let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) }
 
-  it 'has the expected shape' do
+  it 'has the expected shape and replies collection' do
     expect(subject).to include({
       '@context' => include('https://www.w3.org/ns/activitystreams'),
       'type' => 'Note',
@@ -22,26 +22,23 @@ RSpec.describe ActivityPub::NoteSerializer do
       'contentMap' => include({
         'zh-TW' => a_kind_of(String),
       }),
+      'replies' => replies_collection_values,
     })
   end
 
-  it 'has a replies collection' do
-    expect(subject['replies']['type']).to eql('Collection')
+  def replies_collection_values
+    include(
+      'type' => eql('Collection'),
+      'first' => include(
+        'type' => eql('CollectionPage'),
+        'items' => reply_items
+      )
+    )
   end
 
-  it 'has a replies collection with a first Page' do
-    expect(subject['replies']['first']['type']).to eql('CollectionPage')
-  end
-
-  it 'includes public self-replies in its replies collection' do
-    expect(subject['replies']['first']['items']).to include(reply_by_account_first.uri, reply_by_account_next.uri, reply_by_account_third.uri)
-  end
-
-  it 'does not include replies from others in its replies collection' do
-    expect(subject['replies']['first']['items']).to_not include(reply_by_other_first.uri)
-  end
-
-  it 'does not include replies with direct visibility in its replies collection' do
-    expect(subject['replies']['first']['items']).to_not include(reply_by_account_visibility_direct.uri)
+  def reply_items
+    include(reply_by_account_first.uri, reply_by_account_next.uri, reply_by_account_third.uri) # Public self replies
+      .and(not_include(reply_by_other_first.uri)) # Replies from others
+      .and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility
   end
 end

From fbe55a454550a9b78b45a119c43a4c68c88d1ad8 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 09:10:29 -0400
Subject: [PATCH 05/19] Reduce factory creation (73 -> 64) in `PublicFeed` spec
 (#32491)

---
 spec/models/public_feed_spec.rb | 20 ++++++++------------
 1 file changed, 8 insertions(+), 12 deletions(-)

diff --git a/spec/models/public_feed_spec.rb b/spec/models/public_feed_spec.rb
index 20fcdb0024..5ea58cd16f 100644
--- a/spec/models/public_feed_spec.rb
+++ b/spec/models/public_feed_spec.rb
@@ -54,24 +54,20 @@ RSpec.describe PublicFeed do
       context 'without a viewer' do
         let(:viewer) { nil }
 
-        it 'includes remote instances statuses' do
-          expect(subject).to include(remote_status.id)
-        end
-
-        it 'includes local statuses' do
-          expect(subject).to include(local_status.id)
+        it 'includes remote instances statuses and local statuses' do
+          expect(subject)
+            .to include(remote_status.id)
+            .and include(local_status.id)
         end
       end
 
       context 'with a viewer' do
         let(:viewer) { Fabricate(:account, username: 'viewer') }
 
-        it 'includes remote instances statuses' do
-          expect(subject).to include(remote_status.id)
-        end
-
-        it 'includes local statuses' do
-          expect(subject).to include(local_status.id)
+        it 'includes remote instances statuses and local statuses' do
+          expect(subject)
+            .to include(remote_status.id)
+            .and include(local_status.id)
         end
       end
     end

From ff1247ad162ced0ec50d481e5280af7efb70ac6c Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 09:12:58 -0400
Subject: [PATCH 06/19] Use `context` for repeated scenarios in
 `AccountStatusCleanupPolicy` spec (#32489)

---
 .../account_statuses_cleanup_policy_spec.rb   | 82 ++++++++++---------
 1 file changed, 42 insertions(+), 40 deletions(-)

diff --git a/spec/models/account_statuses_cleanup_policy_spec.rb b/spec/models/account_statuses_cleanup_policy_spec.rb
index a08fd723a4..c142a0359a 100644
--- a/spec/models/account_statuses_cleanup_policy_spec.rb
+++ b/spec/models/account_statuses_cleanup_policy_spec.rb
@@ -16,6 +16,8 @@ RSpec.describe AccountStatusesCleanupPolicy do
 
   describe 'save hooks' do
     context 'when widening a policy' do
+      subject { account_statuses_cleanup_policy.last_inspected }
+
       let!(:account_statuses_cleanup_policy) do
         Fabricate(:account_statuses_cleanup_policy,
                   account: account,
@@ -33,64 +35,64 @@ RSpec.describe AccountStatusesCleanupPolicy do
         account_statuses_cleanup_policy.record_last_inspected(42)
       end
 
-      it 'invalidates last_inspected when widened because of keep_direct' do
-        account_statuses_cleanup_policy.keep_direct = false
-        account_statuses_cleanup_policy.save
-        expect(account_statuses_cleanup_policy.last_inspected).to be_nil
+      context 'when widened because of keep_direct' do
+        before { account_statuses_cleanup_policy.update(keep_direct: false) }
+
+        it { is_expected.to be_nil }
       end
 
-      it 'invalidates last_inspected when widened because of keep_pinned' do
-        account_statuses_cleanup_policy.keep_pinned = false
-        account_statuses_cleanup_policy.save
-        expect(account_statuses_cleanup_policy.last_inspected).to be_nil
+      context 'when widened because of keep_pinned' do
+        before { account_statuses_cleanup_policy.update(keep_pinned: false) }
+
+        it { is_expected.to be_nil }
       end
 
-      it 'invalidates last_inspected when widened because of keep_polls' do
-        account_statuses_cleanup_policy.keep_polls = false
-        account_statuses_cleanup_policy.save
-        expect(account_statuses_cleanup_policy.last_inspected).to be_nil
+      context 'when widened because of keep_polls' do
+        before { account_statuses_cleanup_policy.update(keep_polls: false) }
+
+        it { is_expected.to be_nil }
       end
 
-      it 'invalidates last_inspected when widened because of keep_media' do
-        account_statuses_cleanup_policy.keep_media = false
-        account_statuses_cleanup_policy.save
-        expect(account_statuses_cleanup_policy.last_inspected).to be_nil
+      context 'when widened because of keep_media' do
+        before { account_statuses_cleanup_policy.update(keep_media: false) }
+
+        it { is_expected.to be_nil }
       end
 
-      it 'invalidates last_inspected when widened because of keep_self_fav' do
-        account_statuses_cleanup_policy.keep_self_fav = false
-        account_statuses_cleanup_policy.save
-        expect(account_statuses_cleanup_policy.last_inspected).to be_nil
+      context 'when widened because of keep_self_fav' do
+        before { account_statuses_cleanup_policy.update(keep_self_fav: false) }
+
+        it { is_expected.to be_nil }
       end
 
-      it 'invalidates last_inspected when widened because of keep_self_bookmark' do
-        account_statuses_cleanup_policy.keep_self_bookmark = false
-        account_statuses_cleanup_policy.save
-        expect(account_statuses_cleanup_policy.last_inspected).to be_nil
+      context 'when widened because of keep_self_bookmark' do
+        before { account_statuses_cleanup_policy.update(keep_self_bookmark: false) }
+
+        it { is_expected.to be_nil }
       end
 
-      it 'invalidates last_inspected when widened because of higher min_favs' do
-        account_statuses_cleanup_policy.min_favs = 5
-        account_statuses_cleanup_policy.save
-        expect(account_statuses_cleanup_policy.last_inspected).to be_nil
+      context 'when widened because of higher min_favs' do
+        before { account_statuses_cleanup_policy.update(min_favs: 5) }
+
+        it { is_expected.to be_nil }
       end
 
-      it 'invalidates last_inspected when widened because of disabled min_favs' do
-        account_statuses_cleanup_policy.min_favs = nil
-        account_statuses_cleanup_policy.save
-        expect(account_statuses_cleanup_policy.last_inspected).to be_nil
+      context 'when widened because of disabled min_favs' do
+        before { account_statuses_cleanup_policy.update(min_favs: nil) }
+
+        it { is_expected.to be_nil }
       end
 
-      it 'invalidates last_inspected when widened because of higher min_reblogs' do
-        account_statuses_cleanup_policy.min_reblogs = 5
-        account_statuses_cleanup_policy.save
-        expect(account_statuses_cleanup_policy.last_inspected).to be_nil
+      context 'when widened because of higher min_reblogs' do
+        before { account_statuses_cleanup_policy.update(min_reblogs: 5) }
+
+        it { is_expected.to be_nil }
       end
 
-      it 'invalidates last_inspected when widened because of disable min_reblogs' do
-        account_statuses_cleanup_policy.min_reblogs = nil
-        account_statuses_cleanup_policy.save
-        expect(account_statuses_cleanup_policy.last_inspected).to be_nil
+      context 'when widened because of disable min_reblogs' do
+        before { account_statuses_cleanup_policy.update(min_reblogs: nil) }
+
+        it { is_expected.to be_nil }
       end
     end
 

From dc2f9eef7726b7d507bb6dbd7cb6c37c7f5f945f Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 09:18:57 -0400
Subject: [PATCH 07/19] Reduce factories (36 > 12) in `AccountReachFinder` spec
 (#32482)

---
 spec/lib/account_reach_finder_spec.rb | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/spec/lib/account_reach_finder_spec.rb b/spec/lib/account_reach_finder_spec.rb
index e5d85656a2..0c1d92b2da 100644
--- a/spec/lib/account_reach_finder_spec.rb
+++ b/spec/lib/account_reach_finder_spec.rb
@@ -38,16 +38,23 @@ RSpec.describe AccountReachFinder do
   end
 
   describe '#inboxes' do
-    it 'includes the preferred inbox URL of followers' do
-      expect(described_class.new(account).inboxes).to include(*[ap_follower_example_com, ap_follower_example_org, ap_follower_with_shared].map(&:preferred_inbox_url))
+    subject { described_class.new(account).inboxes }
+
+    it 'includes the preferred inbox URL of followers and recently mentioned accounts but not unrelated users' do
+      expect(subject)
+        .to include(*follower_inbox_urls)
+        .and include(*mentioned_account_inbox_urls)
+        .and not_include(unrelated_account.preferred_inbox_url)
     end
 
-    it 'includes the preferred inbox URL of recently-mentioned accounts' do
-      expect(described_class.new(account).inboxes).to include(*[ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org].map(&:preferred_inbox_url))
+    def follower_inbox_urls
+      [ap_follower_example_com, ap_follower_example_org, ap_follower_with_shared]
+        .map(&:preferred_inbox_url)
     end
 
-    it 'does not include the inbox of unrelated users' do
-      expect(described_class.new(account).inboxes).to_not include(unrelated_account.preferred_inbox_url)
+    def mentioned_account_inbox_urls
+      [ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org]
+        .map(&:preferred_inbox_url)
     end
   end
 end

From 0ff427fab3f5eb53dea3f4979b16994417c52fe4 Mon Sep 17 00:00:00 2001
From: Christian Schmidt <github@chsc.dk>
Date: Tue, 15 Oct 2024 14:26:20 +0100
Subject: [PATCH 08/19] Translate to regional language variant (e.g. pt-BR)
 (#32428)

---
 .../api/v1/statuses/translations_controller.rb      |  2 +-
 app/services/translate_status_service.rb            | 10 ++++++++--
 spec/services/translate_status_service_spec.rb      | 13 ++++++++++++-
 3 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb
index 8cf495f78a..bd5cd9bb07 100644
--- a/app/controllers/api/v1/statuses/translations_controller.rb
+++ b/app/controllers/api/v1/statuses/translations_controller.rb
@@ -23,6 +23,6 @@ class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseControl
   private
 
   def set_translation
-    @translation = TranslateStatusService.new.call(@status, content_locale)
+    @translation = TranslateStatusService.new.call(@status, I18n.locale.to_s)
   end
 end
diff --git a/app/services/translate_status_service.rb b/app/services/translate_status_service.rb
index e2e076e21b..bcd4703beb 100644
--- a/app/services/translate_status_service.rb
+++ b/app/services/translate_status_service.rb
@@ -9,6 +9,8 @@ class TranslateStatusService < BaseService
   def call(status, target_language)
     @status = status
     @source_texts = source_texts
+
+    target_language = target_language.split(/[_-]/).first unless target_languages.include?(target_language)
     @target_language = target_language
 
     raise Mastodon::NotPermittedError unless permitted?
@@ -32,11 +34,15 @@ class TranslateStatusService < BaseService
   def permitted?
     return false unless @status.distributable? && TranslationService.configured?
 
-    languages[@status.language]&.include?(@target_language)
+    target_languages.include?(@target_language)
   end
 
   def languages
-    Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { TranslationService.configured.languages }
+    Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { translation_backend.languages }
+  end
+
+  def target_languages
+    languages[@status.language] || []
   end
 
   def content_hash
diff --git a/spec/services/translate_status_service_spec.rb b/spec/services/translate_status_service_spec.rb
index cd92fb8d10..ac7a43ff2a 100644
--- a/spec/services/translate_status_service_spec.rb
+++ b/spec/services/translate_status_service_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe TranslateStatusService do
   describe '#call' do
     before do
       translation_service = TranslationService.new
-      allow(translation_service).to receive(:languages).and_return({ 'en' => ['es'] })
+      allow(translation_service).to receive(:languages).and_return({ 'en' => ['es', 'es-MX'] })
       allow(translation_service).to receive(:translate) do |texts|
         texts.map do |text|
           TranslationService::Translation.new(
@@ -37,6 +37,7 @@ RSpec.describe TranslateStatusService do
         .to have_attributes(
           content: '<p>Hola</p>',
           detected_source_language: 'en',
+          language: 'es',
           provider: 'Dummy',
           status: status
         )
@@ -101,6 +102,16 @@ RSpec.describe TranslateStatusService do
         expect(media_attachment.description).to eq 'Hola & :highfive:'
       end
     end
+
+    describe 'target language is regional' do
+      it 'uses regional variant' do
+        expect(service.call(status, 'es-MX').language).to eq 'es-MX'
+      end
+
+      it 'uses parent locale for unsupported regional variant' do
+        expect(service.call(status, 'es-XX').language).to eq 'es'
+      end
+    end
   end
 
   describe '#source_texts' do

From 63df649fe52e3e19a4f5514375380dd81e3e0e1f Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 09:30:17 -0400
Subject: [PATCH 09/19] Expand coverage for `Block` model (#32480)

---
 spec/models/block_spec.rb | 28 +++++++++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb
index 84f0f318f4..62d7e40e28 100644
--- a/spec/models/block_spec.rb
+++ b/spec/models/block_spec.rb
@@ -3,11 +3,37 @@
 require 'rails_helper'
 
 RSpec.describe Block do
-  describe 'validations' do
+  describe 'Associations' do
     it { is_expected.to belong_to(:account).required }
     it { is_expected.to belong_to(:target_account).required }
   end
 
+  describe '#local?' do
+    it { is_expected.to_not be_local }
+  end
+
+  describe 'Callbacks' do
+    describe 'Setting a URI' do
+      context 'when URI exists' do
+        subject { Fabricate.build :block, uri: 'https://uri/value' }
+
+        it 'does not change' do
+          expect { subject.save }
+            .to not_change(subject, :uri)
+        end
+      end
+
+      context 'when URI is blank' do
+        subject { Fabricate.build :follow, uri: nil }
+
+        it 'populates the value' do
+          expect { subject.save }
+            .to change(subject, :uri).to(be_present)
+        end
+      end
+    end
+  end
+
   it 'removes blocking cache after creation' do
     account = Fabricate(:account)
     target_account = Fabricate(:account)

From ae676edc2b6acf7db09e20bf99c73c94175a4c19 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 09:43:08 -0400
Subject: [PATCH 10/19] Expand coverage for `User#token_for_app` (#32434)

---
 spec/models/user_spec.rb | 40 ++++++++++++++++++++++++++++++----------
 1 file changed, 30 insertions(+), 10 deletions(-)

diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index d28e6658f1..4393be5a4e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -387,23 +387,43 @@ RSpec.describe User do
     end
   end
 
-  describe 'token_for_app' do
+  describe '#token_for_app' do
     let(:user) { Fabricate(:user) }
-    let(:app) { Fabricate(:application, owner: user) }
 
-    it 'returns a token' do
-      expect(user.token_for_app(app)).to be_a(Doorkeeper::AccessToken)
+    context 'when user owns app but does not have tokens' do
+      let(:app) { Fabricate(:application, owner: user) }
+
+      it 'creates and returns a persisted token' do
+        expect { user.token_for_app(app) }
+          .to change(Doorkeeper::AccessToken.where(resource_owner_id: user.id, application: app), :count).by(1)
+      end
     end
 
-    it 'persists a token' do
-      t = user.token_for_app(app)
-      expect(user.token_for_app(app)).to eql(t)
+    context 'when user owns app and already has tokens' do
+      let(:app) { Fabricate(:application, owner: user) }
+      let!(:token) { Fabricate :access_token, application: app, resource_owner_id: user.id }
+
+      it 'returns a persisted token' do
+        expect(user.token_for_app(app))
+          .to be_a(Doorkeeper::AccessToken)
+          .and eq(token)
+      end
     end
 
-    it 'is nil if user does not own app' do
-      app.update!(owner: nil)
+    context 'when user does not own app' do
+      let(:app) { Fabricate(:application) }
 
-      expect(user.token_for_app(app)).to be_nil
+      it 'returns nil' do
+        expect(user.token_for_app(app))
+          .to be_nil
+      end
+    end
+
+    context 'when app is nil' do
+      it 'returns nil' do
+        expect(user.token_for_app(nil))
+          .to be_nil
+      end
     end
   end
 

From 527d1253bf4e49c1255f65f213c2ef01ae14a0e9 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 09:51:52 -0400
Subject: [PATCH 11/19] Reduce factory creation (14 -> 8) in
 `ActivityPub::Activity::Block` spec (#32488)

---
 spec/lib/activitypub/activity/block_spec.rb | 128 ++++++++------------
 1 file changed, 51 insertions(+), 77 deletions(-)

diff --git a/spec/lib/activitypub/activity/block_spec.rb b/spec/lib/activitypub/activity/block_spec.rb
index 6f68984018..385628852b 100644
--- a/spec/lib/activitypub/activity/block_spec.rb
+++ b/spec/lib/activitypub/activity/block_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::Activity::Block do
+  subject { described_class.new(json, sender) }
+
   let(:sender)    { Fabricate(:account) }
   let(:recipient) { Fabricate(:account) }
 
@@ -16,93 +18,65 @@ RSpec.describe ActivityPub::Activity::Block do
     }.with_indifferent_access
   end
 
-  context 'when the recipient does not follow the sender' do
-    describe '#perform' do
-      subject { described_class.new(json, sender) }
-
-      before do
-        subject.perform
-      end
-
+  describe '#perform' do
+    context 'when the recipient does not follow the sender' do
       it 'creates a block from sender to recipient' do
-        expect(sender.blocking?(recipient)).to be true
+        subject.perform
+
+        expect(sender)
+          .to be_blocking(recipient)
       end
     end
-  end
 
-  context 'when the recipient is already blocked' do
-    before do
-      sender.block!(recipient, uri: 'old')
+    context 'when the recipient is already blocked' do
+      before { sender.block!(recipient, uri: 'old') }
+
+      it 'creates a block from sender to recipient and sets uri to last received block activity' do
+        subject.perform
+
+        expect(sender)
+          .to be_blocking(recipient)
+        expect(sender.block_relationships.find_by(target_account: recipient).uri)
+          .to eq 'foo'
+      end
     end
 
-    describe '#perform' do
-      subject { described_class.new(json, sender) }
+    context 'when the recipient follows the sender' do
+      before { recipient.follow!(sender) }
+
+      it 'creates a block from sender to recipient and ensures recipient not following sender' do
+        subject.perform
+
+        expect(sender)
+          .to be_blocking(recipient)
+        expect(recipient)
+          .to_not be_following(sender)
+      end
+    end
+
+    context 'when a matching undo has been received first' do
+      let(:undo_json) do
+        {
+          '@context': 'https://www.w3.org/ns/activitystreams',
+          id: 'bar',
+          type: 'Undo',
+          actor: ActivityPub::TagManager.instance.uri_for(sender),
+          object: json,
+        }.with_indifferent_access
+      end
 
       before do
+        recipient.follow!(sender)
+        ActivityPub::Activity::Undo.new(undo_json, sender).perform
+      end
+
+      it 'does not create a block from sender to recipient and ensures recipient not following sender' do
         subject.perform
-      end
 
-      it 'creates a block from sender to recipient' do
-        expect(sender.blocking?(recipient)).to be true
-      end
-
-      it 'sets the uri to that of last received block activity' do
-        expect(sender.block_relationships.find_by(target_account: recipient).uri).to eq 'foo'
-      end
-    end
-  end
-
-  context 'when the recipient follows the sender' do
-    before do
-      recipient.follow!(sender)
-    end
-
-    describe '#perform' do
-      subject { described_class.new(json, sender) }
-
-      before do
-        subject.perform
-      end
-
-      it 'creates a block from sender to recipient' do
-        expect(sender.blocking?(recipient)).to be true
-      end
-
-      it 'ensures recipient is not following sender' do
-        expect(recipient.following?(sender)).to be false
-      end
-    end
-  end
-
-  context 'when a matching undo has been received first' do
-    let(:undo_json) do
-      {
-        '@context': 'https://www.w3.org/ns/activitystreams',
-        id: 'bar',
-        type: 'Undo',
-        actor: ActivityPub::TagManager.instance.uri_for(sender),
-        object: json,
-      }.with_indifferent_access
-    end
-
-    before do
-      recipient.follow!(sender)
-      ActivityPub::Activity::Undo.new(undo_json, sender).perform
-    end
-
-    describe '#perform' do
-      subject { described_class.new(json, sender) }
-
-      before do
-        subject.perform
-      end
-
-      it 'does not create a block from sender to recipient' do
-        expect(sender.blocking?(recipient)).to be false
-      end
-
-      it 'ensures recipient is not following sender' do
-        expect(recipient.following?(sender)).to be false
+        expect(sender)
+          .to_not be_blocking(recipient)
+        expect(recipient)
+          .to_not be_following(sender)
       end
     end
   end

From ad4be1247310adaf75b8bf6fc9bce8b78f5f74ba Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 10:14:17 -0400
Subject: [PATCH 12/19] Add mention of encryption secrets to production sample
 (#32512)

---
 .env.production.sample | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/.env.production.sample b/.env.production.sample
index 0b458a1aa9..87ea031c4c 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -45,6 +45,16 @@ ES_PASS=password
 SECRET_KEY_BASE=
 OTP_SECRET=
 
+# Encryption secrets
+# ------------------
+# Must be available (and set to same values) for all server processes
+# These are private/secret values, do not share outside hosting environment
+# Use `bin/rails db:encryption:init` to generate fresh secrets
+# ------------------
+# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=
+# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=
+# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
+
 # Web Push
 # --------
 # Generate with `bundle exec rails mastodon:webpush:generate_vapid_key`

From 6d72c13a4d03e6c999d1ad92adff3c55a7ceb826 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 10:18:20 -0400
Subject: [PATCH 13/19] Convert status embed controller to request spec
 (#32448)

---
 spec/controllers/statuses_controller_spec.rb | 72 -------------------
 spec/requests/statuses/embed_spec.rb         | 74 ++++++++++++++++++++
 2 files changed, 74 insertions(+), 72 deletions(-)
 create mode 100644 spec/requests/statuses/embed_spec.rb

diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index d9702251f4..121e4aa6c6 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -736,76 +736,4 @@ RSpec.describe StatusesController do
       end
     end
   end
-
-  describe 'GET #embed' do
-    let(:account) { Fabricate(:account) }
-    let(:status)  { Fabricate(:status, account: account) }
-
-    context 'when account is suspended' do
-      let(:account) { Fabricate(:account, suspended: true) }
-
-      before do
-        get :embed, params: { account_username: account.username, id: status.id }
-      end
-
-      it 'returns http gone' do
-        expect(response).to have_http_status(410)
-      end
-    end
-
-    context 'when status is a reblog' do
-      let(:original_account) { Fabricate(:account, domain: 'example.com') }
-      let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }
-      let(:status) { Fabricate(:status, account: account, reblog: original_status) }
-
-      before do
-        get :embed, params: { account_username: status.account.username, id: status.id }
-      end
-
-      it 'returns http not found' do
-        expect(response).to have_http_status(404)
-      end
-    end
-
-    context 'when status is public' do
-      before do
-        get :embed, params: { account_username: status.account.username, id: status.id }
-      end
-
-      it 'renders status successfully', :aggregate_failures do
-        expect(response)
-          .to have_http_status(200)
-          .and render_template(:embed)
-        expect(response.headers).to include(
-          'Vary' => 'Accept, Accept-Language, Cookie',
-          'Cache-Control' => include('public'),
-          'Link' => include('activity+json')
-        )
-      end
-    end
-
-    context 'when status is private' do
-      let(:status) { Fabricate(:status, account: account, visibility: :private) }
-
-      before do
-        get :embed, params: { account_username: status.account.username, id: status.id }
-      end
-
-      it 'returns http not found' do
-        expect(response).to have_http_status(404)
-      end
-    end
-
-    context 'when status is direct' do
-      let(:status) { Fabricate(:status, account: account, visibility: :direct) }
-
-      before do
-        get :embed, params: { account_username: status.account.username, id: status.id }
-      end
-
-      it 'returns http not found' do
-        expect(response).to have_http_status(404)
-      end
-    end
-  end
 end
diff --git a/spec/requests/statuses/embed_spec.rb b/spec/requests/statuses/embed_spec.rb
new file mode 100644
index 0000000000..33c7ea192c
--- /dev/null
+++ b/spec/requests/statuses/embed_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Status embed' do
+  describe 'GET /users/:account_username/statuses/:id/embed' do
+    subject { get "/users/#{account.username}/statuses/#{status.id}/embed" }
+
+    let(:account) { Fabricate(:account) }
+    let(:status)  { Fabricate(:status, account: account) }
+
+    context 'when account is suspended' do
+      let(:account) { Fabricate(:account, suspended: true) }
+
+      it 'returns http gone' do
+        subject
+
+        expect(response)
+          .to have_http_status(410)
+      end
+    end
+
+    context 'when status is a reblog' do
+      let(:original_account) { Fabricate(:account, domain: 'example.com') }
+      let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }
+      let(:status) { Fabricate(:status, account: account, reblog: original_status) }
+
+      it 'returns http not found' do
+        subject
+
+        expect(response)
+          .to have_http_status(404)
+      end
+    end
+
+    context 'when status is public' do
+      it 'renders status successfully', :aggregate_failures do
+        subject
+
+        expect(response)
+          .to have_http_status(200)
+        expect(response.parsed_body.at('body.embed'))
+          .to be_present
+        expect(response.headers).to include(
+          'Vary' => 'Accept, Accept-Language, Cookie',
+          'Cache-Control' => include('public'),
+          'Link' => include('activity+json')
+        )
+      end
+    end
+
+    context 'when status is private' do
+      let(:status) { Fabricate(:status, account: account, visibility: :private) }
+
+      it 'returns http not found' do
+        subject
+
+        expect(response)
+          .to have_http_status(404)
+      end
+    end
+
+    context 'when status is direct' do
+      let(:status) { Fabricate(:status, account: account, visibility: :direct) }
+
+      it 'returns http not found' do
+        subject
+
+        expect(response)
+          .to have_http_status(404)
+      end
+    end
+  end
+end

From 9258ee884751d6f8d4393d8c39e05a4822943e21 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 10:24:12 -0400
Subject: [PATCH 14/19] Improve `app/policies` coverage (#32426)

---
 .../account_moderation_note_policy_spec.rb    |  5 +-
 spec/policies/account_policy_spec.rb          |  3 +-
 spec/policies/account_warning_policy_spec.rb  | 42 ++++++++++++++
 .../account_warning_preset_policy_spec.rb     |  5 +-
 spec/policies/admin/status_policy_spec.rb     |  5 +-
 spec/policies/announcement_policy_spec.rb     |  5 +-
 spec/policies/appeal_policy_spec.rb           |  7 +--
 spec/policies/audit_log_policy_spec.rb        | 20 +++++++
 spec/policies/backup_policy_spec.rb           |  1 -
 .../canonical_email_block_policy_spec.rb      |  5 +-
 spec/policies/custom_emoji_policy_spec.rb     |  1 -
 spec/policies/dashboard_policy_spec.rb        | 20 +++++++
 spec/policies/delivery_policy_spec.rb         |  5 +-
 spec/policies/domain_allow_policy_spec.rb     | 24 ++++++++
 spec/policies/domain_block_policy_spec.rb     |  3 +-
 .../email_domain_block_policy_spec.rb         |  1 -
 .../follow_recommendation_policy_spec.rb      |  5 +-
 spec/policies/instance_policy_spec.rb         |  1 -
 spec/policies/invite_policy_spec.rb           |  1 -
 spec/policies/ip_block_policy_spec.rb         |  5 +-
 spec/policies/poll_policy_spec.rb             | 36 ++++++++++++
 spec/policies/preview_card_policy_spec.rb     |  5 +-
 .../preview_card_provider_policy_spec.rb      |  5 +-
 spec/policies/relay_policy_spec.rb            |  1 -
 spec/policies/report_note_policy_spec.rb      |  1 -
 spec/policies/report_policy_spec.rb           |  1 -
 spec/policies/rule_policy_spec.rb             |  5 +-
 spec/policies/settings_policy_spec.rb         |  1 -
 spec/policies/software_update_policy_spec.rb  |  1 -
 spec/policies/status_policy_spec.rb           |  1 -
 spec/policies/tag_policy_spec.rb              |  1 -
 spec/policies/user_policy_spec.rb             | 39 ++++++++++++-
 spec/policies/user_role_policy_spec.rb        | 56 +++++++++++++++++++
 spec/policies/webhook_policy_spec.rb          |  1 -
 spec/rails_helper.rb                          |  1 +
 35 files changed, 264 insertions(+), 55 deletions(-)
 create mode 100644 spec/policies/account_warning_policy_spec.rb
 create mode 100644 spec/policies/audit_log_policy_spec.rb
 create mode 100644 spec/policies/dashboard_policy_spec.rb
 create mode 100644 spec/policies/domain_allow_policy_spec.rb
 create mode 100644 spec/policies/poll_policy_spec.rb
 create mode 100644 spec/policies/user_role_policy_spec.rb

diff --git a/spec/policies/account_moderation_note_policy_spec.rb b/spec/policies/account_moderation_note_policy_spec.rb
index 8c37acc39f..8b33a71012 100644
--- a/spec/policies/account_moderation_note_policy_spec.rb
+++ b/spec/policies/account_moderation_note_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe AccountModerationNotePolicy do
   subject { described_class }
@@ -12,13 +11,13 @@ RSpec.describe AccountModerationNotePolicy do
   permissions :create? do
     context 'when staff' do
       it 'grants to create' do
-        expect(subject).to permit(admin, described_class)
+        expect(subject).to permit(admin, AccountModerationNote)
       end
     end
 
     context 'when not staff' do
       it 'denies to create' do
-        expect(subject).to_not permit(john, described_class)
+        expect(subject).to_not permit(john, AccountModerationNote)
       end
     end
   end
diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb
index d7a21d8e39..75724e831b 100644
--- a/spec/policies/account_policy_spec.rb
+++ b/spec/policies/account_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe AccountPolicy do
   subject { described_class }
@@ -24,7 +23,7 @@ RSpec.describe AccountPolicy do
     end
   end
 
-  permissions :show?, :unsilence?, :unsensitive?, :remove_avatar?, :remove_header? do
+  permissions :show?, :unsilence?, :unsensitive?, :remove_avatar?, :remove_header?, :sensitive?, :warn? do
     context 'when staff' do
       it 'permits' do
         expect(subject).to permit(admin, alice)
diff --git a/spec/policies/account_warning_policy_spec.rb b/spec/policies/account_warning_policy_spec.rb
new file mode 100644
index 0000000000..9abc9d35d6
--- /dev/null
+++ b/spec/policies/account_warning_policy_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AccountWarningPolicy do
+  subject { described_class }
+
+  let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:account) { Fabricate(:account) }
+
+  permissions :show? do
+    context 'with an admin' do
+      it { is_expected.to permit(admin, AccountWarning.new) }
+    end
+
+    context 'with a non-admin' do
+      context 'when account is not target' do
+        it { is_expected.to_not permit(account, AccountWarning.new) }
+      end
+
+      context 'when account is target' do
+        it { is_expected.to permit(account, AccountWarning.new(target_account_id: account.id)) }
+      end
+    end
+  end
+
+  permissions :appeal? do
+    context 'when account is not target' do
+      it { is_expected.to_not permit(account, AccountWarning.new) }
+    end
+
+    context 'when account is target' do
+      context 'when record is appealable' do
+        it { is_expected.to permit(account, AccountWarning.new(target_account_id: account.id, created_at: Appeal::MAX_STRIKE_AGE.ago + 1.hour)) }
+      end
+
+      context 'when record is not appealable' do
+        it { is_expected.to_not permit(account, AccountWarning.new(target_account_id: account.id, created_at: Appeal::MAX_STRIKE_AGE.ago - 1.hour)) }
+      end
+    end
+  end
+end
diff --git a/spec/policies/account_warning_preset_policy_spec.rb b/spec/policies/account_warning_preset_policy_spec.rb
index 53e224f19f..33f2fb1187 100644
--- a/spec/policies/account_warning_preset_policy_spec.rb
+++ b/spec/policies/account_warning_preset_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe AccountWarningPresetPolicy do
   let(:policy) { described_class }
@@ -11,13 +10,13 @@ RSpec.describe AccountWarningPresetPolicy do
   permissions :index?, :create?, :update?, :destroy? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, AccountWarningPreset)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, AccountWarningPreset)
       end
     end
   end
diff --git a/spec/policies/admin/status_policy_spec.rb b/spec/policies/admin/status_policy_spec.rb
index 07af425516..4df29393e3 100644
--- a/spec/policies/admin/status_policy_spec.rb
+++ b/spec/policies/admin/status_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe Admin::StatusPolicy do
   let(:policy) { described_class }
@@ -13,13 +12,13 @@ RSpec.describe Admin::StatusPolicy do
   permissions :index?, :update?, :review?, :destroy? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, Status)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, Status)
       end
     end
   end
diff --git a/spec/policies/announcement_policy_spec.rb b/spec/policies/announcement_policy_spec.rb
index 503ffca6dc..ab0c1dbaf5 100644
--- a/spec/policies/announcement_policy_spec.rb
+++ b/spec/policies/announcement_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe AnnouncementPolicy do
   let(:policy) { described_class }
@@ -11,13 +10,13 @@ RSpec.describe AnnouncementPolicy do
   permissions :index?, :create?, :update?, :destroy? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, Announcement)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, Announcement)
       end
     end
   end
diff --git a/spec/policies/appeal_policy_spec.rb b/spec/policies/appeal_policy_spec.rb
index 1bf8ce0a0d..cdb93bf56c 100644
--- a/spec/policies/appeal_policy_spec.rb
+++ b/spec/policies/appeal_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe AppealPolicy do
   let(:policy) { described_class }
@@ -12,18 +11,18 @@ RSpec.describe AppealPolicy do
   permissions :index? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, Appeal)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, Appeal)
       end
     end
   end
 
-  permissions :reject? do
+  permissions :reject?, :approve? do
     context 'with an admin' do
       context 'with a pending appeal' do
         before { allow(appeal).to receive(:pending?).and_return(true) }
diff --git a/spec/policies/audit_log_policy_spec.rb b/spec/policies/audit_log_policy_spec.rb
new file mode 100644
index 0000000000..d9d9359433
--- /dev/null
+++ b/spec/policies/audit_log_policy_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AuditLogPolicy do
+  subject { described_class }
+
+  let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:account) { Fabricate(:account) }
+
+  permissions :index? do
+    context 'with an admin' do
+      it { is_expected.to permit(admin, nil) }
+    end
+
+    context 'with a non-admin' do
+      it { is_expected.to_not permit(account, nil) }
+    end
+  end
+end
diff --git a/spec/policies/backup_policy_spec.rb b/spec/policies/backup_policy_spec.rb
index 28cb65d789..031021d91d 100644
--- a/spec/policies/backup_policy_spec.rb
+++ b/spec/policies/backup_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe BackupPolicy do
   subject { described_class }
diff --git a/spec/policies/canonical_email_block_policy_spec.rb b/spec/policies/canonical_email_block_policy_spec.rb
index f5029d9e6b..b253b439a6 100644
--- a/spec/policies/canonical_email_block_policy_spec.rb
+++ b/spec/policies/canonical_email_block_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe CanonicalEmailBlockPolicy do
   let(:policy) { described_class }
@@ -11,13 +10,13 @@ RSpec.describe CanonicalEmailBlockPolicy do
   permissions :index?, :show?, :test?, :create?, :destroy? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, CanonicalEmailBlock)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, CanonicalEmailBlock)
       end
     end
   end
diff --git a/spec/policies/custom_emoji_policy_spec.rb b/spec/policies/custom_emoji_policy_spec.rb
index cb869c7d9a..189885938c 100644
--- a/spec/policies/custom_emoji_policy_spec.rb
+++ b/spec/policies/custom_emoji_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe CustomEmojiPolicy do
   subject { described_class }
diff --git a/spec/policies/dashboard_policy_spec.rb b/spec/policies/dashboard_policy_spec.rb
new file mode 100644
index 0000000000..90c71db381
--- /dev/null
+++ b/spec/policies/dashboard_policy_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe DashboardPolicy do
+  subject { described_class }
+
+  let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:account) { Fabricate(:account) }
+
+  permissions :index? do
+    context 'with an admin' do
+      it { is_expected.to permit(admin, nil) }
+    end
+
+    context 'with a non-admin' do
+      it { is_expected.to_not permit(account, nil) }
+    end
+  end
+end
diff --git a/spec/policies/delivery_policy_spec.rb b/spec/policies/delivery_policy_spec.rb
index bb82389eec..8bc200159a 100644
--- a/spec/policies/delivery_policy_spec.rb
+++ b/spec/policies/delivery_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe DeliveryPolicy do
   let(:policy) { described_class }
@@ -11,13 +10,13 @@ RSpec.describe DeliveryPolicy do
   permissions :clear_delivery_errors?, :restart_delivery?, :stop_delivery? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, nil)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, nil)
       end
     end
   end
diff --git a/spec/policies/domain_allow_policy_spec.rb b/spec/policies/domain_allow_policy_spec.rb
new file mode 100644
index 0000000000..1d285065b8
--- /dev/null
+++ b/spec/policies/domain_allow_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe DomainAllowPolicy do
+  subject { described_class }
+
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :index?, :show?, :create?, :destroy? do
+    context 'when admin' do
+      it 'permits' do
+        expect(subject).to permit(admin, DomainAllow)
+      end
+    end
+
+    context 'when not admin' do
+      it 'denies' do
+        expect(subject).to_not permit(john, DomainAllow)
+      end
+    end
+  end
+end
diff --git a/spec/policies/domain_block_policy_spec.rb b/spec/policies/domain_block_policy_spec.rb
index 4c89f3f374..7c77d1870d 100644
--- a/spec/policies/domain_block_policy_spec.rb
+++ b/spec/policies/domain_block_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe DomainBlockPolicy do
   subject { described_class }
@@ -9,7 +8,7 @@ RSpec.describe DomainBlockPolicy do
   let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
   let(:john)    { Fabricate(:account) }
 
-  permissions :index?, :show?, :create?, :destroy? do
+  permissions :index?, :show?, :create?, :destroy?, :update? do
     context 'when admin' do
       it 'permits' do
         expect(subject).to permit(admin, DomainBlock)
diff --git a/spec/policies/email_domain_block_policy_spec.rb b/spec/policies/email_domain_block_policy_spec.rb
index 7ecff4be49..e98d65a3c7 100644
--- a/spec/policies/email_domain_block_policy_spec.rb
+++ b/spec/policies/email_domain_block_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe EmailDomainBlockPolicy do
   subject { described_class }
diff --git a/spec/policies/follow_recommendation_policy_spec.rb b/spec/policies/follow_recommendation_policy_spec.rb
index ae74d5c3a8..665ed9b059 100644
--- a/spec/policies/follow_recommendation_policy_spec.rb
+++ b/spec/policies/follow_recommendation_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe FollowRecommendationPolicy do
   let(:policy) { described_class }
@@ -11,13 +10,13 @@ RSpec.describe FollowRecommendationPolicy do
   permissions :show?, :suppress?, :unsuppress? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, FollowRecommendation)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, FollowRecommendation)
       end
     end
   end
diff --git a/spec/policies/instance_policy_spec.rb b/spec/policies/instance_policy_spec.rb
index a0d9a008b7..6cdc738022 100644
--- a/spec/policies/instance_policy_spec.rb
+++ b/spec/policies/instance_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe InstancePolicy do
   subject { described_class }
diff --git a/spec/policies/invite_policy_spec.rb b/spec/policies/invite_policy_spec.rb
index cbe3735d80..3717a44999 100644
--- a/spec/policies/invite_policy_spec.rb
+++ b/spec/policies/invite_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe InvitePolicy do
   subject { described_class }
diff --git a/spec/policies/ip_block_policy_spec.rb b/spec/policies/ip_block_policy_spec.rb
index 97bc239e9a..33ea342c10 100644
--- a/spec/policies/ip_block_policy_spec.rb
+++ b/spec/policies/ip_block_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe IpBlockPolicy do
   let(:policy) { described_class }
@@ -11,13 +10,13 @@ RSpec.describe IpBlockPolicy do
   permissions :index?, :show?, :create?, :update?, :destroy? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, IpBlock)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, IpBlock)
       end
     end
   end
diff --git a/spec/policies/poll_policy_spec.rb b/spec/policies/poll_policy_spec.rb
new file mode 100644
index 0000000000..aa1701cb06
--- /dev/null
+++ b/spec/policies/poll_policy_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe PollPolicy do
+  subject { described_class }
+
+  let(:account) { Fabricate(:account) }
+  let(:poll) { Fabricate :poll }
+
+  permissions :vote? do
+    context 'when account cannot view status' do
+      before { poll.status.update(visibility: :private) }
+
+      it { is_expected.to_not permit(account, poll) }
+    end
+
+    context 'when account can view status' do
+      context 'when accounts do not block each other' do
+        it { is_expected.to permit(account, poll) }
+      end
+
+      context 'when view blocks poll creator' do
+        before { Fabricate :block, account: account, target_account: poll.account }
+
+        it { is_expected.to_not permit(account, poll) }
+      end
+
+      context 'when poll creator blocks viewer' do
+        before { Fabricate :block, account: poll.account, target_account: account }
+
+        it { is_expected.to_not permit(account, poll) }
+      end
+    end
+  end
+end
diff --git a/spec/policies/preview_card_policy_spec.rb b/spec/policies/preview_card_policy_spec.rb
index a1944303e1..d02a6016cd 100644
--- a/spec/policies/preview_card_policy_spec.rb
+++ b/spec/policies/preview_card_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe PreviewCardPolicy do
   let(:policy) { described_class }
@@ -11,13 +10,13 @@ RSpec.describe PreviewCardPolicy do
   permissions :index?, :review? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, PreviewCard)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, PreviewCard)
       end
     end
   end
diff --git a/spec/policies/preview_card_provider_policy_spec.rb b/spec/policies/preview_card_provider_policy_spec.rb
index 676039a1b7..5e25b364a4 100644
--- a/spec/policies/preview_card_provider_policy_spec.rb
+++ b/spec/policies/preview_card_provider_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe PreviewCardProviderPolicy do
   let(:policy) { described_class }
@@ -11,13 +10,13 @@ RSpec.describe PreviewCardProviderPolicy do
   permissions :index?, :review? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, PreviewCardProvider)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, PreviewCardProvider)
       end
     end
   end
diff --git a/spec/policies/relay_policy_spec.rb b/spec/policies/relay_policy_spec.rb
index 29ba02c26a..5983b2d2ff 100644
--- a/spec/policies/relay_policy_spec.rb
+++ b/spec/policies/relay_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe RelayPolicy do
   subject { described_class }
diff --git a/spec/policies/report_note_policy_spec.rb b/spec/policies/report_note_policy_spec.rb
index b40a878887..02317f763a 100644
--- a/spec/policies/report_note_policy_spec.rb
+++ b/spec/policies/report_note_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe ReportNotePolicy do
   subject { described_class }
diff --git a/spec/policies/report_policy_spec.rb b/spec/policies/report_policy_spec.rb
index 4fc4178075..67f40b5188 100644
--- a/spec/policies/report_policy_spec.rb
+++ b/spec/policies/report_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe ReportPolicy do
   subject { described_class }
diff --git a/spec/policies/rule_policy_spec.rb b/spec/policies/rule_policy_spec.rb
index 5d435e38c1..3086f30446 100644
--- a/spec/policies/rule_policy_spec.rb
+++ b/spec/policies/rule_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe RulePolicy do
   let(:policy) { described_class }
@@ -11,13 +10,13 @@ RSpec.describe RulePolicy do
   permissions :index?, :create?, :update?, :destroy? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, Rule)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, Rule)
       end
     end
   end
diff --git a/spec/policies/settings_policy_spec.rb b/spec/policies/settings_policy_spec.rb
index 4a99314905..48821c706a 100644
--- a/spec/policies/settings_policy_spec.rb
+++ b/spec/policies/settings_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe SettingsPolicy do
   subject { described_class }
diff --git a/spec/policies/software_update_policy_spec.rb b/spec/policies/software_update_policy_spec.rb
index e19ba61612..2bda84cce9 100644
--- a/spec/policies/software_update_policy_spec.rb
+++ b/spec/policies/software_update_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe SoftwareUpdatePolicy do
   subject { described_class }
diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb
index 36ac8d8027..538742610c 100644
--- a/spec/policies/status_policy_spec.rb
+++ b/spec/policies/status_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe StatusPolicy, type: :model do
   subject { described_class }
diff --git a/spec/policies/tag_policy_spec.rb b/spec/policies/tag_policy_spec.rb
index 35da3cc62a..23166e4669 100644
--- a/spec/policies/tag_policy_spec.rb
+++ b/spec/policies/tag_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe TagPolicy do
   subject { described_class }
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index 7854547d26..11a166a24e 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe UserPolicy do
   subject { described_class }
@@ -112,4 +111,42 @@ RSpec.describe UserPolicy do
       end
     end
   end
+
+  permissions :approve?, :reject? do
+    context 'when admin' do
+      context 'when user is approved' do
+        it { is_expected.to_not permit(admin, User.new(approved: true)) }
+      end
+
+      context 'when user is not approved' do
+        it { is_expected.to permit(admin, User.new(approved: false)) }
+      end
+    end
+
+    context 'when not admin' do
+      it { is_expected.to_not permit(john, User.new) }
+    end
+  end
+
+  permissions :change_role? do
+    context 'when not admin' do
+      it { is_expected.to_not permit(john, User.new) }
+    end
+
+    context 'when admin' do
+      let(:user) { User.new(role: role) }
+
+      context 'when role of admin overrides user role' do
+        let(:role) { UserRole.new(position: admin.user.role.position - 10, id: 123) }
+
+        it { is_expected.to permit(admin, user) }
+      end
+
+      context 'when role of admin does not override user role' do
+        let(:role) { UserRole.new(position: admin.user.role.position + 10, id: 123) }
+
+        it { is_expected.to_not permit(admin, user) }
+      end
+    end
+  end
 end
diff --git a/spec/policies/user_role_policy_spec.rb b/spec/policies/user_role_policy_spec.rb
new file mode 100644
index 0000000000..c48b345d68
--- /dev/null
+++ b/spec/policies/user_role_policy_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe UserRolePolicy do
+  subject { described_class }
+
+  let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:account) { Fabricate(:account) }
+
+  permissions :index?, :create? do
+    context 'when admin' do
+      it { is_expected.to permit(admin, UserRole.new) }
+    end
+
+    context 'when not admin' do
+      it { is_expected.to_not permit(account, UserRole.new) }
+    end
+  end
+
+  permissions :update? do
+    context 'when admin' do
+      context 'when role of admin overrides relevant role' do
+        it { is_expected.to permit(admin, UserRole.new(position: admin.user.role.position - 10, id: 123)) }
+      end
+
+      context 'when role of admin does not override relevant role' do
+        it { is_expected.to_not permit(admin, UserRole.new(position: admin.user.role.position + 10, id: 123)) }
+      end
+    end
+
+    context 'when not admin' do
+      it { is_expected.to_not permit(account, UserRole.new) }
+    end
+  end
+
+  permissions :destroy? do
+    context 'when admin' do
+      context 'when role of admin overrides relevant role' do
+        it { is_expected.to permit(admin, UserRole.new(position: admin.user.role.position - 10)) }
+      end
+
+      context 'when role of admin does not override relevant role' do
+        it { is_expected.to_not permit(admin, UserRole.new(position: admin.user.role.position + 10)) }
+      end
+
+      context 'when everyone role' do
+        it { is_expected.to_not permit(admin, UserRole.everyone) }
+      end
+    end
+
+    context 'when not admin' do
+      it { is_expected.to_not permit(account, UserRole.new) }
+    end
+  end
+end
diff --git a/spec/policies/webhook_policy_spec.rb b/spec/policies/webhook_policy_spec.rb
index 96aaae2c30..9899235d83 100644
--- a/spec/policies/webhook_policy_spec.rb
+++ b/spec/policies/webhook_policy_spec.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
-require 'pundit/rspec'
 
 RSpec.describe WebhookPolicy do
   let(:policy) { described_class }
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 84cee0974f..91a2e21bbb 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -43,6 +43,7 @@ require 'paperclip/matchers'
 require 'capybara/rspec'
 require 'chewy/rspec'
 require 'email_spec/rspec'
+require 'pundit/rspec'
 require 'test_prof/recipes/rspec/before_all'
 
 Rails.root.glob('spec/support/**/*.rb').each { |f| require f }

From 41e342a88fb6168054e37c868196e0589acfbb55 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 15 Oct 2024 10:27:46 -0400
Subject: [PATCH 15/19] Convert `admin/invites` controller specs to system
 specs (#32450)

---
 app/views/admin/invites/_invite.html.haml     |  2 +-
 .../admin/invites_controller_spec.rb          | 59 -----------------
 spec/system/admin/invites_spec.rb             | 63 +++++++++++++++++++
 3 files changed, 64 insertions(+), 60 deletions(-)
 delete mode 100644 spec/controllers/admin/invites_controller_spec.rb
 create mode 100644 spec/system/admin/invites_spec.rb

diff --git a/app/views/admin/invites/_invite.html.haml b/app/views/admin/invites/_invite.html.haml
index 53eac1d0cd..e3e5d32542 100644
--- a/app/views/admin/invites/_invite.html.haml
+++ b/app/views/admin/invites/_invite.html.haml
@@ -1,4 +1,4 @@
-%tr
+%tr{ id: dom_id(invite) }
   %td
     .input-copy
       .input-copy__wrapper
diff --git a/spec/controllers/admin/invites_controller_spec.rb b/spec/controllers/admin/invites_controller_spec.rb
deleted file mode 100644
index b6471e80b2..0000000000
--- a/spec/controllers/admin/invites_controller_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe Admin::InvitesController do
-  render_views
-
-  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
-
-  before do
-    sign_in user, scope: :user
-  end
-
-  describe 'GET #index' do
-    subject { get :index, params: { available: true } }
-
-    let!(:invite) { Fabricate(:invite) }
-
-    it 'renders index page' do
-      expect(subject).to render_template :index
-      expect(response.body)
-        .to include(invite.code)
-    end
-  end
-
-  describe 'POST #create' do
-    subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } }
-
-    it 'succeeds to create a invite' do
-      expect { subject }.to change(Invite, :count).by(1)
-      expect(subject).to redirect_to admin_invites_path
-      expect(Invite.last).to have_attributes(user_id: user.id, max_uses: 10)
-    end
-  end
-
-  describe 'DELETE #destroy' do
-    subject { delete :destroy, params: { id: invite.id } }
-
-    let!(:invite) { Fabricate(:invite, expires_at: nil) }
-
-    it 'expires invite' do
-      expect(subject).to redirect_to admin_invites_path
-      expect(invite.reload).to be_expired
-    end
-  end
-
-  describe 'POST #deactivate_all' do
-    before { Fabricate(:invite, expires_at: nil) }
-
-    it 'expires all invites, then redirects to admin_invites_path' do
-      expect { post :deactivate_all }
-        .to change { Invite.exists?(expires_at: nil) }
-        .from(true)
-        .to(false)
-
-      expect(response).to redirect_to admin_invites_path
-    end
-  end
-end
diff --git a/spec/system/admin/invites_spec.rb b/spec/system/admin/invites_spec.rb
new file mode 100644
index 0000000000..f2cee626c6
--- /dev/null
+++ b/spec/system/admin/invites_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Admin Invites' do
+  describe 'Invite interaction' do
+    let!(:invite) { Fabricate(:invite, expires_at: nil) }
+
+    let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+
+    before { sign_in user }
+
+    it 'allows invite listing and creation' do
+      visit admin_invites_path
+
+      expect(page)
+        .to have_title(I18n.t('admin.invites.title'))
+      for_invite(invite) do
+        expect(find('input').value)
+          .to include(invite.code)
+      end
+
+      select I18n.t('invites.max_uses', count: 10), from: max_use_field
+
+      expect { generate_invite }
+        .to change(Invite, :count).by(1)
+      expect(user.invites.last)
+        .to have_attributes(max_uses: 10)
+    end
+
+    it 'allows invite expiration' do
+      visit admin_invites_path
+
+      for_invite(invite) do
+        expect { expire_invite }
+          .to change { invite.reload.expired? }.from(false).to(true)
+      end
+    end
+
+    it 'allows invite deactivation' do
+      visit admin_invites_path
+
+      expect { click_on I18n.t('admin.invites.deactivate_all') }
+        .to change { Invite.exists?(expires_at: nil) }.from(true).to(false)
+    end
+
+    def for_invite(invite, &block)
+      within("#invite_#{invite.id}", &block)
+    end
+
+    def expire_invite
+      click_on I18n.t('invites.delete')
+    end
+
+    def generate_invite
+      click_on I18n.t('invites.generate')
+    end
+
+    def max_use_field
+      I18n.t('simple_form.labels.defaults.max_uses')
+    end
+  end
+end

From b01bd74698aada07d7f5b0d9889cd28e8590a5ab Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Wed, 16 Oct 2024 09:27:44 +0200
Subject: [PATCH 16/19] Add back a 6 hours mute duration option (#32522)

---
 app/javascript/mastodon/features/ui/components/mute_modal.jsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.jsx b/app/javascript/mastodon/features/ui/components/mute_modal.jsx
index 70d95b5931..90b88030a0 100644
--- a/app/javascript/mastodon/features/ui/components/mute_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/mute_modal.jsx
@@ -116,6 +116,7 @@ export const MuteModal = ({ accountId, acct }) => {
         <div className='safety-action-modal__bottom__collapsible'>
           <div className='safety-action-modal__field-group'>
             <RadioButtonLabel name='duration' value='0' label={intl.formatMessage(messages.indefinite)} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
+            <RadioButtonLabel name='duration' value='21600' label={intl.formatMessage(messages.hours, { number: 6 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
             <RadioButtonLabel name='duration' value='86400' label={intl.formatMessage(messages.hours, { number: 24 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
             <RadioButtonLabel name='duration' value='604800' label={intl.formatMessage(messages.days, { number: 7 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
             <RadioButtonLabel name='duration' value='2592000' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />

From a20f38c930d95f9bf94a2bf6fa693a3944aa9f4c Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Oct 2024 09:30:53 +0200
Subject: [PATCH 17/19]  Fix only the first paragraph being displayed in some
 notifications (#32348)

---
 app/javascript/styles/mastodon/components.scss | 14 +++++---------
 1 file changed, 5 insertions(+), 9 deletions(-)

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 1f69dab7be..a20e84ce75 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -10804,21 +10804,17 @@ noscript {
       color: $darker-text-color;
       -webkit-line-clamp: 4;
       -webkit-box-orient: vertical;
-      max-height: 4 * 22px;
+      max-height: none;
       overflow: hidden;
 
-      p {
-        display: none;
-
-        &:first-child {
-          display: initial;
-        }
-      }
-
       p,
       a {
         color: inherit;
       }
+
+      p {
+        margin-bottom: 8px;
+      }
     }
 
     .reply-indicator__attachments {

From 5c4bcd2f082865500c932e2cd33ac0017e694e1e Mon Sep 17 00:00:00 2001
From: Christian Winther <jippignu@gmail.com>
Date: Wed, 16 Oct 2024 09:44:28 +0200
Subject: [PATCH 18/19] Run migration tests against postgres 16 and 17 as well
 (#32416)

---
 .github/workflows/test-migrations.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.github/workflows/test-migrations.yml b/.github/workflows/test-migrations.yml
index 6a0e67c58e..5b80fef037 100644
--- a/.github/workflows/test-migrations.yml
+++ b/.github/workflows/test-migrations.yml
@@ -32,6 +32,8 @@ jobs:
         postgres:
           - 14-alpine
           - 15-alpine
+          - 16-alpine
+          - 17-alpine
 
     services:
       postgres:

From 36452845d78f6c3501af1e39391d06ab88a45a5a Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 16 Oct 2024 10:03:35 +0200
Subject: [PATCH 19/19] Explicitly install ImageMagick in CI (except for
 libvips tests) (#32534)

---
 .github/workflows/test-ruby.yml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml
index 3da53c1ae8..c05c8333b2 100644
--- a/.github/workflows/test-ruby.yml
+++ b/.github/workflows/test-ruby.yml
@@ -143,7 +143,7 @@ jobs:
         uses: ./.github/actions/setup-ruby
         with:
           ruby-version: ${{ matrix.ruby-version}}
-          additional-system-dependencies: ffmpeg libpam-dev
+          additional-system-dependencies: ffmpeg imagemagick libpam-dev
 
       - name: Load database schema
         run: |
@@ -245,7 +245,7 @@ jobs:
         uses: ./.github/actions/setup-ruby
         with:
           ruby-version: ${{ matrix.ruby-version}}
-          additional-system-dependencies: ffmpeg libpam-dev libyaml-dev
+          additional-system-dependencies: ffmpeg libpam-dev
 
       - name: Load database schema
         run: './bin/rails db:create db:schema:load db:seed'
@@ -325,7 +325,7 @@ jobs:
         uses: ./.github/actions/setup-ruby
         with:
           ruby-version: ${{ matrix.ruby-version}}
-          additional-system-dependencies: ffmpeg
+          additional-system-dependencies: ffmpeg imagemagick
 
       - name: Set up Javascript environment
         uses: ./.github/actions/setup-javascript
@@ -445,7 +445,7 @@ jobs:
         uses: ./.github/actions/setup-ruby
         with:
           ruby-version: ${{ matrix.ruby-version}}
-          additional-system-dependencies: ffmpeg
+          additional-system-dependencies: ffmpeg imagemagick
 
       - name: Set up Javascript environment
         uses: ./.github/actions/setup-javascript