From 3a6451c867595fd58998ee1706589b15a69d993b Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 20:19:29 +0100
Subject: [PATCH 01/40] Add support for incoming rich text (#23913)

---
 app/javascript/styles/application.scss        |  1 +
 app/javascript/styles/mastodon/rich_text.scss | 64 +++++++++++++++++++
 lib/sanitize_ext/sanitize_config.rb           | 19 ++----
 spec/lib/sanitize_config_spec.rb              | 28 ++++----
 4 files changed, 85 insertions(+), 27 deletions(-)
 create mode 100644 app/javascript/styles/mastodon/rich_text.scss

diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index 81a040108e..1b2969c234 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -23,3 +23,4 @@
 @import 'mastodon/dashboard';
 @import 'mastodon/rtl';
 @import 'mastodon/accessibility';
+@import 'mastodon/rich_text';
diff --git a/app/javascript/styles/mastodon/rich_text.scss b/app/javascript/styles/mastodon/rich_text.scss
new file mode 100644
index 0000000000..35901984b4
--- /dev/null
+++ b/app/javascript/styles/mastodon/rich_text.scss
@@ -0,0 +1,64 @@
+.status__content__text,
+.e-content,
+.reply-indicator__content {
+  pre,
+  blockquote {
+    margin-bottom: 20px;
+    white-space: pre-wrap;
+    unicode-bidi: plaintext;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  blockquote {
+    padding-left: 10px;
+    border-left: 3px solid $darker-text-color;
+    color: $darker-text-color;
+    white-space: normal;
+
+    p:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  & > ul,
+  & > ol {
+    margin-bottom: 20px;
+  }
+
+  b,
+  strong {
+    font-weight: 700;
+  }
+
+  em,
+  i {
+    font-style: italic;
+  }
+
+  ul,
+  ol {
+    margin-left: 2em;
+
+    p {
+      margin: 0;
+    }
+  }
+
+  ul {
+    list-style-type: disc;
+  }
+
+  ol {
+    list-style-type: decimal;
+  }
+}
+
+.reply-indicator__content {
+  blockquote {
+    border-left-color: $inverted-text-color;
+    color: $inverted-text-color;
+  }
+}
diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb
index dc39e9c90f..9cc500c36e 100644
--- a/lib/sanitize_ext/sanitize_config.rb
+++ b/lib/sanitize_ext/sanitize_config.rb
@@ -51,29 +51,22 @@ class Sanitize
     end
 
     UNSUPPORTED_ELEMENTS_TRANSFORMER = lambda do |env|
-      return unless %w(h1 h2 h3 h4 h5 h6 blockquote pre ul ol li).include?(env[:node_name])
+      return unless %w(h1 h2 h3 h4 h5 h6).include?(env[:node_name])
 
       current_node = env[:node]
 
-      case env[:node_name]
-      when 'li'
-        current_node.traverse do |node|
-          next unless %w(p ul ol li).include?(node.name)
-
-          node.add_next_sibling('<br>') if node.next_sibling
-          node.replace(node.children) unless node.text?
-        end
-      else
-        current_node.name = 'p'
-      end
+      current_node.name = 'strong'
+      current_node.wrap('<p></p>')
     end
 
     MASTODON_STRICT ||= freeze_config(
-      elements: %w(p br span a),
+      elements: %w(p br span a del pre blockquote code b strong u i em ul ol li),
 
       attributes: {
         'a' => %w(href rel class),
         'span' => %w(class),
+        'ol' => %w(start reversed),
+        'li' => %w(value),
       },
 
       add_attributes: {
diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb
index c9543ceb0c..a01122bed0 100644
--- a/spec/lib/sanitize_config_spec.rb
+++ b/spec/lib/sanitize_config_spec.rb
@@ -6,24 +6,16 @@ describe Sanitize::Config do
   describe '::MASTODON_STRICT' do
     subject { Sanitize::Config::MASTODON_STRICT }
 
-    it 'converts h1 to p' do
-      expect(Sanitize.fragment('<h1>Foo</h1>', subject)).to eq '<p>Foo</p>'
+    it 'converts h1 to p strong' do
+      expect(Sanitize.fragment('<h1>Foo</h1>', subject)).to eq '<p><strong>Foo</strong></p>'
     end
 
-    it 'converts ul to p' do
-      expect(Sanitize.fragment('<p>Check out:</p><ul><li>Foo</li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><p>Foo<br>Bar</p>'
+    it 'keeps ul' do
+      expect(Sanitize.fragment('<p>Check out:</p><ul><li>Foo</li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><ul><li>Foo</li><li>Bar</li></ul>'
     end
 
-    it 'converts p inside ul' do
-      expect(Sanitize.fragment('<ul><li><p>Foo</p><p>Bar</p></li><li>Baz</li></ul>', subject)).to eq '<p>Foo<br>Bar<br>Baz</p>'
-    end
-
-    it 'converts ul inside ul' do
-      expect(Sanitize.fragment('<ul><li>Foo</li><li><ul><li>Bar</li><li>Baz</li></ul></li></ul>', subject)).to eq '<p>Foo<br>Bar<br>Baz</p>'
-    end
-
-    it 'keep links in lists' do
-      expect(Sanitize.fragment('<p>Check out:</p><ul><li><a href="https://joinmastodon.org" rel="nofollow noopener noreferrer" target="_blank">joinmastodon.org</a></li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><p><a href="https://joinmastodon.org" rel="nofollow noopener noreferrer" target="_blank">joinmastodon.org</a><br>Bar</p>'
+    it 'keeps start and reversed attributes of ol' do
+      expect(Sanitize.fragment('<p>Check out:</p><ol start="3" reversed=""><li>Foo</li><li>Bar</li></ol>', subject)).to eq '<p>Check out:</p><ol start="3" reversed=""><li>Foo</li><li>Bar</li></ol>'
     end
 
     it 'removes a without href' do
@@ -45,5 +37,13 @@ describe Sanitize::Config do
     it 'keeps a with href' do
       expect(Sanitize.fragment('<a href="http://example.com">Test</a>', subject)).to eq '<a href="http://example.com" rel="nofollow noopener noreferrer" target="_blank">Test</a>'
     end
+
+    it 'removes a with unparsable href' do
+      expect(Sanitize.fragment('<a href=" https://google.fr">Test</a>', subject)).to eq 'Test'
+    end
+
+    it 'keeps a with supported scheme and no host' do
+      expect(Sanitize.fragment('<a href="dweb:/a/foo">Test</a>', subject)).to eq '<a href="dweb:/a/foo" rel="nofollow noopener noreferrer" target="_blank">Test</a>'
+    end
   end
 end

From c2a046ded1d47e2504df05568e34bc6a2a6dc810 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 20:25:15 +0100
Subject: [PATCH 02/40] =?UTF-8?q?Fix=20=E2=80=9CRemove=20all=20followers?=
 =?UTF-8?q?=20from=20the=20selected=20domains=E2=80=9D=20being=20more=20de?=
 =?UTF-8?q?structive=20than=20it=20claims=20(#23805)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/controllers/relationships_controller.rb   |  4 ++--
 app/models/form/account_batch.rb              | 10 ++++----
 .../remove_domains_from_followers_service.rb  | 23 +++++++++++++++++++
 app/views/relationships/show.html.haml        |  2 +-
 .../relationships_controller_spec.rb          | 11 ++++++++-
 5 files changed, 40 insertions(+), 10 deletions(-)
 create mode 100644 app/services/remove_domains_from_followers_service.rb

diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb
index 96cce55e9e..baa34da22a 100644
--- a/app/controllers/relationships_controller.rb
+++ b/app/controllers/relationships_controller.rb
@@ -60,8 +60,8 @@ class RelationshipsController < ApplicationController
       'unfollow'
     elsif params[:remove_from_followers]
       'remove_from_followers'
-    elsif params[:block_domains]
-      'block_domains'
+    elsif params[:block_domains] || params[:remove_domains_from_followers]
+      'remove_domains_from_followers'
     end
   end
 
diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb
index 473622edf4..5a7fc7ed1b 100644
--- a/app/models/form/account_batch.rb
+++ b/app/models/form/account_batch.rb
@@ -17,8 +17,8 @@ class Form::AccountBatch
       unfollow!
     when 'remove_from_followers'
       remove_from_followers!
-    when 'block_domains'
-      block_domains!
+    when 'remove_domains_from_followers'
+      remove_domains_from_followers!
     when 'approve'
       approve!
     when 'reject'
@@ -50,10 +50,8 @@ class Form::AccountBatch
     RemoveFromFollowersService.new.call(current_account, account_ids)
   end
 
-  def block_domains!
-    AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain|
-      [current_account.id, domain]
-    end
+  def remove_domains_from_followers!
+    RemoveDomainsFromFollowersService.new.call(current_account, account_domains)
   end
 
   def account_domains
diff --git a/app/services/remove_domains_from_followers_service.rb b/app/services/remove_domains_from_followers_service.rb
new file mode 100644
index 0000000000..d76763409d
--- /dev/null
+++ b/app/services/remove_domains_from_followers_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class RemoveDomainsFromFollowersService < BaseService
+  include Payloadable
+
+  def call(source_account, target_domains)
+    source_account.passive_relationships.where(account_id: Account.where(domain: target_domains)).find_each do |follow|
+      follow.destroy
+
+      create_notification(follow) if source_account.local? && !follow.account.local? && follow.account.activitypub?
+    end
+  end
+
+  private
+
+  def create_notification(follow)
+    ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.target_account_id, follow.account.inbox_url)
+  end
+
+  def build_json(follow)
+    Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
+  end
+end
diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml
index 2899cd5140..f08e9c1df8 100644
--- a/app/views/relationships/show.html.haml
+++ b/app/views/relationships/show.html.haml
@@ -48,7 +48,7 @@
 
         = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('relationships.confirm_remove_selected_followers') } unless following_relationship?
 
-        = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
+        = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :remove_domains_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
     .batch-table__body
       - if @accounts.empty?
         = nothing_here 'nothing-here--under-tabs'
diff --git a/spec/controllers/relationships_controller_spec.rb b/spec/controllers/relationships_controller_spec.rb
index 39f455e038..53a5daa517 100644
--- a/spec/controllers/relationships_controller_spec.rb
+++ b/spec/controllers/relationships_controller_spec.rb
@@ -58,7 +58,7 @@ describe RelationshipsController do
     end
 
     context 'when select parameter is provided' do
-      subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, block_domains: '' } }
+      subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, remove_domains_from_followers: '' } }
 
       it 'soft-blocks followers from selected domains' do
         poopfeast.follow!(user.account)
@@ -69,6 +69,15 @@ describe RelationshipsController do
         expect(poopfeast.following?(user.account)).to be false
       end
 
+      it 'does not unfollow users from selected domains' do
+        user.account.follow!(poopfeast)
+
+        sign_in user, scope: :user
+        subject
+
+        expect(user.account.following?(poopfeast)).to be true
+      end
+
       include_examples 'authenticate user'
       include_examples 'redirects back to followers page'
     end

From b55fc883b6181fb8080fbafd53bc7a9f1896b295 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 20:25:36 +0100
Subject: [PATCH 03/40] =?UTF-8?q?Fix=20duplicate=20=E2=80=9CPublish?=
 =?UTF-8?q?=E2=80=9D=20button=20on=20mobile=20(#23804)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/javascript/mastodon/features/ui/components/header.jsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx
index 1384bebda0..92adc47a9c 100644
--- a/app/javascript/mastodon/features/ui/components/header.jsx
+++ b/app/javascript/mastodon/features/ui/components/header.jsx
@@ -22,8 +22,8 @@ const mapDispatchToProps = (dispatch) => ({
   },
 });
 
-export default @connect(null, mapDispatchToProps)
-@withRouter
+export default @withRouter
+@connect(null, mapDispatchToProps)
 class Header extends React.PureComponent {
 
   static contextTypes = {

From f8bb4d0d6b1050de481187e9f034b8bbb649d931 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 20:36:18 +0100
Subject: [PATCH 04/40] Fix server error when failing to follow back followers
 from `/relationships` (#23787)

---
 app/controllers/relationships_controller.rb | 2 ++
 app/models/form/account_batch.rb            | 6 ++++++
 config/locales/en.yml                       | 1 +
 3 files changed, 9 insertions(+)

diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb
index baa34da22a..de5dc58792 100644
--- a/app/controllers/relationships_controller.rb
+++ b/app/controllers/relationships_controller.rb
@@ -19,6 +19,8 @@ class RelationshipsController < ApplicationController
     @form.save
   rescue ActionController::ParameterMissing
     # Do nothing
+  rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound
+    flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow'
   ensure
     redirect_to relationships_path(filter_params)
   end
diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb
index 5a7fc7ed1b..6a05f8163a 100644
--- a/app/models/form/account_batch.rb
+++ b/app/models/form/account_batch.rb
@@ -35,9 +35,15 @@ class Form::AccountBatch
   private
 
   def follow!
+    error = nil
+
     accounts.each do |target_account|
       FollowService.new.call(current_account, target_account)
+    rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound => e
+      error ||= e
     end
+
+    raise error if error.present?
   end
 
   def unfollow!
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9f8ba7ce78..d142962b5a 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1408,6 +1408,7 @@ en:
     confirm_remove_selected_followers: Are you sure you want to remove selected followers?
     confirm_remove_selected_follows: Are you sure you want to remove selected follows?
     dormant: Dormant
+    follow_failure: Could not follow some of the selected accounts.
     follow_selected_followers: Follow selected followers
     followers: Followers
     following: Following

From 02c6bad3ca7b59a8be6403fbbddd7d46346532dc Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 20:37:22 +0100
Subject: [PATCH 05/40] Change unintended SMTP read timeout from 5 seconds to
 20 seconds (#23750)

---
 config/environments/production.rb | 1 +
 1 file changed, 1 insertion(+)

diff --git a/config/environments/production.rb b/config/environments/production.rb
index 99c9bb40c5..345a255a74 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -128,6 +128,7 @@ Rails.application.configure do
     enable_starttls_auto: enable_starttls_auto,
     tls: ENV['SMTP_TLS'].presence && ENV['SMTP_TLS'] == 'true',
     ssl: ENV['SMTP_SSL'].presence && ENV['SMTP_SSL'] == 'true',
+    read_timeout: 20,
   }
 
   config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym

From a1347f456e47e085ef1031b607cabd0683c496c2 Mon Sep 17 00:00:00 2001
From: Terry Garcia <10190993+TerryGarcia@users.noreply.github.com>
Date: Fri, 3 Mar 2023 13:37:49 -0600
Subject: [PATCH 06/40] Switched bookmark and favourites around (#23701)

---
 .../mastodon/features/ui/components/navigation_panel.jsx        | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index 9a9309be05..755b19349d 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -82,8 +82,8 @@ class NavigationPanel extends React.Component {
         {signedIn && (
           <React.Fragment>
             <ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
-            <ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
             <ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
+            <ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
             <ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
 
             <ListPanel />

From f94aa70b814159964de660ac33bc797a1e01af30 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 20:44:02 +0100
Subject: [PATCH 07/40] Fix error when displaying post history of a trendable
 post in the admin interface (#23574)

---
 app/views/admin/statuses/show.html.haml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml
index 62b49de8c8..4631e97f16 100644
--- a/app/views/admin/statuses/show.html.haml
+++ b/app/views/admin/statuses/show.html.haml
@@ -34,7 +34,7 @@
           %td
             - if @status.trend.allowed?
               %abbr{ title: t('admin.trends.tags.current_score', score: @status.trend.score) }= t('admin.trends.tags.trending_rank', rank: @status.trend.rank)
-            - elsif @status.trend.requires_review?
+            - elsif @status.requires_review?
               = t('admin.trends.pending_review')
             - else
               = t('admin.trends.not_allowed_to_trend')

From d6679d175181e425f52e5a6b062bc2c5545ce0cc Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 20:44:46 +0100
Subject: [PATCH 08/40] Add mail headers to avoid auto-replies (#23597)

---
 app/mailers/application_mailer.rb | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 73b623576c..35f0b5fee1 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -7,9 +7,17 @@ class ApplicationMailer < ActionMailer::Base
   helper :instance
   helper :formatting
 
+  after_action :set_autoreply_headers!
+
   protected
 
   def locale_for_account(account, &block)
     I18n.with_locale(account.user_locale || I18n.default_locale, &block)
   end
+
+  def set_autoreply_headers!
+    headers['Precedence'] = 'list'
+    headers['X-Auto-Response-Suppress'] = 'All'
+    headers['Auto-Submitted'] = 'auto-generated'
+  end
 end

From 8784498ebfc5508034f447d92645551d9d2c5907 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 20:45:12 +0100
Subject: [PATCH 09/40] Fix tootctl accounts migrate error due to typo (#23567)

---
 lib/mastodon/accounts_cli.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb
index db379eb853..98855cbd06 100644
--- a/lib/mastodon/accounts_cli.rb
+++ b/lib/mastodon/accounts_cli.rb
@@ -627,7 +627,7 @@ module Mastodon
           exit(1)
         end
 
-        unless options[:force] || migration.target_acount_id == account.moved_to_account_id
+        unless options[:force] || migration.target_account_id == account.moved_to_account_id
           say('The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway', :red)
           exit(1)
         end

From 6b16b77ab0f347e688f009c5a04961142b8dd203 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 20:45:55 +0100
Subject: [PATCH 10/40] Fix external authentication not running onboarding code
 for new users (#23458)

---
 app/models/concerns/omniauthable.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb
index b0aa5be6f1..41eae215b4 100644
--- a/app/models/concerns/omniauthable.rb
+++ b/app/models/concerns/omniauthable.rb
@@ -61,7 +61,7 @@ module Omniauthable
         user.account.avatar_remote_url = nil
       end
 
-      user.skip_confirmation! if email_is_verified
+      user.confirm! if email_is_verified
       user.save!
       user
     end

From 3f52e717fa22e89a0eaf5f12f0fb9d2b121c7944 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 20:50:46 +0100
Subject: [PATCH 11/40] Add tests for moderation actions without custom text
 (#23184)

---
 .../admin/reports/actions_controller_spec.rb  | 27 +++++++++++++++++--
 1 file changed, 25 insertions(+), 2 deletions(-)

diff --git a/spec/controllers/admin/reports/actions_controller_spec.rb b/spec/controllers/admin/reports/actions_controller_spec.rb
index 3e42e4cb19..4c2624a408 100644
--- a/spec/controllers/admin/reports/actions_controller_spec.rb
+++ b/spec/controllers/admin/reports/actions_controller_spec.rb
@@ -57,6 +57,9 @@ describe Admin::Reports::ActionsController do
     let!(:media)         { Fabricate(:media_attachment, account: target_account, status: statuses[0]) }
     let(:report)         { Fabricate(:report, target_account: target_account, status_ids: statuses.map(&:id)) }
     let(:text)           { 'hello' }
+    let(:common_params) do
+      { report_id: report.id, text: text }
+    end
 
     shared_examples 'common behavior' do
       it 'closes the report' do
@@ -72,6 +75,26 @@ describe Admin::Reports::ActionsController do
         subject
         expect(response).to redirect_to(admin_reports_path)
       end
+
+      context 'when text is unset' do
+        let(:common_params) do
+          { report_id: report.id }
+        end
+
+        it 'closes the report' do
+          expect { subject }.to change { report.reload.action_taken? }.from(false).to(true)
+        end
+
+        it 'creates a strike with the expected text' do
+          expect { subject }.to change { report.target_account.strikes.count }.by(1)
+          expect(report.target_account.strikes.last.text).to eq ''
+        end
+
+        it 'redirects' do
+          subject
+          expect(response).to redirect_to(admin_reports_path)
+        end
+      end
     end
 
     shared_examples 'all action types' do
@@ -124,13 +147,13 @@ describe Admin::Reports::ActionsController do
     end
 
     context 'action as submit button' do
-      subject { post :create, params: { report_id: report.id, text: text, action => '' } }
+      subject { post :create, params: common_params.merge({ action => '' }) }
 
       it_behaves_like 'all action types'
     end
 
     context 'action as submit button' do
-      subject { post :create, params: { report_id: report.id, text: text, moderation_action: action } }
+      subject { post :create, params: common_params.merge({ moderation_action: action }) }
 
       it_behaves_like 'all action types'
     end

From de137e6bb0d69354b4be5d4a8d845f8bd90551ff Mon Sep 17 00:00:00 2001
From: Jamie Hoyle <j@jamiehoyle.com>
Date: Fri, 3 Mar 2023 19:53:37 +0000
Subject: [PATCH 12/40] Added support for specifying S3 storage classes in
 environment (#22480)

---
 config/initializers/paperclip.rb | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index a2285427c8..9282c941da 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -90,6 +90,12 @@ if ENV['S3_ENABLED'] == 'true'
     )
   end
 
+  if ENV.has_key?('S3_STORAGE_CLASS')
+    Paperclip::Attachment.default_options[:s3_headers].merge!(
+      'X-Amz-Storage-Class' => ENV['S3_STORAGE_CLASS']
+    )
+  end
+
   # Some S3-compatible providers might not actually be compatible with some APIs
   # used by kt-paperclip, see https://github.com/mastodon/mastodon/issues/16822
   if ENV['S3_FORCE_SINGLE_REQUEST'] == 'true'

From 0872f3e3d743a16533ac4fad4cd83b103047808c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ram=C5=ABns=20Usovs?= <ramuuns@enkurs.org>
Date: Fri, 3 Mar 2023 21:01:18 +0100
Subject: [PATCH 13/40] Allow streaming to connect to postgress with
 self-signed certs (#21431)

---
 package.json       |  1 +
 streaming/index.js | 38 +-------------------------------------
 yarn.lock          |  5 +++++
 3 files changed, 7 insertions(+), 37 deletions(-)

diff --git a/package.json b/package.json
index 1ab297d041..15bf30925c 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
     "object.values": "^1.1.6",
     "path-complete-extname": "^1.0.0",
     "pg": "^8.5.0",
+    "pg-connection-string": "^2.5.0",
     "postcss": "^8.4.21",
     "postcss-loader": "^3.0.0",
     "promise.prototype.finally": "^3.1.4",
diff --git a/streaming/index.js b/streaming/index.js
index 32e3babaa4..ba7cfea191 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -7,6 +7,7 @@ const express = require('express');
 const http = require('http');
 const redis = require('redis');
 const pg = require('pg');
+const dbUrlToConfig = require('pg-connection-string').parse;
 const log = require('npmlog');
 const url = require('url');
 const uuid = require('uuid');
@@ -23,43 +24,6 @@ dotenv.config({
 
 log.level = process.env.LOG_LEVEL || 'verbose';
 
-/**
- * @param {string} dbUrl
- * @return {Object.<string, any>}
- */
-const dbUrlToConfig = (dbUrl) => {
-  if (!dbUrl) {
-    return {};
-  }
-
-  const params = url.parse(dbUrl, true);
-  const config = {};
-
-  if (params.auth) {
-    [config.user, config.password] = params.auth.split(':');
-  }
-
-  if (params.hostname) {
-    config.host = params.hostname;
-  }
-
-  if (params.port) {
-    config.port = params.port;
-  }
-
-  if (params.pathname) {
-    config.database = params.pathname.split('/')[1];
-  }
-
-  const ssl = params.query && params.query.ssl;
-
-  if (ssl && ssl === 'true' || ssl === '1') {
-    config.ssl = true;
-  }
-
-  return config;
-};
-
 /**
  * @param {Object.<string, any>} defaultConfig
  * @param {string} redisUrl
diff --git a/yarn.lock b/yarn.lock
index a856cf91dd..68cef79651 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8322,6 +8322,11 @@ pg-connection-string@^2.4.0:
   resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10"
   integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==
 
+pg-connection-string@^2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
+  integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
+
 pg-int8@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"

From 5a8c651e8f0252c7135042e79396f782361302d9 Mon Sep 17 00:00:00 2001
From: Christian Schmidt <github@chsc.dk>
Date: Fri, 3 Mar 2023 21:06:31 +0100
Subject: [PATCH 14/40] Only offer translation for supported languages (#23879)

---
 .rubocop.yml                                  |   4 +
 .../mastodon/components/status_content.jsx    |   4 +-
 app/javascript/mastodon/initial_state.js      |   2 -
 app/lib/translation_service.rb                |   4 +
 app/lib/translation_service/deepl.rb          |  46 +++++---
 .../translation_service/libre_translate.rb    |  38 +++++--
 app/models/status.rb                          |  10 ++
 app/serializers/initial_state_serializer.rb   |   1 -
 app/serializers/rest/status_serializer.rb     |   6 +-
 app/services/translate_status_service.rb      |   2 +-
 spec/lib/translation_service/deepl_spec.rb    | 100 ++++++++++++++++++
 .../libre_translate_spec.rb                   |  71 +++++++++++++
 spec/models/status_spec.rb                    |  79 ++++++++++++++
 13 files changed, 336 insertions(+), 31 deletions(-)
 create mode 100644 spec/lib/translation_service/deepl_spec.rb
 create mode 100644 spec/lib/translation_service/libre_translate_spec.rb

diff --git a/.rubocop.yml b/.rubocop.yml
index 27d778edfb..0a41c54b90 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -97,6 +97,10 @@ Rails/Exit:
     - 'lib/mastodon/cli_helper.rb'
     - 'lib/cli.rb'
 
+RSpec/FilePath:
+  CustomTransform:
+    DeepL: deepl
+
 RSpec/NotToNot:
   EnforcedStyle: to_not
 
diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx
index a1c38171f2..f9c9fe0791 100644
--- a/app/javascript/mastodon/components/status_content.jsx
+++ b/app/javascript/mastodon/components/status_content.jsx
@@ -6,7 +6,7 @@ import { Link } from 'react-router-dom';
 import classnames from 'classnames';
 import PollContainer from 'mastodon/containers/poll_container';
 import Icon from 'mastodon/components/icon';
-import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state';
+import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
 
 const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
 
@@ -220,7 +220,7 @@ class StatusContent extends React.PureComponent {
 
     const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
     const renderReadMore = this.props.onClick && status.get('collapsed');
-    const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language');
+    const renderTranslate = this.props.onTranslate && status.get('translatable');
 
     const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
     const spoilerContent = { __html: status.get('spoilerHtml') };
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index d04c4a42d2..919e0fc282 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -80,7 +80,6 @@
  * @property {boolean} use_blurhash
  * @property {boolean=} use_pending_items
  * @property {string} version
- * @property {boolean} translation_enabled
  */
 
 /**
@@ -132,7 +131,6 @@ export const unfollowModal = getMeta('unfollow_modal');
 export const useBlurhash = getMeta('use_blurhash');
 export const usePendingItems = getMeta('use_pending_items');
 export const version = getMeta('version');
-export const translationEnabled = getMeta('translation_enabled');
 export const languages = initialState?.languages;
 export const statusPageUrl = getMeta('status_page_url');
 
diff --git a/app/lib/translation_service.rb b/app/lib/translation_service.rb
index 285f309393..5ff93674a4 100644
--- a/app/lib/translation_service.rb
+++ b/app/lib/translation_service.rb
@@ -21,6 +21,10 @@ class TranslationService
     ENV['DEEPL_API_KEY'].present? || ENV['LIBRE_TRANSLATE_ENDPOINT'].present?
   end
 
+  def supported?(_source_language, _target_language)
+    false
+  end
+
   def translate(_text, _source_language, _target_language)
     raise NotImplementedError
   end
diff --git a/app/lib/translation_service/deepl.rb b/app/lib/translation_service/deepl.rb
index 151d33d909..deff95a1db 100644
--- a/app/lib/translation_service/deepl.rb
+++ b/app/lib/translation_service/deepl.rb
@@ -11,33 +11,53 @@ class TranslationService::DeepL < TranslationService
   end
 
   def translate(text, source_language, target_language)
-    request(text, source_language, target_language).perform do |res|
+    form = { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' }
+    request(:post, '/v2/translate', form: form) do |res|
+      transform_response(res.body_with_limit)
+    end
+  end
+
+  def supported?(source_language, target_language)
+    source_language.in?(languages('source')) && target_language.in?(languages('target'))
+  end
+
+  private
+
+  def languages(type)
+    Rails.cache.fetch("translation_service/deepl/languages/#{type}", expires_in: 7.days, race_condition_ttl: 1.minute) do
+      request(:get, "/v2/languages?type=#{type}") do |res|
+        # In DeepL, EN and PT are deprecated in favor of EN-GB/EN-US and PT-BR/PT-PT, so
+        # they are supported but not returned by the API.
+        extra = type == 'source' ? [nil] : %w(en pt)
+        languages = Oj.load(res.body_with_limit).map { |language| language['language'].downcase }
+
+        languages + extra
+      end
+    end
+  end
+
+  def request(verb, path, **options)
+    req = Request.new(verb, "#{base_url}#{path}", **options)
+    req.add_headers(Authorization: "DeepL-Auth-Key #{@api_key}")
+    req.perform do |res|
       case res.code
       when 429
         raise TooManyRequestsError
       when 456
         raise QuotaExceededError
       when 200...300
-        transform_response(res.body_with_limit)
+        yield res
       else
         raise UnexpectedResponseError
       end
     end
   end
 
-  private
-
-  def request(text, source_language, target_language)
-    req = Request.new(:post, endpoint_url, form: { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' })
-    req.add_headers(Authorization: "DeepL-Auth-Key #{@api_key}")
-    req
-  end
-
-  def endpoint_url
+  def base_url
     if @plan == 'free'
-      'https://api-free.deepl.com/v2/translate'
+      'https://api-free.deepl.com'
     else
-      'https://api.deepl.com/v2/translate'
+      'https://api.deepl.com'
     end
   end
 
diff --git a/app/lib/translation_service/libre_translate.rb b/app/lib/translation_service/libre_translate.rb
index 4ebe21e454..743e4d77f7 100644
--- a/app/lib/translation_service/libre_translate.rb
+++ b/app/lib/translation_service/libre_translate.rb
@@ -9,29 +9,45 @@ class TranslationService::LibreTranslate < TranslationService
   end
 
   def translate(text, source_language, target_language)
-    request(text, source_language, target_language).perform do |res|
+    body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key)
+    request(:post, '/translate', body: body) do |res|
+      transform_response(res.body_with_limit, source_language)
+    end
+  end
+
+  def supported?(source_language, target_language)
+    languages.key?(source_language) && languages[source_language].include?(target_language)
+  end
+
+  private
+
+  def languages
+    Rails.cache.fetch('translation_service/libre_translate/languages', expires_in: 7.days, race_condition_ttl: 1.minute) do
+      request(:get, '/languages') do |res|
+        languages = Oj.load(res.body_with_limit).to_h { |language| [language['code'], language['targets']] }
+        languages[nil] = languages.values.flatten.uniq
+        languages
+      end
+    end
+  end
+
+  def request(verb, path, **options)
+    req = Request.new(verb, "#{@base_url}#{path}", allow_local: true, **options)
+    req.add_headers('Content-Type': 'application/json')
+    req.perform do |res|
       case res.code
       when 429
         raise TooManyRequestsError
       when 403
         raise QuotaExceededError
       when 200...300
-        transform_response(res.body_with_limit, source_language)
+        yield res
       else
         raise UnexpectedResponseError
       end
     end
   end
 
-  private
-
-  def request(text, source_language, target_language)
-    body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key)
-    req = Request.new(:post, "#{@base_url}/translate", body: body, allow_local: true)
-    req.add_headers('Content-Type': 'application/json')
-    req
-  end
-
   def transform_response(str, source_language)
     json = Oj.load(str, mode: :strict)
 
diff --git a/app/models/status.rb b/app/models/status.rb
index e7ea191a80..dd7ac2edb1 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -232,6 +232,16 @@ class Status < ApplicationRecord
     public_visibility? || unlisted_visibility?
   end
 
+  def translatable?
+    translate_target_locale = I18n.locale.to_s.split(/[_-]/).first
+
+    distributable? &&
+      content.present? &&
+      language != translate_target_locale &&
+      TranslationService.configured? &&
+      TranslationService.configured.supported?(language, translate_target_locale)
+  end
+
   alias sign? distributable?
 
   def with_media?
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 7905444e98..769ba653ed 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -30,7 +30,6 @@ class InitialStateSerializer < ActiveModel::Serializer
       timeline_preview: Setting.timeline_preview,
       activity_api_enabled: Setting.activity_api_enabled,
       single_user_mode: Rails.configuration.x.single_user_mode,
-      translation_enabled: TranslationService.configured?,
       trends_as_landing_page: Setting.trends_as_landing_page,
       status_page_url: Setting.status_page_url,
     }
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index e0b8f32a68..a422f5b258 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
   include FormattingHelper
 
   attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
-             :sensitive, :spoiler_text, :visibility, :language,
+             :sensitive, :spoiler_text, :visibility, :language, :translatable,
              :uri, :url, :replies_count, :reblogs_count,
              :favourites_count, :edited_at
 
@@ -50,6 +50,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
     object.account.user_shows_application? || (current_user? && current_user.account_id == object.account_id)
   end
 
+  def translatable
+    current_user? && object.translatable?
+  end
+
   def visibility
     # This visibility is masked behind "private"
     # to avoid API changes because there are no
diff --git a/app/services/translate_status_service.rb b/app/services/translate_status_service.rb
index 539a0d9db5..92d8b62a05 100644
--- a/app/services/translate_status_service.rb
+++ b/app/services/translate_status_service.rb
@@ -6,7 +6,7 @@ class TranslateStatusService < BaseService
   include FormattingHelper
 
   def call(status, target_language)
-    raise Mastodon::NotPermittedError unless status.public_visibility? || status.unlisted_visibility?
+    raise Mastodon::NotPermittedError unless status.translatable?
 
     @status = status
     @content = status_content_format(@status)
diff --git a/spec/lib/translation_service/deepl_spec.rb b/spec/lib/translation_service/deepl_spec.rb
new file mode 100644
index 0000000000..aa24731860
--- /dev/null
+++ b/spec/lib/translation_service/deepl_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe TranslationService::DeepL do
+  subject(:service) { described_class.new(plan, 'my-api-key') }
+
+  let(:plan) { 'advanced' }
+
+  before do
+    stub_request(:get, 'https://api.deepl.com/v2/languages?type=source').to_return(
+      body: '[{"language":"EN","name":"English"},{"language":"UK","name":"Ukrainian"}]'
+    )
+    stub_request(:get, 'https://api.deepl.com/v2/languages?type=target').to_return(
+      body: '[{"language":"EN-GB","name":"English (British)"},{"language":"ZH","name":"Chinese"}]'
+    )
+  end
+
+  describe '#supported?' do
+    it 'supports included languages as source and target languages' do
+      expect(service.supported?('uk', 'en')).to be true
+    end
+
+    it 'supports auto-detecting source language' do
+      expect(service.supported?(nil, 'en')).to be true
+    end
+
+    it 'supports "en" and "pt" as target languages though not included in language list' do
+      expect(service.supported?('uk', 'en')).to be true
+      expect(service.supported?('uk', 'pt')).to be true
+    end
+
+    it 'does not support non-included language as target language' do
+      expect(service.supported?('uk', 'nl')).to be false
+    end
+
+    it 'does not support non-included language as source language' do
+      expect(service.supported?('da', 'en')).to be false
+    end
+  end
+
+  describe '#translate' do
+    it 'returns translation with specified source language' do
+      stub_request(:post, 'https://api.deepl.com/v2/translate')
+        .with(body: 'text=Hasta+la+vista&source_lang=ES&target_lang=en&tag_handling=html')
+        .to_return(body: '{"translations":[{"detected_source_language":"ES","text":"See you soon"}]}')
+
+      translation = service.translate('Hasta la vista', 'es', 'en')
+      expect(translation.detected_source_language).to eq 'es'
+      expect(translation.provider).to eq 'DeepL.com'
+      expect(translation.text).to eq 'See you soon'
+    end
+
+    it 'returns translation with auto-detected source language' do
+      stub_request(:post, 'https://api.deepl.com/v2/translate')
+        .with(body: 'text=Guten+Tag&source_lang&target_lang=en&tag_handling=html')
+        .to_return(body: '{"translations":[{"detected_source_language":"DE","text":"Good Morning"}]}')
+
+      translation = service.translate('Guten Tag', nil, 'en')
+      expect(translation.detected_source_language).to eq 'de'
+      expect(translation.provider).to eq 'DeepL.com'
+      expect(translation.text).to eq 'Good Morning'
+    end
+  end
+
+  describe '#languages?' do
+    it 'returns source languages' do
+      expect(service.send(:languages, 'source')).to eq ['en', 'uk', nil]
+    end
+
+    it 'returns target languages' do
+      expect(service.send(:languages, 'target')).to eq %w(en-gb zh en pt)
+    end
+  end
+
+  describe '#request' do
+    before do
+      stub_request(:any, //)
+      # rubocop:disable Lint/EmptyBlock
+      service.send(:request, :get, '/v2/languages') { |res| }
+      # rubocop:enable Lint/EmptyBlock
+    end
+
+    it 'uses paid plan base URL' do
+      expect(a_request(:get, 'https://api.deepl.com/v2/languages')).to have_been_made.once
+    end
+
+    context 'with free plan' do
+      let(:plan) { 'free' }
+
+      it 'uses free plan base URL' do
+        expect(a_request(:get, 'https://api-free.deepl.com/v2/languages')).to have_been_made.once
+      end
+    end
+
+    it 'sends API key' do
+      expect(a_request(:get, 'https://api.deepl.com/v2/languages').with(headers: { Authorization: 'DeepL-Auth-Key my-api-key' })).to have_been_made.once
+    end
+  end
+end
diff --git a/spec/lib/translation_service/libre_translate_spec.rb b/spec/lib/translation_service/libre_translate_spec.rb
new file mode 100644
index 0000000000..a6cb01884a
--- /dev/null
+++ b/spec/lib/translation_service/libre_translate_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe TranslationService::LibreTranslate do
+  subject(:service) { described_class.new('https://libretranslate.example.com', 'my-api-key') }
+
+  before do
+    stub_request(:get, 'https://libretranslate.example.com/languages').to_return(
+      body: '[{"code": "en","name": "English","targets": ["de","es"]},{"code": "da","name": "Danish","targets": ["en","de"]}]'
+    )
+  end
+
+  describe '#supported?' do
+    it 'supports included language pair' do
+      expect(service.supported?('en', 'de')).to be true
+    end
+
+    it 'does not support reversed language pair' do
+      expect(service.supported?('de', 'en')).to be false
+    end
+
+    it 'supports auto-detecting source language' do
+      expect(service.supported?(nil, 'de')).to be true
+    end
+
+    it 'does not support auto-detecting for unsupported target language' do
+      expect(service.supported?(nil, 'pt')).to be false
+    end
+  end
+
+  describe '#languages' do
+    subject(:languages) { service.send(:languages) }
+
+    it 'includes supported source languages' do
+      expect(languages.keys).to eq ['en', 'da', nil]
+    end
+
+    it 'includes supported target languages for source language' do
+      expect(languages['en']).to eq %w(de es)
+    end
+
+    it 'includes supported target languages for auto-detected language' do
+      expect(languages[nil]).to eq %w(de es en)
+    end
+  end
+
+  describe '#translate' do
+    it 'returns translation with specified source language' do
+      stub_request(:post, 'https://libretranslate.example.com/translate')
+        .with(body: '{"q":"Hasta la vista","source":"es","target":"en","format":"html","api_key":"my-api-key"}')
+        .to_return(body: '{"translatedText": "See you"}')
+
+      translation = service.translate('Hasta la vista', 'es', 'en')
+      expect(translation.detected_source_language).to eq 'es'
+      expect(translation.provider).to eq 'LibreTranslate'
+      expect(translation.text).to eq 'See you'
+    end
+
+    it 'returns translation with auto-detected source language' do
+      stub_request(:post, 'https://libretranslate.example.com/translate')
+        .with(body: '{"q":"Guten Morgen","source":"auto","target":"en","format":"html","api_key":"my-api-key"}')
+        .to_return(body: '{"detectedLanguage":{"confidence":92,"language":"de"},"translatedText":"Good morning"}')
+
+      translation = service.translate('Guten Morgen', nil, 'en')
+      expect(translation.detected_source_language).to be_nil
+      expect(translation.provider).to eq 'LibreTranslate'
+      expect(translation.text).to eq 'Good morning'
+    end
+  end
+end
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 1e58c6d0d1..1f6cfc7967 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -114,6 +114,85 @@ RSpec.describe Status, type: :model do
     end
   end
 
+  describe '#translatable?' do
+    before do
+      allow(TranslationService).to receive(:configured?).and_return(true)
+      allow(TranslationService).to receive(:configured).and_return(TranslationService.new)
+      allow(TranslationService.configured).to receive(:supported?).with('es', 'en').and_return(true)
+
+      subject.language = 'es'
+      subject.visibility = :public
+    end
+
+    context 'all conditions are satisfied' do
+      it 'returns true' do
+        expect(subject.translatable?).to be true
+      end
+    end
+
+    context 'translation service is not configured' do
+      it 'returns false' do
+        allow(TranslationService).to receive(:configured?).and_return(false)
+        allow(TranslationService).to receive(:configured).and_raise(TranslationService::NotConfiguredError)
+        expect(subject.translatable?).to be false
+      end
+    end
+
+    context 'status language is nil' do
+      it 'returns true' do
+        subject.language = nil
+        allow(TranslationService.configured).to receive(:supported?).with(nil, 'en').and_return(true)
+        expect(subject.translatable?).to be true
+      end
+    end
+
+    context 'status language is same as default locale' do
+      it 'returns false' do
+        subject.language = I18n.locale
+        expect(subject.translatable?).to be false
+      end
+    end
+
+    context 'status language is unsupported' do
+      it 'returns false' do
+        subject.language = 'af'
+        allow(TranslationService.configured).to receive(:supported?).with('af', 'en').and_return(false)
+        expect(subject.translatable?).to be false
+      end
+    end
+
+    context 'default locale is unsupported' do
+      it 'returns false' do
+        allow(TranslationService.configured).to receive(:supported?).with('es', 'af').and_return(false)
+        I18n.with_locale('af') do
+          expect(subject.translatable?).to be false
+        end
+      end
+    end
+
+    context 'default locale has region' do
+      it 'returns true' do
+        I18n.with_locale('en-GB') do
+          expect(subject.translatable?).to be true
+        end
+      end
+    end
+
+    context 'status text is blank' do
+      it 'returns false' do
+        subject.text = ' '
+        expect(subject.translatable?).to be false
+      end
+    end
+
+    context 'status visiblity is hidden' do
+      it 'returns false' do
+        subject.visibility = 'limited'
+        expect(subject.translatable?).to be false
+      end
+    end
+  end
+
   describe '#content' do
     it 'returns the text of the status if it is not a reblog' do
       expect(subject.content).to eql subject.text

From ddde4e0d954fbb1338774d61f1130e4af6924496 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 21:08:22 +0100
Subject: [PATCH 15/40] Change `ActivityPub::DeliveryWorker` retries to be
 spread out more (#21956)

---
 app/workers/activitypub/delivery_worker.rb | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb
index d9153132b3..7c1c14766b 100644
--- a/app/workers/activitypub/delivery_worker.rb
+++ b/app/workers/activitypub/delivery_worker.rb
@@ -10,6 +10,16 @@ class ActivityPub::DeliveryWorker
 
   sidekiq_options queue: 'push', retry: 16, dead: false
 
+  # Unfortunately, we cannot control Sidekiq's jitter, so add our own
+  sidekiq_retry_in do |count|
+    # This is Sidekiq's default delay
+    delay  = (count**4) + 15
+    # Our custom jitter, that will be added to Sidekiq's built-in one.
+    # Sidekiq's built-in jitter is `rand(10) * (count + 1)`
+    jitter = rand(0.5 * (count**4))
+    delay + jitter
+  end
+
   HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
 
   def perform(json, source_account_id, inbox_url, options = {})

From 050f1669c6fc02d7a917261d16d9264512955bc6 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 21:13:55 +0100
Subject: [PATCH 16/40] Fix original account being unfollowed on migration
 before the follow request could be sent (#21957)

---
 app/services/follow_migration_service.rb      | 40 +++++++++++++++++++
 .../migrated_follow_delivery_worker.rb        | 17 ++++++++
 app/workers/unfollow_follow_worker.rb         |  8 +---
 3 files changed, 58 insertions(+), 7 deletions(-)
 create mode 100644 app/services/follow_migration_service.rb
 create mode 100644 app/workers/activitypub/migrated_follow_delivery_worker.rb

diff --git a/app/services/follow_migration_service.rb b/app/services/follow_migration_service.rb
new file mode 100644
index 0000000000..cfe9093cbe
--- /dev/null
+++ b/app/services/follow_migration_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class FollowMigrationService < FollowService
+  # Follow an account with the same settings as another account, and unfollow the old account once the request is sent
+  # @param [Account] source_account From which to follow
+  # @param [Account] target_account Account to follow
+  # @param [Account] old_target_account Account to unfollow once the follow request has been sent to the new one
+  # @option [Boolean] bypass_locked Whether to immediately follow the new account even if it is locked
+  def call(source_account, target_account, old_target_account, bypass_locked: false)
+    @old_target_account = old_target_account
+
+    follow    = source_account.active_relationships.find_by(target_account: old_target_account)
+    reblogs   = follow&.show_reblogs?
+    notify    = follow&.notify?
+    languages = follow&.languages
+
+    super(source_account, target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true)
+  end
+
+  private
+
+  def request_follow!
+    follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]))
+
+    if @target_account.local?
+      LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
+      UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)
+    elsif @target_account.activitypub?
+      ActivityPub::MigratedFollowDeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, @old_target_account.id)
+    end
+
+    follow_request
+  end
+
+  def direct_follow!
+    follow = super
+    UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)
+    follow
+  end
+end
diff --git a/app/workers/activitypub/migrated_follow_delivery_worker.rb b/app/workers/activitypub/migrated_follow_delivery_worker.rb
new file mode 100644
index 0000000000..17a9e515ef
--- /dev/null
+++ b/app/workers/activitypub/migrated_follow_delivery_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ActivityPub::MigratedFollowDeliveryWorker < ActivityPub::DeliveryWorker
+  def perform(json, source_account_id, inbox_url, old_target_account_id, options = {})
+    super(json, source_account_id, inbox_url, options)
+    unfollow_old_account!(old_target_account_id)
+  end
+
+  private
+
+  def unfollow_old_account!(old_target_account_id)
+    old_target_account = Account.find(old_target_account_id)
+    UnfollowService.new.call(@source_account, old_target_account, skip_unmerge: true)
+  rescue StandardError
+    true
+  end
+end
diff --git a/app/workers/unfollow_follow_worker.rb b/app/workers/unfollow_follow_worker.rb
index 7203b4888f..a4d57839de 100644
--- a/app/workers/unfollow_follow_worker.rb
+++ b/app/workers/unfollow_follow_worker.rb
@@ -10,13 +10,7 @@ class UnfollowFollowWorker
     old_target_account = Account.find(old_target_account_id)
     new_target_account = Account.find(new_target_account_id)
 
-    follow    = follower_account.active_relationships.find_by(target_account: old_target_account)
-    reblogs   = follow&.show_reblogs?
-    notify    = follow&.notify?
-    languages = follow&.languages
-
-    FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true)
-    UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true)
+    FollowMigrationService.new.call(follower_account, new_target_account, old_target_account, bypass_locked: bypass_locked)
   rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
     true
   end

From c65c34dfd11be9b307e9049fb58194c4cfa76a73 Mon Sep 17 00:00:00 2001
From: Nick Schonning <nschonni@gmail.com>
Date: Fri, 3 Mar 2023 16:48:48 -0500
Subject: [PATCH 17/40] Remove climate_control gem (#23886)

---
 Gemfile      | 1 -
 Gemfile.lock | 1 -
 2 files changed, 2 deletions(-)

diff --git a/Gemfile b/Gemfile
index 354f6bd6cf..ede81af163 100644
--- a/Gemfile
+++ b/Gemfile
@@ -119,7 +119,6 @@ end
 
 group :test do
   gem 'capybara', '~> 3.38'
-  gem 'climate_control', '~> 0.2'
   gem 'faker', '~> 3.1'
   gem 'json-schema', '~> 3.0'
   gem 'rack-test', '~> 2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 78774ac9cc..73d45fb547 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -792,7 +792,6 @@ DEPENDENCIES
   capybara (~> 3.38)
   charlock_holmes (~> 0.7.7)
   chewy (~> 7.2)
-  climate_control (~> 0.2)
   cocoon (~> 1.2)
   color_diff (~> 0.1)
   concurrent-ruby

From b00f945d92b6fff58c8a8ed702a9511ddb56a3da Mon Sep 17 00:00:00 2001
From: Nick Schonning <nschonni@gmail.com>
Date: Fri, 3 Mar 2023 16:49:16 -0500
Subject: [PATCH 18/40] Remove implied StandardError rescue (#23942)

---
 app/workers/activitypub/migrated_follow_delivery_worker.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/workers/activitypub/migrated_follow_delivery_worker.rb b/app/workers/activitypub/migrated_follow_delivery_worker.rb
index 17a9e515ef..daf30e0ae7 100644
--- a/app/workers/activitypub/migrated_follow_delivery_worker.rb
+++ b/app/workers/activitypub/migrated_follow_delivery_worker.rb
@@ -11,7 +11,7 @@ class ActivityPub::MigratedFollowDeliveryWorker < ActivityPub::DeliveryWorker
   def unfollow_old_account!(old_target_account_id)
     old_target_account = Account.find(old_target_account_id)
     UnfollowService.new.call(@source_account, old_target_account, skip_unmerge: true)
-  rescue StandardError
+  rescue
     true
   end
 end

From 1840d5d50c68a7fa85ea026cdd53c0b2c5cda83f Mon Sep 17 00:00:00 2001
From: Nick Schonning <nschonni@gmail.com>
Date: Fri, 3 Mar 2023 16:53:08 -0500
Subject: [PATCH 19/40] Remove pry gems (#23884)

---
 Gemfile      |  2 --
 Gemfile.lock | 11 -----------
 2 files changed, 13 deletions(-)

diff --git a/Gemfile b/Gemfile
index ede81af163..d3eefb224c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -104,8 +104,6 @@ group :development, :test do
   gem 'fabrication', '~> 2.30'
   gem 'fuubar', '~> 2.5'
   gem 'i18n-tasks', '~> 1.0', require: false
-  gem 'pry-byebug', '~> 3.10'
-  gem 'pry-rails', '~> 0.3'
   gem 'rspec-rails', '~> 6.0'
   gem 'rubocop-performance', require: false
   gem 'rubocop-rails', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 73d45fb547..51cf8147b0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -155,7 +155,6 @@ GEM
     bundler-audit (0.9.1)
       bundler (>= 1.2.0, < 3)
       thor (~> 1.0)
-    byebug (11.1.3)
     capistrano (3.17.2)
       airbrussh (>= 1.0.0)
       i18n
@@ -497,14 +496,6 @@ GEM
       net-smtp
       premailer (~> 1.7, >= 1.7.9)
     private_address_check (0.5.0)
-    pry (0.14.1)
-      coderay (~> 1.1)
-      method_source (~> 1.0)
-    pry-byebug (3.10.1)
-      byebug (~> 11.0)
-      pry (>= 0.13, < 0.15)
-    pry-rails (0.3.9)
-      pry (>= 0.10.4)
     public_suffix (5.0.1)
     puma (6.1.0)
       nio4r (~> 2.0)
@@ -849,8 +840,6 @@ DEPENDENCIES
   posix-spawn
   premailer-rails
   private_address_check (~> 0.5)
-  pry-byebug (~> 3.10)
-  pry-rails (~> 0.3)
   public_suffix (~> 5.0)
   puma (~> 6.1)
   pundit (~> 2.3)

From aa98c8fbeb02fecac2681464fd7c0445deb466b1 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 3 Mar 2023 22:55:43 +0100
Subject: [PATCH 20/40] Disable Style/SymbolArray (#23921)

---
 .rubocop.yml      |   3 ++
 .rubocop_todo.yml | 131 ----------------------------------------------
 2 files changed, 3 insertions(+), 131 deletions(-)

diff --git a/.rubocop.yml b/.rubocop.yml
index 0a41c54b90..f7ed79b76d 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -127,3 +127,6 @@ Style/TrailingCommaInArrayLiteral:
 
 Style/TrailingCommaInHashLiteral:
   EnforcedStyleForMultiline: 'comma'
+
+Style/SymbolArray:
+  Enabled: false
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 22e1a99c12..b53f655bd0 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -2235,134 +2235,3 @@ Style/SlicingWithRange:
     - 'lib/active_record/batches.rb'
     - 'lib/mastodon/premailer_webpack_strategy.rb'
     - 'lib/tasks/repo.rake'
-
-# Offense count: 272
-# This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: EnforcedStyle, MinSize.
-# SupportedStyles: percent, brackets
-Style/SymbolArray:
-  Exclude:
-    - 'app/controllers/accounts_controller.rb'
-    - 'app/controllers/activitypub/replies_controller.rb'
-    - 'app/controllers/admin/accounts_controller.rb'
-    - 'app/controllers/admin/announcements_controller.rb'
-    - 'app/controllers/admin/domain_blocks_controller.rb'
-    - 'app/controllers/admin/email_domain_blocks_controller.rb'
-    - 'app/controllers/admin/relationships_controller.rb'
-    - 'app/controllers/admin/relays_controller.rb'
-    - 'app/controllers/admin/roles_controller.rb'
-    - 'app/controllers/admin/rules_controller.rb'
-    - 'app/controllers/admin/statuses_controller.rb'
-    - 'app/controllers/admin/trends/statuses_controller.rb'
-    - 'app/controllers/admin/warning_presets_controller.rb'
-    - 'app/controllers/admin/webhooks_controller.rb'
-    - 'app/controllers/api/v1/accounts/credentials_controller.rb'
-    - 'app/controllers/api/v1/accounts_controller.rb'
-    - 'app/controllers/api/v1/admin/accounts_controller.rb'
-    - 'app/controllers/api/v1/admin/canonical_email_blocks_controller.rb'
-    - 'app/controllers/api/v1/admin/domain_allows_controller.rb'
-    - 'app/controllers/api/v1/admin/domain_blocks_controller.rb'
-    - 'app/controllers/api/v1/admin/email_domain_blocks_controller.rb'
-    - 'app/controllers/api/v1/admin/ip_blocks_controller.rb'
-    - 'app/controllers/api/v1/admin/reports_controller.rb'
-    - 'app/controllers/api/v1/crypto/deliveries_controller.rb'
-    - 'app/controllers/api/v1/crypto/keys/claims_controller.rb'
-    - 'app/controllers/api/v1/crypto/keys/uploads_controller.rb'
-    - 'app/controllers/api/v1/featured_tags_controller.rb'
-    - 'app/controllers/api/v1/filters_controller.rb'
-    - 'app/controllers/api/v1/lists_controller.rb'
-    - 'app/controllers/api/v1/notifications_controller.rb'
-    - 'app/controllers/api/v1/push/subscriptions_controller.rb'
-    - 'app/controllers/api/v1/scheduled_statuses_controller.rb'
-    - 'app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb'
-    - 'app/controllers/api/v1/statuses_controller.rb'
-    - 'app/controllers/api/v2/filters/keywords_controller.rb'
-    - 'app/controllers/api/v2/filters/statuses_controller.rb'
-    - 'app/controllers/api/v2/filters_controller.rb'
-    - 'app/controllers/api/web/push_subscriptions_controller.rb'
-    - 'app/controllers/application_controller.rb'
-    - 'app/controllers/auth/registrations_controller.rb'
-    - 'app/controllers/filters_controller.rb'
-    - 'app/controllers/settings/applications_controller.rb'
-    - 'app/controllers/settings/featured_tags_controller.rb'
-    - 'app/controllers/settings/profiles_controller.rb'
-    - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb'
-    - 'app/controllers/statuses_controller.rb'
-    - 'app/lib/feed_manager.rb'
-    - 'app/models/account.rb'
-    - 'app/models/account_filter.rb'
-    - 'app/models/admin/status_filter.rb'
-    - 'app/models/announcement.rb'
-    - 'app/models/concerns/ldap_authenticable.rb'
-    - 'app/models/concerns/status_threading_concern.rb'
-    - 'app/models/custom_filter.rb'
-    - 'app/models/domain_block.rb'
-    - 'app/models/import.rb'
-    - 'app/models/list.rb'
-    - 'app/models/media_attachment.rb'
-    - 'app/models/preview_card.rb'
-    - 'app/models/relay.rb'
-    - 'app/models/report.rb'
-    - 'app/models/site_upload.rb'
-    - 'app/models/status.rb'
-    - 'app/serializers/initial_state_serializer.rb'
-    - 'app/serializers/rest/notification_serializer.rb'
-    - 'db/migrate/20160220174730_create_accounts.rb'
-    - 'db/migrate/20160221003621_create_follows.rb'
-    - 'db/migrate/20160223171800_create_favourites.rb'
-    - 'db/migrate/20160224223247_create_mentions.rb'
-    - 'db/migrate/20160314164231_add_owner_to_application.rb'
-    - 'db/migrate/20160316103650_add_missing_indices.rb'
-    - 'db/migrate/20160926213048_remove_owner_from_application.rb'
-    - 'db/migrate/20161003145426_create_blocks.rb'
-    - 'db/migrate/20161006213403_rails_settings_migration.rb'
-    - 'db/migrate/20161105130633_create_statuses_tags_join_table.rb'
-    - 'db/migrate/20161119211120_create_notifications.rb'
-    - 'db/migrate/20161128103007_create_subscriptions.rb'
-    - 'db/migrate/20161222204147_create_follow_requests.rb'
-    - 'db/migrate/20170112154826_migrate_settings.rb'
-    - 'db/migrate/20170301222600_create_mutes.rb'
-    - 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb'
-    - 'db/migrate/20170424003227_create_account_domain_blocks.rb'
-    - 'db/migrate/20170427011934_re_add_owner_to_application.rb'
-    - 'db/migrate/20170507141759_optimize_index_subscriptions.rb'
-    - 'db/migrate/20170508230434_create_conversation_mutes.rb'
-    - 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb'
-    - 'db/migrate/20170823162448_create_status_pins.rb'
-    - 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb'
-    - 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb'
-    - 'db/migrate/20170917153509_create_custom_emojis.rb'
-    - 'db/migrate/20170918125918_ids_to_bigints.rb'
-    - 'db/migrate/20171116161857_create_list_accounts.rb'
-    - 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb'
-    - 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb'
-    - 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb'
-    - 'db/migrate/20171129172043_add_index_on_stream_entries.rb'
-    - 'db/migrate/20171226094803_more_faster_index_on_notifications.rb'
-    - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
-    - 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
-    - 'db/migrate/20180808175627_create_account_pins.rb'
-    - 'db/migrate/20180831171112_create_bookmarks.rb'
-    - 'db/migrate/20180929222014_create_account_conversations.rb'
-    - 'db/migrate/20181007025445_create_pghero_space_stats.rb'
-    - 'db/migrate/20181203003808_create_accounts_tags_join_table.rb'
-    - 'db/migrate/20190316190352_create_account_identity_proofs.rb'
-    - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
-    - 'db/migrate/20190820003045_update_statuses_index.rb'
-    - 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
-    - 'db/migrate/20190904222339_create_markers.rb'
-    - 'db/migrate/20200113125135_create_announcement_mutes.rb'
-    - 'db/migrate/20200114113335_create_announcement_reactions.rb'
-    - 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
-    - 'db/migrate/20200628133322_create_account_notes.rb'
-    - 'db/migrate/20200917222316_add_index_notifications_on_type.rb'
-    - 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb'
-    - 'db/migrate/20220714171049_create_tag_follows.rb'
-    - 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb'
-    - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb'
-    - 'db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb'
-    - 'spec/controllers/api/v1/streaming_controller_spec.rb'
-    - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
-    - 'spec/controllers/concerns/signature_verification_spec.rb'
-    - 'spec/fabricators/notification_fabricator.rb'
-    - 'spec/models/public_feed_spec.rb'

From 922837dc96154b0455a4cf660c3f8369c65aacb4 Mon Sep 17 00:00:00 2001
From: Jean byroot Boussier <jean.boussier+github@shopify.com>
Date: Sat, 4 Mar 2023 16:38:28 +0100
Subject: [PATCH 21/40] Upgrade to latest redis-rb 4.x and fix deprecations
 (#23616)

Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
---
 Gemfile.lock                                  |  2 +-
 app/lib/feed_manager.rb                       | 20 +++++++++----------
 .../follow_recommendation_suppression.rb      |  4 ++--
 app/services/batched_remove_status_service.rb | 18 ++++++++---------
 .../follow_recommendations_scheduler.rb       | 13 +++++-------
 config/environments/development.rb            |  2 ++
 config/environments/test.rb                   |  2 ++
 config/initializers/redis.rb                  |  1 +
 .../20170920032311_fix_reblogs_in_feeds.rb    |  2 +-
 ...00407202420_migrate_unavailable_inboxes.rb |  5 +++--
 lib/mastodon/feeds_cli.rb                     |  6 +-----
 11 files changed, 37 insertions(+), 38 deletions(-)
 create mode 100644 config/initializers/redis.rb

diff --git a/Gemfile.lock b/Gemfile.lock
index 51cf8147b0..b8b0943256 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -558,7 +558,7 @@ GEM
     rdf-normalize (0.5.1)
       rdf (~> 3.2)
     redcarpet (3.6.0)
-    redis (4.5.1)
+    redis (4.8.1)
     redis-namespace (1.10.0)
       redis (>= 4)
     redlock (1.3.2)
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 8d7540e0f7..7dda6b1853 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -273,27 +273,27 @@ class FeedManager
   def clean_feeds!(type, ids)
     reblogged_id_sets = {}
 
-    redis.pipelined do
+    redis.pipelined do |pipeline|
       ids.each do |feed_id|
-        redis.del(key(type, feed_id))
         reblog_key = key(type, feed_id, 'reblogs')
         # We collect a future for this: we don't block while getting
         # it, but we can iterate over it later.
-        reblogged_id_sets[feed_id] = redis.zrange(reblog_key, 0, -1)
-        redis.del(reblog_key)
+        reblogged_id_sets[feed_id] = pipeline.zrange(reblog_key, 0, -1)
+        pipeline.del(key(type, feed_id), reblog_key)
       end
     end
 
     # Remove all of the reblog tracking keys we just removed the
     # references to.
-    redis.pipelined do
-      reblogged_id_sets.each do |feed_id, future|
-        future.value.each do |reblogged_id|
-          reblog_set_key = key(type, feed_id, "reblogs:#{reblogged_id}")
-          redis.del(reblog_set_key)
-        end
+    keys_to_delete = reblogged_id_sets.flat_map do |feed_id, future|
+      future.value.map do |reblogged_id|
+        key(type, feed_id, "reblogs:#{reblogged_id}")
       end
     end
+
+    redis.del(keys_to_delete) unless keys_to_delete.empty?
+
+    nil
   end
 
   private
diff --git a/app/models/follow_recommendation_suppression.rb b/app/models/follow_recommendation_suppression.rb
index a9dbbfc18f..e261a2fe35 100644
--- a/app/models/follow_recommendation_suppression.rb
+++ b/app/models/follow_recommendation_suppression.rb
@@ -20,9 +20,9 @@ class FollowRecommendationSuppression < ApplicationRecord
   private
 
   def remove_follow_recommendations
-    redis.pipelined do
+    redis.pipelined do |pipeline|
       I18n.available_locales.each do |locale|
-        redis.zrem("follow_recommendations:#{locale}", account_id)
+        pipeline.zrem("follow_recommendations:#{locale}", account_id)
       end
     end
   end
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index 54e5f10a4f..7e9b671266 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -45,9 +45,9 @@ class BatchedRemoveStatusService < BaseService
 
     # Cannot be batched
     @status_id_cutoff = Mastodon::Snowflake.id_at(2.weeks.ago)
-    redis.pipelined do
+    redis.pipelined do |pipeline|
       statuses.each do |status|
-        unpush_from_public_timelines(status)
+        unpush_from_public_timelines(status, pipeline)
       end
     end
   end
@@ -70,22 +70,22 @@ class BatchedRemoveStatusService < BaseService
     end
   end
 
-  def unpush_from_public_timelines(status)
+  def unpush_from_public_timelines(status, pipeline)
     return unless status.public_visibility? && status.id > @status_id_cutoff
 
     payload = Oj.dump(event: :delete, payload: status.id.to_s)
 
-    redis.publish('timeline:public', payload)
-    redis.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload)
+    pipeline.publish('timeline:public', payload)
+    pipeline.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload)
 
     if status.media_attachments.any?
-      redis.publish('timeline:public:media', payload)
-      redis.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload)
+      pipeline.publish('timeline:public:media', payload)
+      pipeline.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload)
     end
 
     status.tags.map { |tag| tag.name.mb_chars.downcase }.each do |hashtag|
-      redis.publish("timeline:hashtag:#{hashtag}", payload)
-      redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
+      pipeline.publish("timeline:hashtag:#{hashtag}", payload)
+      pipeline.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
     end
   end
 end
diff --git a/app/workers/scheduler/follow_recommendations_scheduler.rb b/app/workers/scheduler/follow_recommendations_scheduler.rb
index 04008a9d99..17cf3f2cc3 100644
--- a/app/workers/scheduler/follow_recommendations_scheduler.rb
+++ b/app/workers/scheduler/follow_recommendations_scheduler.rb
@@ -20,7 +20,7 @@ class Scheduler::FollowRecommendationsScheduler
 
     Trends.available_locales.each do |locale|
       recommendations = if AccountSummary.safe.filtered.localized(locale).exists? # We can skip the work if no accounts with that language exist
-                          FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).map { |recommendation| [recommendation.account_id, recommendation.rank] }
+                          FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).map { |recommendation| [recommendation.rank, recommendation.account_id] }
                         else
                           []
                         end
@@ -33,14 +33,14 @@ class Scheduler::FollowRecommendationsScheduler
 
         # Language-specific results should be above language-agnostic ones,
         # otherwise language-agnostic ones will always overshadow them
-        recommendations.map! { |(account_id, rank)| [account_id, rank + max_fallback_rank] }
+        recommendations.map! { |(rank, account_id)| [rank + max_fallback_rank, account_id] }
 
         added = 0
 
         fallback_recommendations.each do |recommendation|
-          next if recommendations.any? { |(account_id, _)| account_id == recommendation.account_id }
+          next if recommendations.any? { |(_, account_id)| account_id == recommendation.account_id }
 
-          recommendations << [recommendation.account_id, recommendation.rank]
+          recommendations << [recommendation.rank, recommendation.account_id]
           added += 1
 
           break if added >= missing
@@ -49,10 +49,7 @@ class Scheduler::FollowRecommendationsScheduler
 
       redis.multi do |multi|
         multi.del(key(locale))
-
-        recommendations.each do |(account_id, rank)|
-          multi.zadd(key(locale), rank, account_id)
-        end
+        multi.zadd(key(locale), recommendations)
       end
     end
   end
diff --git a/config/environments/development.rb b/config/environments/development.rb
index de8762ff74..29b17a3500 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -87,6 +87,8 @@ Rails.application.configure do
   config.x.otp_secret = ENV.fetch('OTP_SECRET', '1fc2b87989afa6351912abeebe31ffc5c476ead9bf8b3d74cbc4a302c7b69a45b40b1bbef3506ddad73e942e15ed5ca4b402bf9a66423626051104f4b5f05109')
 end
 
+Redis.raise_deprecations = true
+
 ActiveRecordQueryTrace.enabled = ENV['QUERY_TRACE_ENABLED'] == 'true'
 
 module PrivateAddressCheck
diff --git a/config/environments/test.rb b/config/environments/test.rb
index ef3cb2e487..9cbf31e8d7 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -73,3 +73,5 @@ end
 
 # Catch serialization warnings early
 Sidekiq.strict_args!
+
+Redis.raise_deprecations = true
diff --git a/config/initializers/redis.rb b/config/initializers/redis.rb
new file mode 100644
index 0000000000..f2bbd1e456
--- /dev/null
+++ b/config/initializers/redis.rb
@@ -0,0 +1 @@
+Redis.sadd_returns_boolean = false
diff --git a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb
index 4ab68e8f32..7e2db0ff3e 100644
--- a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb
+++ b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb
@@ -1,6 +1,6 @@
 class FixReblogsInFeeds < ActiveRecord::Migration[5.1]
   def up
-    redis = Redis.current
+    redis = RedisConfiguration.pool.checkout
     fm = FeedManager.instance
 
     # Old scheme:
diff --git a/db/migrate/20200407202420_migrate_unavailable_inboxes.rb b/db/migrate/20200407202420_migrate_unavailable_inboxes.rb
index 92a3acb5d1..8f9c687942 100644
--- a/db/migrate/20200407202420_migrate_unavailable_inboxes.rb
+++ b/db/migrate/20200407202420_migrate_unavailable_inboxes.rb
@@ -2,7 +2,8 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2]
   disable_ddl_transaction!
 
   def up
-    urls = Redis.current.smembers('unavailable_inboxes')
+    redis = RedisConfiguration.pool.checkout
+    urls = redis.smembers('unavailable_inboxes')
 
     hosts = urls.map do |url|
       Addressable::URI.parse(url).normalized_host
@@ -14,7 +15,7 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2]
       UnavailableDomain.create(domain: host)
     end
 
-    Redis.current.del(*(['unavailable_inboxes'] + Redis.current.keys('exhausted_deliveries:*')))
+    redis.del(*(['unavailable_inboxes'] + redis.keys('exhausted_deliveries:*')))
   end
 
   def down; end
diff --git a/lib/mastodon/feeds_cli.rb b/lib/mastodon/feeds_cli.rb
index 428d63a446..fcfb487404 100644
--- a/lib/mastodon/feeds_cli.rb
+++ b/lib/mastodon/feeds_cli.rb
@@ -53,11 +53,7 @@ module Mastodon
     desc 'clear', 'Remove all home and list feeds from Redis'
     def clear
       keys = redis.keys('feed:*')
-
-      redis.pipelined do
-        keys.each { |key| redis.del(key) }
-      end
-
+      redis.del(keys)
       say('OK', :green)
     end
   end

From f9c2213ae5e12f14a38506c728a9598b337969cb Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 10:43:47 -0500
Subject: [PATCH 22/40] Models specs coverage (#23940)

---
 .../preview_card_provider_fabricator.rb       |  5 +++
 spec/models/account_warning_preset_spec.rb    | 17 ++++++++
 spec/models/extended_description_spec.rb      | 29 +++++++++++++
 spec/models/preview_card_provider_spec.rb     | 42 +++++++++++++++++++
 spec/models/privacy_policy_spec.rb            | 28 +++++++++++++
 5 files changed, 121 insertions(+)
 create mode 100644 spec/fabricators/preview_card_provider_fabricator.rb
 create mode 100644 spec/models/account_warning_preset_spec.rb
 create mode 100644 spec/models/extended_description_spec.rb
 create mode 100644 spec/models/preview_card_provider_spec.rb
 create mode 100644 spec/models/privacy_policy_spec.rb

diff --git a/spec/fabricators/preview_card_provider_fabricator.rb b/spec/fabricators/preview_card_provider_fabricator.rb
new file mode 100644
index 0000000000..78db710003
--- /dev/null
+++ b/spec/fabricators/preview_card_provider_fabricator.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Fabricator(:preview_card_provider) do
+  domain { Faker::Internet.domain_name }
+end
diff --git a/spec/models/account_warning_preset_spec.rb b/spec/models/account_warning_preset_spec.rb
new file mode 100644
index 0000000000..f171df7c97
--- /dev/null
+++ b/spec/models/account_warning_preset_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AccountWarningPreset do
+  describe 'alphabetical' do
+    let(:first) { Fabricate(:account_warning_preset, title: 'aaa', text: 'aaa') }
+    let(:second) { Fabricate(:account_warning_preset, title: 'bbb', text: 'aaa') }
+    let(:third) { Fabricate(:account_warning_preset, title: 'bbb', text: 'bbb') }
+
+    it 'returns records in order of title and text' do
+      results = described_class.alphabetic
+
+      expect(results).to eq([first, second, third])
+    end
+  end
+end
diff --git a/spec/models/extended_description_spec.rb b/spec/models/extended_description_spec.rb
new file mode 100644
index 0000000000..ecc27c0f6d
--- /dev/null
+++ b/spec/models/extended_description_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ExtendedDescription do
+  describe '.current' do
+    context 'with the default values' do
+      it 'makes a new instance' do
+        record = described_class.current
+
+        expect(record.text).to be_nil
+        expect(record.updated_at).to be_nil
+      end
+    end
+
+    context 'with a custom setting value' do
+      before do
+        setting = instance_double(Setting, value: 'Extended text', updated_at: 10.days.ago)
+        allow(Setting).to receive(:find_by).with(var: 'site_extended_description').and_return(setting)
+      end
+
+      it 'has the privacy text' do
+        record = described_class.current
+
+        expect(record.text).to eq('Extended text')
+      end
+    end
+  end
+end
diff --git a/spec/models/preview_card_provider_spec.rb b/spec/models/preview_card_provider_spec.rb
new file mode 100644
index 0000000000..7425b93946
--- /dev/null
+++ b/spec/models/preview_card_provider_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PreviewCardProvider do
+  describe 'scopes' do
+    let(:trendable_and_reviewed) { Fabricate(:preview_card_provider, trendable: true, reviewed_at: 5.days.ago) }
+    let(:not_trendable_and_not_reviewed) { Fabricate(:preview_card_provider, trendable: false, reviewed_at: nil) }
+
+    describe 'trendable' do
+      it 'returns the relevant records' do
+        results = described_class.trendable
+
+        expect(results).to eq([trendable_and_reviewed])
+      end
+    end
+
+    describe 'not_trendable' do
+      it 'returns the relevant records' do
+        results = described_class.not_trendable
+
+        expect(results).to eq([not_trendable_and_not_reviewed])
+      end
+    end
+
+    describe 'reviewed' do
+      it 'returns the relevant records' do
+        results = described_class.reviewed
+
+        expect(results).to eq([trendable_and_reviewed])
+      end
+    end
+
+    describe 'pending_review' do
+      it 'returns the relevant records' do
+        results = described_class.pending_review
+
+        expect(results).to eq([not_trendable_and_not_reviewed])
+      end
+    end
+  end
+end
diff --git a/spec/models/privacy_policy_spec.rb b/spec/models/privacy_policy_spec.rb
new file mode 100644
index 0000000000..0d74713755
--- /dev/null
+++ b/spec/models/privacy_policy_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PrivacyPolicy do
+  describe '.current' do
+    context 'with the default values' do
+      it 'has the privacy text' do
+        policy = described_class.current
+
+        expect(policy.text).to eq(PrivacyPolicy::DEFAULT_PRIVACY_POLICY)
+      end
+    end
+
+    context 'with a custom setting value' do
+      before do
+        terms_setting = instance_double(Setting, value: 'Terms text', updated_at: 10.days.ago)
+        allow(Setting).to receive(:find_by).with(var: 'site_terms').and_return(terms_setting)
+      end
+
+      it 'has the privacy text' do
+        policy = described_class.current
+
+        expect(policy.text).to eq('Terms text')
+      end
+    end
+  end
+end

From 39e7525c96962ad33b27d9cb4bd5fa8086060b75 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 10:56:09 -0500
Subject: [PATCH 23/40] Add basic coverage for some worker jobs (#23943)

---
 .../admin/account_deletion_worker_spec.rb     | 19 +++++++++++++++++++
 spec/workers/cache_buster_worker_spec.rb      | 19 +++++++++++++++++++
 .../poll_expiration_notify_worker_spec.rb     | 13 +++++++++++++
 .../workers/post_process_media_worker_spec.rb | 13 +++++++++++++
 spec/workers/push_conversation_worker_spec.rb | 13 +++++++++++++
 .../push_encrypted_message_worker_spec.rb     | 13 +++++++++++++
 spec/workers/push_update_worker_spec.rb       | 16 ++++++++++++++++
 spec/workers/redownload_avatar_worker_spec.rb | 13 +++++++++++++
 spec/workers/redownload_header_worker_spec.rb | 13 +++++++++++++
 .../remove_featured_tag_worker_spec.rb        | 15 +++++++++++++++
 spec/workers/resolve_account_worker_spec.rb   | 13 +++++++++++++
 .../scheduler/indexing_scheduler_spec.rb      | 13 +++++++++++++
 .../instance_refresh_scheduler_spec.rb        | 13 +++++++++++++
 .../scheduler/ip_cleanup_scheduler_spec.rb    | 13 +++++++++++++
 .../scheduler/pghero_scheduler_spec.rb        | 13 +++++++++++++
 .../scheduled_statuses_scheduler_spec.rb      | 13 +++++++++++++
 .../suspended_user_cleanup_scheduler_spec.rb  | 13 +++++++++++++
 .../trends/refresh_scheduler_spec.rb          | 13 +++++++++++++
 .../review_notifications_scheduler_spec.rb    | 13 +++++++++++++
 .../scheduler/vacuum_scheduler_spec.rb        | 13 +++++++++++++
 .../unpublish_announcement_worker_spec.rb     | 13 +++++++++++++
 spec/workers/webhooks/delivery_worker_spec.rb | 13 +++++++++++++
 22 files changed, 303 insertions(+)
 create mode 100644 spec/workers/admin/account_deletion_worker_spec.rb
 create mode 100644 spec/workers/cache_buster_worker_spec.rb
 create mode 100644 spec/workers/poll_expiration_notify_worker_spec.rb
 create mode 100644 spec/workers/post_process_media_worker_spec.rb
 create mode 100644 spec/workers/push_conversation_worker_spec.rb
 create mode 100644 spec/workers/push_encrypted_message_worker_spec.rb
 create mode 100644 spec/workers/push_update_worker_spec.rb
 create mode 100644 spec/workers/redownload_avatar_worker_spec.rb
 create mode 100644 spec/workers/redownload_header_worker_spec.rb
 create mode 100644 spec/workers/remove_featured_tag_worker_spec.rb
 create mode 100644 spec/workers/resolve_account_worker_spec.rb
 create mode 100644 spec/workers/scheduler/indexing_scheduler_spec.rb
 create mode 100644 spec/workers/scheduler/instance_refresh_scheduler_spec.rb
 create mode 100644 spec/workers/scheduler/ip_cleanup_scheduler_spec.rb
 create mode 100644 spec/workers/scheduler/pghero_scheduler_spec.rb
 create mode 100644 spec/workers/scheduler/scheduled_statuses_scheduler_spec.rb
 create mode 100644 spec/workers/scheduler/suspended_user_cleanup_scheduler_spec.rb
 create mode 100644 spec/workers/scheduler/trends/refresh_scheduler_spec.rb
 create mode 100644 spec/workers/scheduler/trends/review_notifications_scheduler_spec.rb
 create mode 100644 spec/workers/scheduler/vacuum_scheduler_spec.rb
 create mode 100644 spec/workers/unpublish_announcement_worker_spec.rb
 create mode 100644 spec/workers/webhooks/delivery_worker_spec.rb

diff --git a/spec/workers/admin/account_deletion_worker_spec.rb b/spec/workers/admin/account_deletion_worker_spec.rb
new file mode 100644
index 0000000000..631cab6648
--- /dev/null
+++ b/spec/workers/admin/account_deletion_worker_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::AccountDeletionWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    let(:account) { Fabricate(:account) }
+    let(:service) { instance_double(DeleteAccountService, call: true) }
+
+    it 'calls delete account service' do
+      allow(DeleteAccountService).to receive(:new).and_return(service)
+      worker.perform(account.id)
+
+      expect(service).to have_received(:call).with(account, { reserve_email: true, reserve_username: true })
+    end
+  end
+end
diff --git a/spec/workers/cache_buster_worker_spec.rb b/spec/workers/cache_buster_worker_spec.rb
new file mode 100644
index 0000000000..adeb287fa3
--- /dev/null
+++ b/spec/workers/cache_buster_worker_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe CacheBusterWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    let(:path) { 'https://example.com' }
+    let(:service) { instance_double(CacheBuster, bust: true) }
+
+    it 'calls the cache buster' do
+      allow(CacheBuster).to receive(:new).and_return(service)
+      worker.perform(path)
+
+      expect(service).to have_received(:bust).with(path)
+    end
+  end
+end
diff --git a/spec/workers/poll_expiration_notify_worker_spec.rb b/spec/workers/poll_expiration_notify_worker_spec.rb
new file mode 100644
index 0000000000..8229db815d
--- /dev/null
+++ b/spec/workers/poll_expiration_notify_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PollExpirationNotifyWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error for missing record' do
+      expect { worker.perform(nil) }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/post_process_media_worker_spec.rb b/spec/workers/post_process_media_worker_spec.rb
new file mode 100644
index 0000000000..33072704bf
--- /dev/null
+++ b/spec/workers/post_process_media_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PostProcessMediaWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error for missing record' do
+      expect { worker.perform(nil) }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/push_conversation_worker_spec.rb b/spec/workers/push_conversation_worker_spec.rb
new file mode 100644
index 0000000000..5fbb4c6853
--- /dev/null
+++ b/spec/workers/push_conversation_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PushConversationWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error for missing record' do
+      expect { worker.perform(nil) }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/push_encrypted_message_worker_spec.rb b/spec/workers/push_encrypted_message_worker_spec.rb
new file mode 100644
index 0000000000..3cd04ce7b4
--- /dev/null
+++ b/spec/workers/push_encrypted_message_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PushEncryptedMessageWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error for missing record' do
+      expect { worker.perform(nil) }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/push_update_worker_spec.rb b/spec/workers/push_update_worker_spec.rb
new file mode 100644
index 0000000000..c8f94fa82a
--- /dev/null
+++ b/spec/workers/push_update_worker_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PushUpdateWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error for missing record' do
+      account_id = nil
+      status_id = nil
+
+      expect { worker.perform(account_id, status_id) }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/redownload_avatar_worker_spec.rb b/spec/workers/redownload_avatar_worker_spec.rb
new file mode 100644
index 0000000000..b44ae9f035
--- /dev/null
+++ b/spec/workers/redownload_avatar_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe RedownloadAvatarWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error for missing record' do
+      expect { worker.perform(nil) }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/redownload_header_worker_spec.rb b/spec/workers/redownload_header_worker_spec.rb
new file mode 100644
index 0000000000..767ae7a5ab
--- /dev/null
+++ b/spec/workers/redownload_header_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe RedownloadHeaderWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error for missing record' do
+      expect { worker.perform(nil) }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/remove_featured_tag_worker_spec.rb b/spec/workers/remove_featured_tag_worker_spec.rb
new file mode 100644
index 0000000000..a64bd0605f
--- /dev/null
+++ b/spec/workers/remove_featured_tag_worker_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe RemoveFeaturedTagWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error for missing record' do
+      account_id = nil
+      featured_tag_id = nil
+      expect { worker.perform(account_id, featured_tag_id) }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/resolve_account_worker_spec.rb b/spec/workers/resolve_account_worker_spec.rb
new file mode 100644
index 0000000000..6f3cff099f
--- /dev/null
+++ b/spec/workers/resolve_account_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ResolveAccountWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error for missing record' do
+      expect { worker.perform(nil) }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/scheduler/indexing_scheduler_spec.rb b/spec/workers/scheduler/indexing_scheduler_spec.rb
new file mode 100644
index 0000000000..568f0fc84d
--- /dev/null
+++ b/spec/workers/scheduler/indexing_scheduler_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::IndexingScheduler do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error' do
+      expect { worker.perform }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/scheduler/instance_refresh_scheduler_spec.rb b/spec/workers/scheduler/instance_refresh_scheduler_spec.rb
new file mode 100644
index 0000000000..8f686a6998
--- /dev/null
+++ b/spec/workers/scheduler/instance_refresh_scheduler_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::InstanceRefreshScheduler do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error' do
+      expect { worker.perform }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/scheduler/ip_cleanup_scheduler_spec.rb b/spec/workers/scheduler/ip_cleanup_scheduler_spec.rb
new file mode 100644
index 0000000000..50af030117
--- /dev/null
+++ b/spec/workers/scheduler/ip_cleanup_scheduler_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::IpCleanupScheduler do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error' do
+      expect { worker.perform }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/scheduler/pghero_scheduler_spec.rb b/spec/workers/scheduler/pghero_scheduler_spec.rb
new file mode 100644
index 0000000000..e404e5fe47
--- /dev/null
+++ b/spec/workers/scheduler/pghero_scheduler_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::PgheroScheduler do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error' do
+      expect { worker.perform }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/scheduler/scheduled_statuses_scheduler_spec.rb b/spec/workers/scheduler/scheduled_statuses_scheduler_spec.rb
new file mode 100644
index 0000000000..13c853c62a
--- /dev/null
+++ b/spec/workers/scheduler/scheduled_statuses_scheduler_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::ScheduledStatusesScheduler do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error' do
+      expect { worker.perform }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/scheduler/suspended_user_cleanup_scheduler_spec.rb b/spec/workers/scheduler/suspended_user_cleanup_scheduler_spec.rb
new file mode 100644
index 0000000000..25f0e1fce4
--- /dev/null
+++ b/spec/workers/scheduler/suspended_user_cleanup_scheduler_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::SuspendedUserCleanupScheduler do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error' do
+      expect { worker.perform }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/scheduler/trends/refresh_scheduler_spec.rb b/spec/workers/scheduler/trends/refresh_scheduler_spec.rb
new file mode 100644
index 0000000000..c0c5f032bf
--- /dev/null
+++ b/spec/workers/scheduler/trends/refresh_scheduler_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::Trends::RefreshScheduler do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error' do
+      expect { worker.perform }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/scheduler/trends/review_notifications_scheduler_spec.rb b/spec/workers/scheduler/trends/review_notifications_scheduler_spec.rb
new file mode 100644
index 0000000000..cc971c24b4
--- /dev/null
+++ b/spec/workers/scheduler/trends/review_notifications_scheduler_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::Trends::ReviewNotificationsScheduler do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error' do
+      expect { worker.perform }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/scheduler/vacuum_scheduler_spec.rb b/spec/workers/scheduler/vacuum_scheduler_spec.rb
new file mode 100644
index 0000000000..36ecc93d8e
--- /dev/null
+++ b/spec/workers/scheduler/vacuum_scheduler_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::VacuumScheduler do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error' do
+      expect { worker.perform }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/unpublish_announcement_worker_spec.rb b/spec/workers/unpublish_announcement_worker_spec.rb
new file mode 100644
index 0000000000..c742c30bce
--- /dev/null
+++ b/spec/workers/unpublish_announcement_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe UnpublishAnnouncementWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error for missing record' do
+      expect { worker.perform(nil) }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/workers/webhooks/delivery_worker_spec.rb b/spec/workers/webhooks/delivery_worker_spec.rb
new file mode 100644
index 0000000000..daf8a3e285
--- /dev/null
+++ b/spec/workers/webhooks/delivery_worker_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Webhooks::DeliveryWorker do
+  let(:worker) { described_class.new }
+
+  describe 'perform' do
+    it 'runs without error' do
+      expect { worker.perform(nil, nil) }.to_not raise_error
+    end
+  end
+end

From 6a57c423169d76301078948821c1da916dcdf75b Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 10:56:43 -0500
Subject: [PATCH 24/40] Settings controllers specs (#23915)

---
 .../settings/aliases_controller_spec.rb       | 21 ++++++++++++++++++
 .../blocked_domains_controller_spec.rb        | 20 +++++++++++++++++
 .../settings/exports/lists_controller_spec.rb | 21 ++++++++++++++++++
 .../login_activities_controller_spec.rb       | 20 +++++++++++++++++
 .../migration/redirects_controller_spec.rb    | 20 +++++++++++++++++
 .../settings/pictures_controller_spec.rb      | 22 +++++++++++++++++++
 .../preferences/appearance_controller_spec.rb | 20 +++++++++++++++++
 7 files changed, 144 insertions(+)
 create mode 100644 spec/controllers/settings/aliases_controller_spec.rb
 create mode 100644 spec/controllers/settings/exports/blocked_domains_controller_spec.rb
 create mode 100644 spec/controllers/settings/exports/lists_controller_spec.rb
 create mode 100644 spec/controllers/settings/login_activities_controller_spec.rb
 create mode 100644 spec/controllers/settings/migration/redirects_controller_spec.rb
 create mode 100644 spec/controllers/settings/pictures_controller_spec.rb
 create mode 100644 spec/controllers/settings/preferences/appearance_controller_spec.rb

diff --git a/spec/controllers/settings/aliases_controller_spec.rb b/spec/controllers/settings/aliases_controller_spec.rb
new file mode 100644
index 0000000000..805f659886
--- /dev/null
+++ b/spec/controllers/settings/aliases_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Settings::AliasesController do
+  render_views
+
+  let!(:user) { Fabricate(:user) }
+  let(:account) { user.account }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/settings/exports/blocked_domains_controller_spec.rb b/spec/controllers/settings/exports/blocked_domains_controller_spec.rb
new file mode 100644
index 0000000000..ac72fd9dd7
--- /dev/null
+++ b/spec/controllers/settings/exports/blocked_domains_controller_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Settings::Exports::BlockedDomainsController do
+  render_views
+
+  describe 'GET #index' do
+    it 'returns a csv of the domains' do
+      account = Fabricate(:account, domain: 'example.com')
+      user = Fabricate(:user, account: account)
+      Fabricate(:account_domain_block, domain: 'example.com', account: account)
+
+      sign_in user, scope: :user
+      get :index, format: :csv
+
+      expect(response.body).to eq "example.com\n"
+    end
+  end
+end
diff --git a/spec/controllers/settings/exports/lists_controller_spec.rb b/spec/controllers/settings/exports/lists_controller_spec.rb
new file mode 100644
index 0000000000..29623ba499
--- /dev/null
+++ b/spec/controllers/settings/exports/lists_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Settings::Exports::ListsController do
+  render_views
+
+  describe 'GET #index' do
+    it 'returns a csv of the domains' do
+      account = Fabricate(:account)
+      user = Fabricate(:user, account: account)
+      list = Fabricate(:list, account: account, title: 'The List')
+      Fabricate(:list_account, list: list, account: account)
+
+      sign_in user, scope: :user
+      get :index, format: :csv
+
+      expect(response.body).to match 'The List'
+    end
+  end
+end
diff --git a/spec/controllers/settings/login_activities_controller_spec.rb b/spec/controllers/settings/login_activities_controller_spec.rb
new file mode 100644
index 0000000000..6f1f3de314
--- /dev/null
+++ b/spec/controllers/settings/login_activities_controller_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Settings::LoginActivitiesController do
+  render_views
+
+  let!(:user) { Fabricate(:user) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/settings/migration/redirects_controller_spec.rb b/spec/controllers/settings/migration/redirects_controller_spec.rb
new file mode 100644
index 0000000000..50d9e1927b
--- /dev/null
+++ b/spec/controllers/settings/migration/redirects_controller_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Settings::Migration::RedirectsController do
+  render_views
+
+  let!(:user) { Fabricate(:user) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #new' do
+    it 'returns http success' do
+      get :new
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/settings/pictures_controller_spec.rb b/spec/controllers/settings/pictures_controller_spec.rb
new file mode 100644
index 0000000000..2368dc55dd
--- /dev/null
+++ b/spec/controllers/settings/pictures_controller_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Settings::PicturesController do
+  render_views
+
+  let!(:user) { Fabricate(:user) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'DELETE #destroy' do
+    context 'with invalid picture id' do
+      it 'returns http bad request' do
+        delete :destroy, params: { id: 'invalid' }
+        expect(response).to have_http_status(400)
+      end
+    end
+  end
+end
diff --git a/spec/controllers/settings/preferences/appearance_controller_spec.rb b/spec/controllers/settings/preferences/appearance_controller_spec.rb
new file mode 100644
index 0000000000..7c7f716b71
--- /dev/null
+++ b/spec/controllers/settings/preferences/appearance_controller_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Settings::Preferences::AppearanceController do
+  render_views
+
+  let!(:user) { Fabricate(:user) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show
+      expect(response).to have_http_status(200)
+    end
+  end
+end

From 00eb2269b691b90f361dd03454cf823934389282 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 10:57:22 -0500
Subject: [PATCH 25/40] Policies specs (#23924)

---
 spec/policies/account_policy_spec.rb          | 40 +++++++++++++++
 .../account_warning_preset_policy_spec.rb     | 24 +++++++++
 spec/policies/admin/status_policy_spec.rb     | 51 +++++++++++++++++++
 spec/policies/announcement_policy_spec.rb     | 24 +++++++++
 spec/policies/appeal_policy_spec.rb           | 51 +++++++++++++++++++
 .../canonical_email_block_policy_spec.rb      | 24 +++++++++
 spec/policies/delivery_policy_spec.rb         | 24 +++++++++
 .../email_domain_block_policy_spec.rb         |  2 +-
 .../follow_recommendation_policy_spec.rb      | 24 +++++++++
 spec/policies/ip_block_policy_spec.rb         | 24 +++++++++
 spec/policies/preview_card_policy_spec.rb     | 24 +++++++++
 .../preview_card_provider_policy_spec.rb      | 24 +++++++++
 spec/policies/rule_policy_spec.rb             | 24 +++++++++
 spec/policies/settings_policy_spec.rb         |  2 +-
 spec/policies/status_policy_spec.rb           |  8 +++
 spec/policies/tag_policy_spec.rb              |  2 +-
 spec/policies/webhook_policy_spec.rb          | 24 +++++++++
 17 files changed, 393 insertions(+), 3 deletions(-)
 create mode 100644 spec/policies/account_warning_preset_policy_spec.rb
 create mode 100644 spec/policies/admin/status_policy_spec.rb
 create mode 100644 spec/policies/announcement_policy_spec.rb
 create mode 100644 spec/policies/appeal_policy_spec.rb
 create mode 100644 spec/policies/canonical_email_block_policy_spec.rb
 create mode 100644 spec/policies/delivery_policy_spec.rb
 create mode 100644 spec/policies/follow_recommendation_policy_spec.rb
 create mode 100644 spec/policies/ip_block_policy_spec.rb
 create mode 100644 spec/policies/preview_card_policy_spec.rb
 create mode 100644 spec/policies/preview_card_provider_policy_spec.rb
 create mode 100644 spec/policies/rule_policy_spec.rb
 create mode 100644 spec/policies/webhook_policy_spec.rb

diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb
index 0f23fd97e2..d961532332 100644
--- a/spec/policies/account_policy_spec.rb
+++ b/spec/policies/account_policy_spec.rb
@@ -116,4 +116,44 @@ RSpec.describe AccountPolicy do
       end
     end
   end
+
+  permissions :review? do
+    context 'admin' do
+      it 'permits' do
+        expect(subject).to permit(admin)
+      end
+    end
+
+    context 'not admin' do
+      it 'denies' do
+        expect(subject).to_not permit(john)
+      end
+    end
+  end
+
+  permissions :destroy? do
+    context 'admin' do
+      context 'with a temporarily suspended account' do
+        before { allow(alice).to receive(:suspended_temporarily?).and_return(true) }
+
+        it 'permits' do
+          expect(subject).to permit(admin, alice)
+        end
+      end
+
+      context 'with a not temporarily suspended account' do
+        before { allow(alice).to receive(:suspended_temporarily?).and_return(false) }
+
+        it 'denies' do
+          expect(subject).to_not permit(admin, alice)
+        end
+      end
+    end
+
+    context 'not admin' do
+      it 'denies' do
+        expect(subject).to_not permit(john, alice)
+      end
+    end
+  end
 end
diff --git a/spec/policies/account_warning_preset_policy_spec.rb b/spec/policies/account_warning_preset_policy_spec.rb
new file mode 100644
index 0000000000..63bf33de24
--- /dev/null
+++ b/spec/policies/account_warning_preset_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe AccountWarningPresetPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :index?, :create?, :update?, :destroy? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+end
diff --git a/spec/policies/admin/status_policy_spec.rb b/spec/policies/admin/status_policy_spec.rb
new file mode 100644
index 0000000000..9e81a4f5f1
--- /dev/null
+++ b/spec/policies/admin/status_policy_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe Admin::StatusPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+  let(:status) { Fabricate(:status) }
+
+  permissions :index?, :update?, :review?, :destroy? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+
+  permissions :show? do
+    context 'with an admin' do
+      context 'with a public visible status' do
+        before { allow(status).to receive(:public_visibility?).and_return(true) }
+
+        it 'permits' do
+          expect(policy).to permit(admin, status)
+        end
+      end
+
+      context 'with a not public visible status' do
+        before { allow(status).to receive(:public_visibility?).and_return(false) }
+
+        it 'denies' do
+          expect(policy).to_not permit(admin, status)
+        end
+      end
+    end
+
+    context 'with a non admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, status)
+      end
+    end
+  end
+end
diff --git a/spec/policies/announcement_policy_spec.rb b/spec/policies/announcement_policy_spec.rb
new file mode 100644
index 0000000000..3d230b3cb4
--- /dev/null
+++ b/spec/policies/announcement_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe AnnouncementPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :index?, :create?, :update?, :destroy? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+end
diff --git a/spec/policies/appeal_policy_spec.rb b/spec/policies/appeal_policy_spec.rb
new file mode 100644
index 0000000000..d7498eb9f0
--- /dev/null
+++ b/spec/policies/appeal_policy_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe AppealPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+  let(:appeal) { Fabricate(:appeal) }
+
+  permissions :index? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+
+  permissions :reject? do
+    context 'with an admin' do
+      context 'with a pending appeal' do
+        before { allow(appeal).to receive(:pending?).and_return(true) }
+
+        it 'permits' do
+          expect(policy).to permit(admin, appeal)
+        end
+      end
+
+      context 'with a not pending appeal' do
+        before { allow(appeal).to receive(:pending?).and_return(false) }
+
+        it 'denies' do
+          expect(policy).to_not permit(admin, appeal)
+        end
+      end
+    end
+
+    context 'with a non admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, appeal)
+      end
+    end
+  end
+end
diff --git a/spec/policies/canonical_email_block_policy_spec.rb b/spec/policies/canonical_email_block_policy_spec.rb
new file mode 100644
index 0000000000..0e55febfa9
--- /dev/null
+++ b/spec/policies/canonical_email_block_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe CanonicalEmailBlockPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :index?, :show?, :test?, :create?, :destroy? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+end
diff --git a/spec/policies/delivery_policy_spec.rb b/spec/policies/delivery_policy_spec.rb
new file mode 100644
index 0000000000..fbcbf390d7
--- /dev/null
+++ b/spec/policies/delivery_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe DeliveryPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :clear_delivery_errors?, :restart_delivery?, :stop_delivery? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+end
diff --git a/spec/policies/email_domain_block_policy_spec.rb b/spec/policies/email_domain_block_policy_spec.rb
index 913075c3d2..e7c455907a 100644
--- a/spec/policies/email_domain_block_policy_spec.rb
+++ b/spec/policies/email_domain_block_policy_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe EmailDomainBlockPolicy do
   let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
   let(:john)    { Fabricate(:account) }
 
-  permissions :index?, :create?, :destroy? do
+  permissions :index?, :show?, :create?, :destroy? do
     context 'admin' do
       it 'permits' do
         expect(subject).to permit(admin, EmailDomainBlock)
diff --git a/spec/policies/follow_recommendation_policy_spec.rb b/spec/policies/follow_recommendation_policy_spec.rb
new file mode 100644
index 0000000000..01f4da0be2
--- /dev/null
+++ b/spec/policies/follow_recommendation_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe FollowRecommendationPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :show?, :suppress?, :unsuppress? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+end
diff --git a/spec/policies/ip_block_policy_spec.rb b/spec/policies/ip_block_policy_spec.rb
new file mode 100644
index 0000000000..3cfa85863c
--- /dev/null
+++ b/spec/policies/ip_block_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe IpBlockPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :index?, :show?, :create?, :update?, :destroy? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+end
diff --git a/spec/policies/preview_card_policy_spec.rb b/spec/policies/preview_card_policy_spec.rb
new file mode 100644
index 0000000000..d6675c5b34
--- /dev/null
+++ b/spec/policies/preview_card_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe PreviewCardPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :index?, :review? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+end
diff --git a/spec/policies/preview_card_provider_policy_spec.rb b/spec/policies/preview_card_provider_policy_spec.rb
new file mode 100644
index 0000000000..8d3715de95
--- /dev/null
+++ b/spec/policies/preview_card_provider_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe PreviewCardProviderPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :index?, :review? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+end
diff --git a/spec/policies/rule_policy_spec.rb b/spec/policies/rule_policy_spec.rb
new file mode 100644
index 0000000000..0e45f6df02
--- /dev/null
+++ b/spec/policies/rule_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe RulePolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :index?, :create?, :update?, :destroy? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+end
diff --git a/spec/policies/settings_policy_spec.rb b/spec/policies/settings_policy_spec.rb
index e16ee51a48..3268c16225 100644
--- a/spec/policies/settings_policy_spec.rb
+++ b/spec/policies/settings_policy_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe SettingsPolicy do
   let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
   let(:john)    { Fabricate(:account) }
 
-  permissions :update?, :show? do
+  permissions :update?, :show?, :destroy? do
     context 'admin?' do
       it 'permits' do
         expect(subject).to permit(admin, Settings)
diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb
index b88521708a..9ae54780eb 100644
--- a/spec/policies/status_policy_spec.rb
+++ b/spec/policies/status_policy_spec.rb
@@ -39,6 +39,14 @@ RSpec.describe StatusPolicy, type: :model do
       expect(subject).to permit(alice, status)
     end
 
+    it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do
+      status.visibility = :direct
+      status.mentions = [Fabricate(:mention, account: bob)]
+      status.mentions.load
+
+      expect(subject).to permit(bob, status)
+    end
+
     it 'denies access when direct and viewer is not mentioned' do
       viewer = Fabricate(:account)
       status.visibility = :direct
diff --git a/spec/policies/tag_policy_spec.rb b/spec/policies/tag_policy_spec.rb
index 9be7140fc2..fb09fdd3be 100644
--- a/spec/policies/tag_policy_spec.rb
+++ b/spec/policies/tag_policy_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe TagPolicy do
   let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
   let(:john)    { Fabricate(:account) }
 
-  permissions :index?, :show?, :update? do
+  permissions :index?, :show?, :update?, :review? do
     context 'staff?' do
       it 'permits' do
         expect(subject).to permit(admin, Tag)
diff --git a/spec/policies/webhook_policy_spec.rb b/spec/policies/webhook_policy_spec.rb
new file mode 100644
index 0000000000..1eac8932d4
--- /dev/null
+++ b/spec/policies/webhook_policy_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+describe WebhookPolicy do
+  let(:policy) { described_class }
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :index?, :create?, :show?, :update?, :enable?, :disable?, :rotate_secret?, :destroy? do
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, Tag)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, Tag)
+      end
+    end
+  end
+end

From 2f606ba1220edf29e805296939f4e5612721bbf0 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 10:58:11 -0500
Subject: [PATCH 26/40] Helpers specs coverage improvement (#23937)

---
 .../account_moderation_notes_helper_spec.rb   | 10 ++-
 spec/helpers/admin/dashboard_helper_spec.rb   | 69 +++++++++++++++++++
 spec/helpers/languages_helper_spec.rb         | 48 +++++++++++--
 spec/helpers/settings_helper_spec.rb          | 37 ++++++++++
 spec/helpers/statuses_helper_spec.rb          | 62 +++++++++++++++++
 5 files changed, 216 insertions(+), 10 deletions(-)
 create mode 100644 spec/helpers/admin/dashboard_helper_spec.rb
 create mode 100644 spec/helpers/settings_helper_spec.rb

diff --git a/spec/helpers/admin/account_moderation_notes_helper_spec.rb b/spec/helpers/admin/account_moderation_notes_helper_spec.rb
index 622ce88065..e01eba51da 100644
--- a/spec/helpers/admin/account_moderation_notes_helper_spec.rb
+++ b/spec/helpers/admin/account_moderation_notes_helper_spec.rb
@@ -42,13 +42,11 @@ RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do
       let(:account) { Fabricate(:account) }
 
       it 'calls #link_to' do
-        expect(helper).to receive(:link_to).with(
-          admin_account_path(account.id),
-          class: name_tag_classes(account, true),
-          title: account.acct
-        )
+        result = helper.admin_account_inline_link_to(account)
 
-        helper.admin_account_inline_link_to(account)
+        expect(result).to match(name_tag_classes(account, true))
+        expect(result).to match(account.acct)
+        expect(result).to match(admin_account_path(account.id))
       end
     end
   end
diff --git a/spec/helpers/admin/dashboard_helper_spec.rb b/spec/helpers/admin/dashboard_helper_spec.rb
new file mode 100644
index 0000000000..59062e4839
--- /dev/null
+++ b/spec/helpers/admin/dashboard_helper_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::DashboardHelper do
+  describe 'relevant_account_timestamp' do
+    context 'with an account with older sign in' do
+      let(:account) { Fabricate(:account) }
+      let(:stamp) { 10.days.ago }
+
+      it 'returns a time element' do
+        account.user.update(current_sign_in_at: stamp)
+        result = helper.relevant_account_timestamp(account)
+
+        expect(result).to match('time-ago')
+        expect(result).to match(I18n.l(stamp))
+      end
+    end
+
+    context 'with an account with newer sign in' do
+      let(:account) { Fabricate(:account) }
+
+      it 'returns a time element' do
+        account.user.update(current_sign_in_at: 10.hours.ago)
+        result = helper.relevant_account_timestamp(account)
+
+        expect(result).to eq(I18n.t('generic.today'))
+      end
+    end
+
+    context 'with an account where the user is pending' do
+      let(:account) { Fabricate(:account) }
+
+      it 'returns a time element' do
+        account.user.update(current_sign_in_at: nil)
+        account.user.update(approved: false)
+        result = helper.relevant_account_timestamp(account)
+
+        expect(result).to match('time-ago')
+        expect(result).to match(I18n.l(account.user.created_at))
+      end
+    end
+
+    context 'with an account with a last status value' do
+      let(:account) { Fabricate(:account) }
+      let(:stamp) { 5.minutes.ago }
+
+      it 'returns a time element' do
+        account.user.update(current_sign_in_at: nil)
+        account.account_stat.update(last_status_at: stamp)
+        result = helper.relevant_account_timestamp(account)
+
+        expect(result).to match('time-ago')
+        expect(result).to match(I18n.l(stamp))
+      end
+    end
+
+    context 'with an account without sign in or last status or pending' do
+      let(:account) { Fabricate(:account) }
+
+      it 'returns a time element' do
+        account.user.update(current_sign_in_at: nil)
+        result = helper.relevant_account_timestamp(account)
+
+        expect(result).to eq('-')
+      end
+    end
+  end
+end
diff --git a/spec/helpers/languages_helper_spec.rb b/spec/helpers/languages_helper_spec.rb
index 217c9b2397..98c8064a33 100644
--- a/spec/helpers/languages_helper_spec.rb
+++ b/spec/helpers/languages_helper_spec.rb
@@ -10,14 +10,54 @@ describe LanguagesHelper do
   end
 
   describe 'native_locale_name' do
-    it 'finds the human readable native name from a key' do
-      expect(helper.native_locale_name(:de)).to eq('Deutsch')
+    context 'with a blank locale' do
+      it 'defaults to a generic value' do
+        expect(helper.native_locale_name(nil)).to eq(I18n.t('generic.none'))
+      end
+    end
+
+    context 'with a locale of `und`' do
+      it 'defaults to a generic value' do
+        expect(helper.native_locale_name('und')).to eq(I18n.t('generic.none'))
+      end
+    end
+
+    context 'with a supported locale' do
+      it 'finds the human readable native name from a key' do
+        expect(helper.native_locale_name(:de)).to eq('Deutsch')
+      end
+    end
+
+    context 'with a regional locale' do
+      it 'finds the human readable regional name from a key' do
+        expect(helper.native_locale_name('en-GB')).to eq('English (British)')
+      end
+    end
+
+    context 'with a non-existent locale' do
+      it 'returns the supplied locale value' do
+        expect(helper.native_locale_name(:xxx)).to eq(:xxx)
+      end
     end
   end
 
   describe 'standard_locale_name' do
-    it 'finds the human readable standard name from a key' do
-      expect(helper.standard_locale_name(:de)).to eq('German')
+    context 'with a blank locale' do
+      it 'defaults to a generic value' do
+        expect(helper.standard_locale_name(nil)).to eq(I18n.t('generic.none'))
+      end
+    end
+
+    context 'with a non-existent locale' do
+      it 'returns the supplied locale value' do
+        expect(helper.standard_locale_name(:xxx)).to eq(:xxx)
+      end
+    end
+
+    context 'with a supported locale' do
+      it 'finds the human readable standard name from a key' do
+        expect(helper.standard_locale_name(:de)).to eq('German')
+      end
     end
   end
 end
diff --git a/spec/helpers/settings_helper_spec.rb b/spec/helpers/settings_helper_spec.rb
new file mode 100644
index 0000000000..cba5c6ee89
--- /dev/null
+++ b/spec/helpers/settings_helper_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe SettingsHelper do
+  describe 'session_device_icon' do
+    context 'with a mobile device' do
+      let(:session) { SessionActivation.new(user_agent: 'Mozilla/5.0 (iPhone)') }
+
+      it 'detects the device and returns a descriptive string' do
+        result = helper.session_device_icon(session)
+
+        expect(result).to eq('mobile')
+      end
+    end
+
+    context 'with a tablet device' do
+      let(:session) { SessionActivation.new(user_agent: 'Mozilla/5.0 (iPad)') }
+
+      it 'detects the device and returns a descriptive string' do
+        result = helper.session_device_icon(session)
+
+        expect(result).to eq('tablet')
+      end
+    end
+
+    context 'with a desktop device' do
+      let(:session) { SessionActivation.new(user_agent: 'Mozilla/5.0 (Macintosh)') }
+
+      it 'detects the device and returns a descriptive string' do
+        result = helper.session_device_icon(session)
+
+        expect(result).to eq('desktop')
+      end
+    end
+  end
+end
diff --git a/spec/helpers/statuses_helper_spec.rb b/spec/helpers/statuses_helper_spec.rb
index ce2a4680e0..c8ca2ed323 100644
--- a/spec/helpers/statuses_helper_spec.rb
+++ b/spec/helpers/statuses_helper_spec.rb
@@ -3,6 +3,68 @@
 require 'rails_helper'
 
 RSpec.describe StatusesHelper, type: :helper do
+  describe 'link_to_newer' do
+    it 'returns a link to newer content' do
+      url = 'https://example.com'
+      result = helper.link_to_newer(url)
+
+      expect(result).to match('load-more')
+      expect(result).to match(I18n.t('statuses.show_newer'))
+    end
+  end
+
+  describe 'link_to_older' do
+    it 'returns a link to older content' do
+      url = 'https://example.com'
+      result = helper.link_to_older(url)
+
+      expect(result).to match('load-more')
+      expect(result).to match(I18n.t('statuses.show_older'))
+    end
+  end
+
+  describe 'fa_visibility_icon' do
+    context 'with a status that is public' do
+      let(:status) { Status.new(visibility: 'public') }
+
+      it 'returns the correct fa icon' do
+        result = helper.fa_visibility_icon(status)
+
+        expect(result).to match('fa-globe')
+      end
+    end
+
+    context 'with a status that is unlisted' do
+      let(:status) { Status.new(visibility: 'unlisted') }
+
+      it 'returns the correct fa icon' do
+        result = helper.fa_visibility_icon(status)
+
+        expect(result).to match('fa-unlock')
+      end
+    end
+
+    context 'with a status that is private' do
+      let(:status) { Status.new(visibility: 'private') }
+
+      it 'returns the correct fa icon' do
+        result = helper.fa_visibility_icon(status)
+
+        expect(result).to match('fa-lock')
+      end
+    end
+
+    context 'with a status that is direct' do
+      let(:status) { Status.new(visibility: 'direct') }
+
+      it 'returns the correct fa icon' do
+        result = helper.fa_visibility_icon(status)
+
+        expect(result).to match('fa-at')
+      end
+    end
+  end
+
   describe '#stream_link_target' do
     it 'returns nil if it is not an embedded view' do
       set_not_embedded_view

From c40d5e5a8fb02f2c603a23a1b0130b3f86a15710 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 11:00:00 -0500
Subject: [PATCH 27/40] Misc coverage improvements for validators (#23928)

---
 app/validators/ed25519_key_validator.rb       |  2 +-
 app/validators/ed25519_signature_validator.rb |  2 +-
 spec/models/import_spec.rb                    |  5 +++++
 spec/models/one_time_key_spec.rb              | 19 ++++++++++++++++++-
 spec/validators/email_mx_validator_spec.rb    | 16 ++++++++++++++++
 5 files changed, 41 insertions(+), 3 deletions(-)

diff --git a/app/validators/ed25519_key_validator.rb b/app/validators/ed25519_key_validator.rb
index 00a448d5a2..adf49296b2 100644
--- a/app/validators/ed25519_key_validator.rb
+++ b/app/validators/ed25519_key_validator.rb
@@ -6,7 +6,7 @@ class Ed25519KeyValidator < ActiveModel::EachValidator
 
     key = Base64.decode64(value)
 
-    record.errors[attribute] << I18n.t('crypto.errors.invalid_key') unless verified?(key)
+    record.errors.add(attribute, I18n.t('crypto.errors.invalid_key')) unless verified?(key)
   end
 
   private
diff --git a/app/validators/ed25519_signature_validator.rb b/app/validators/ed25519_signature_validator.rb
index 77a21b8373..0e74c231ec 100644
--- a/app/validators/ed25519_signature_validator.rb
+++ b/app/validators/ed25519_signature_validator.rb
@@ -8,7 +8,7 @@ class Ed25519SignatureValidator < ActiveModel::EachValidator
     signature  = Base64.decode64(value)
     message    = option_to_value(record, :message)
 
-    record.errors[attribute] << I18n.t('crypto.errors.invalid_signature') unless verified?(verify_key, signature, message)
+    record.errors.add(attribute, I18n.t('crypto.errors.invalid_signature')) unless verified?(verify_key, signature, message)
   end
 
   private
diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb
index 81c75a9641..1c84744138 100644
--- a/spec/models/import_spec.rb
+++ b/spec/models/import_spec.rb
@@ -23,6 +23,11 @@ RSpec.describe Import, type: :model do
       expect(import).to model_have_error_on_field(:data)
     end
 
+    it 'is invalid with malformed data' do
+      import = Import.create(account: account, type: type, data: StringIO.new('\"test'))
+      expect(import).to model_have_error_on_field(:data)
+    end
+
     it 'is invalid with too many rows in data' do
       import = Import.create(account: account, type: type, data: StringIO.new("foo@bar.com\n" * (ImportService::ROWS_PROCESSING_LIMIT + 10)))
       expect(import).to model_have_error_on_field(:data)
diff --git a/spec/models/one_time_key_spec.rb b/spec/models/one_time_key_spec.rb
index 2a5fe8a9d9..6ff7ffc5c1 100644
--- a/spec/models/one_time_key_spec.rb
+++ b/spec/models/one_time_key_spec.rb
@@ -2,5 +2,22 @@
 
 require 'rails_helper'
 
-RSpec.describe OneTimeKey, type: :model do
+describe OneTimeKey do
+  describe 'validations' do
+    context 'with an invalid signature' do
+      let(:one_time_key) { Fabricate.build(:one_time_key, signature: 'wrong!') }
+
+      it 'is invalid' do
+        expect(one_time_key).to_not be_valid
+      end
+    end
+
+    context 'with an invalid key' do
+      let(:one_time_key) { Fabricate.build(:one_time_key, key: 'wrong!') }
+
+      it 'is invalid' do
+        expect(one_time_key).to_not be_valid
+      end
+    end
+  end
 end
diff --git a/spec/validators/email_mx_validator_spec.rb b/spec/validators/email_mx_validator_spec.rb
index ffb6851d09..a11b8e01e0 100644
--- a/spec/validators/email_mx_validator_spec.rb
+++ b/spec/validators/email_mx_validator_spec.rb
@@ -41,6 +41,22 @@ describe EmailMxValidator do
       expect(user.errors).to_not have_received(:add)
     end
 
+    it 'adds an error if the TagManager fails to normalize domain' do
+      double = instance_double(TagManager)
+      allow(TagManager).to receive(:instance).and_return(double)
+      allow(double).to receive(:normalize_domain).with('example.com').and_raise(Addressable::URI::InvalidURIError)
+
+      user = double(email: 'foo@example.com', errors: double(add: nil))
+      subject.validate(user)
+      expect(user.errors).to have_received(:add)
+    end
+
+    it 'adds an error if the domain email portion is blank' do
+      user = double(email: 'foo@', errors: double(add: nil))
+      subject.validate(user)
+      expect(user.errors).to have_received(:add)
+    end
+
     it 'adds an error if the email domain name contains empty labels' do
       resolver = double
 

From cd99fa8cebc6efa059b4fca743d517c88f8d9686 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 11:12:09 -0500
Subject: [PATCH 28/40] Fabricator specs (#23925)

---
 spec/fabricators/account_alias_fabricator.rb        |  7 -------
 .../account_deletion_request_fabricator.rb          |  5 -----
 spec/fabricators/account_migration_fabricator.rb    |  1 +
 .../account_moderation_note_fabricator.rb           |  3 ++-
 spec/fabricators/account_pin_fabricator.rb          |  5 +++--
 spec/fabricators/account_stat_fabricator.rb         |  8 ++++----
 spec/fabricators/account_tag_stat_fabricator.rb     |  5 -----
 .../account_warning_preset_fabricator.rb            |  5 -----
 spec/fabricators/admin_action_log_fabricator.rb     |  2 +-
 spec/fabricators/announcement_mute_fabricator.rb    |  6 ------
 .../fabricators/announcement_reaction_fabricator.rb |  7 -------
 spec/fabricators/conversation_account_fabricator.rb |  8 --------
 spec/fabricators/conversation_mute_fabricator.rb    |  4 ----
 .../fabricators/custom_emoji_category_fabricator.rb |  5 -----
 spec/fabricators/encrypted_message_fabricator.rb    | 10 ----------
 spec/fabricators/featured_tag_fabricator.rb         |  8 --------
 .../follow_recommendation_suppression_fabricator.rb |  5 -----
 spec/fabricators/identity_fabricator.rb             |  2 +-
 spec/fabricators/import_fabricator.rb               |  4 ----
 spec/fabricators/ip_block_fabricator.rb             |  8 --------
 spec/fabricators/list_account_fabricator.rb         |  7 -------
 spec/fabricators/one_time_key_fabricator.rb         | 13 -------------
 spec/fabricators/setting_fabricator.rb              |  1 +
 spec/fabricators/site_upload_fabricator.rb          |  1 +
 spec/fabricators/status_edit_fabricator.rb          |  9 ---------
 spec/fabricators/status_stat_fabricator.rb          |  8 --------
 spec/fabricators/unavailable_domain_fabricator.rb   |  2 +-
 spec/fabricators/user_invite_request_fabricator.rb  |  6 ------
 spec/fabricators/web_setting_fabricator.rb          |  4 ----
 spec/fabricators_spec.rb                            | 12 ++++++++++++
 spec/models/account_spec.rb                         |  6 ------
 spec/models/block_spec.rb                           |  5 -----
 spec/models/domain_block_spec.rb                    |  5 -----
 spec/models/email_domain_block_spec.rb              |  7 -------
 spec/models/follow_spec.rb                          |  5 -----
 spec/models/mention_spec.rb                         |  5 -----
 spec/models/report_spec.rb                          |  6 ------
 37 files changed, 27 insertions(+), 183 deletions(-)
 delete mode 100644 spec/fabricators/account_alias_fabricator.rb
 delete mode 100644 spec/fabricators/account_deletion_request_fabricator.rb
 delete mode 100644 spec/fabricators/account_tag_stat_fabricator.rb
 delete mode 100644 spec/fabricators/account_warning_preset_fabricator.rb
 delete mode 100644 spec/fabricators/announcement_mute_fabricator.rb
 delete mode 100644 spec/fabricators/announcement_reaction_fabricator.rb
 delete mode 100644 spec/fabricators/conversation_account_fabricator.rb
 delete mode 100644 spec/fabricators/conversation_mute_fabricator.rb
 delete mode 100644 spec/fabricators/custom_emoji_category_fabricator.rb
 delete mode 100644 spec/fabricators/encrypted_message_fabricator.rb
 delete mode 100644 spec/fabricators/featured_tag_fabricator.rb
 delete mode 100644 spec/fabricators/follow_recommendation_suppression_fabricator.rb
 delete mode 100644 spec/fabricators/import_fabricator.rb
 delete mode 100644 spec/fabricators/ip_block_fabricator.rb
 delete mode 100644 spec/fabricators/list_account_fabricator.rb
 delete mode 100644 spec/fabricators/one_time_key_fabricator.rb
 delete mode 100644 spec/fabricators/status_edit_fabricator.rb
 delete mode 100644 spec/fabricators/status_stat_fabricator.rb
 delete mode 100644 spec/fabricators/user_invite_request_fabricator.rb
 delete mode 100644 spec/fabricators/web_setting_fabricator.rb
 create mode 100644 spec/fabricators_spec.rb

diff --git a/spec/fabricators/account_alias_fabricator.rb b/spec/fabricators/account_alias_fabricator.rb
deleted file mode 100644
index 4f434c078a..0000000000
--- a/spec/fabricators/account_alias_fabricator.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:account_alias) do
-  account
-  acct 'test@example.com'
-  uri 'https://example.com/users/test'
-end
diff --git a/spec/fabricators/account_deletion_request_fabricator.rb b/spec/fabricators/account_deletion_request_fabricator.rb
deleted file mode 100644
index 3d3d373988..0000000000
--- a/spec/fabricators/account_deletion_request_fabricator.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:account_deletion_request) do
-  account
-end
diff --git a/spec/fabricators/account_migration_fabricator.rb b/spec/fabricators/account_migration_fabricator.rb
index fd453f6d2a..ae6143a65c 100644
--- a/spec/fabricators/account_migration_fabricator.rb
+++ b/spec/fabricators/account_migration_fabricator.rb
@@ -5,4 +5,5 @@ Fabricator(:account_migration) do
   target_account { |attrs| Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(attrs[:account])]) }
   acct           { |attrs| attrs[:target_account].acct }
   followers_count 1234
+  created_at { 60.days.ago }
 end
diff --git a/spec/fabricators/account_moderation_note_fabricator.rb b/spec/fabricators/account_moderation_note_fabricator.rb
index 403870db6d..341a24dea0 100644
--- a/spec/fabricators/account_moderation_note_fabricator.rb
+++ b/spec/fabricators/account_moderation_note_fabricator.rb
@@ -2,5 +2,6 @@
 
 Fabricator(:account_moderation_note) do
   content 'MyText'
-  account nil
+  account
+  target_account { Fabricate(:account) }
 end
diff --git a/spec/fabricators/account_pin_fabricator.rb b/spec/fabricators/account_pin_fabricator.rb
index 7d8a77bb54..32a5f3bdb8 100644
--- a/spec/fabricators/account_pin_fabricator.rb
+++ b/spec/fabricators/account_pin_fabricator.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 Fabricator(:account_pin) do
-  account        nil
-  target_account nil
+  account
+  target_account(fabricator: :account)
+  before_create { |account_pin, _| account_pin.account.follow!(account_pin.target_account) }
 end
diff --git a/spec/fabricators/account_stat_fabricator.rb b/spec/fabricators/account_stat_fabricator.rb
index 45b1524ef3..e6085c5f2b 100644
--- a/spec/fabricators/account_stat_fabricator.rb
+++ b/spec/fabricators/account_stat_fabricator.rb
@@ -1,8 +1,8 @@
 # frozen_string_literal: true
 
 Fabricator(:account_stat) do
-  account         nil
-  statuses_count  ''
-  following_count ''
-  followers_count ''
+  account
+  statuses_count  '123'
+  following_count '456'
+  followers_count '789'
 end
diff --git a/spec/fabricators/account_tag_stat_fabricator.rb b/spec/fabricators/account_tag_stat_fabricator.rb
deleted file mode 100644
index 769015bd02..0000000000
--- a/spec/fabricators/account_tag_stat_fabricator.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:account_tag_stat) do
-  accounts_count ''
-end
diff --git a/spec/fabricators/account_warning_preset_fabricator.rb b/spec/fabricators/account_warning_preset_fabricator.rb
deleted file mode 100644
index 7588e7f9cf..0000000000
--- a/spec/fabricators/account_warning_preset_fabricator.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:account_warning_preset) do
-  text 'MyText'
-end
diff --git a/spec/fabricators/admin_action_log_fabricator.rb b/spec/fabricators/admin_action_log_fabricator.rb
index eb738c01c0..a259644bdc 100644
--- a/spec/fabricators/admin_action_log_fabricator.rb
+++ b/spec/fabricators/admin_action_log_fabricator.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 Fabricator('Admin::ActionLog') do
-  account nil
+  account
   action  'MyString'
   target  nil
 end
diff --git a/spec/fabricators/announcement_mute_fabricator.rb b/spec/fabricators/announcement_mute_fabricator.rb
deleted file mode 100644
index 109fec0412..0000000000
--- a/spec/fabricators/announcement_mute_fabricator.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:announcement_mute) do
-  account
-  announcement
-end
diff --git a/spec/fabricators/announcement_reaction_fabricator.rb b/spec/fabricators/announcement_reaction_fabricator.rb
deleted file mode 100644
index 5da51caaa3..0000000000
--- a/spec/fabricators/announcement_reaction_fabricator.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:announcement_reaction) do
-  account
-  announcement
-  name '🌿'
-end
diff --git a/spec/fabricators/conversation_account_fabricator.rb b/spec/fabricators/conversation_account_fabricator.rb
deleted file mode 100644
index f69d36855e..0000000000
--- a/spec/fabricators/conversation_account_fabricator.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:conversation_account) do
-  account                 nil
-  conversation            nil
-  participant_account_ids ''
-  last_status             nil
-end
diff --git a/spec/fabricators/conversation_mute_fabricator.rb b/spec/fabricators/conversation_mute_fabricator.rb
deleted file mode 100644
index 5cf4dd3d59..0000000000
--- a/spec/fabricators/conversation_mute_fabricator.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:conversation_mute) do
-end
diff --git a/spec/fabricators/custom_emoji_category_fabricator.rb b/spec/fabricators/custom_emoji_category_fabricator.rb
deleted file mode 100644
index 6019baba21..0000000000
--- a/spec/fabricators/custom_emoji_category_fabricator.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:custom_emoji_category) do
-  name 'MyString'
-end
diff --git a/spec/fabricators/encrypted_message_fabricator.rb b/spec/fabricators/encrypted_message_fabricator.rb
deleted file mode 100644
index 2898827549..0000000000
--- a/spec/fabricators/encrypted_message_fabricator.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:encrypted_message) do
-  device
-  from_account
-  from_device_id   { Faker::Number.number(digits: 5) }
-  type             0
-  body             ''
-  message_franking ''
-end
diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb
deleted file mode 100644
index 4bfa3e924e..0000000000
--- a/spec/fabricators/featured_tag_fabricator.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:featured_tag) do
-  account
-  tag
-  statuses_count 1_337
-  last_status_at Time.now.utc
-end
diff --git a/spec/fabricators/follow_recommendation_suppression_fabricator.rb b/spec/fabricators/follow_recommendation_suppression_fabricator.rb
deleted file mode 100644
index 6477baee11..0000000000
--- a/spec/fabricators/follow_recommendation_suppression_fabricator.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:follow_recommendation_suppression) do
-  account
-end
diff --git a/spec/fabricators/identity_fabricator.rb b/spec/fabricators/identity_fabricator.rb
index b830101117..58072c0d65 100644
--- a/spec/fabricators/identity_fabricator.rb
+++ b/spec/fabricators/identity_fabricator.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 Fabricator(:identity) do
-  user     nil
+  user
   provider 'MyString'
   uid      'MyString'
 end
diff --git a/spec/fabricators/import_fabricator.rb b/spec/fabricators/import_fabricator.rb
deleted file mode 100644
index 11602f407f..0000000000
--- a/spec/fabricators/import_fabricator.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:import) do
-end
diff --git a/spec/fabricators/ip_block_fabricator.rb b/spec/fabricators/ip_block_fabricator.rb
deleted file mode 100644
index a5da3f7065..0000000000
--- a/spec/fabricators/ip_block_fabricator.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:ip_block) do
-  ip         ''
-  severity   ''
-  expires_at '2020-10-08 22:20:37'
-  comment    'MyText'
-end
diff --git a/spec/fabricators/list_account_fabricator.rb b/spec/fabricators/list_account_fabricator.rb
deleted file mode 100644
index b0af29e6fa..0000000000
--- a/spec/fabricators/list_account_fabricator.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:list_account) do
-  list    nil
-  account nil
-  follow  nil
-end
diff --git a/spec/fabricators/one_time_key_fabricator.rb b/spec/fabricators/one_time_key_fabricator.rb
deleted file mode 100644
index e317c28bd1..0000000000
--- a/spec/fabricators/one_time_key_fabricator.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:one_time_key) do
-  device
-  key_id { Faker::Alphanumeric.alphanumeric(number: 10) }
-  key    { Base64.strict_encode64(Ed25519::SigningKey.generate.verify_key.to_bytes) }
-
-  signature do |attrs|
-    signing_key = Ed25519::SigningKey.generate
-    attrs[:device].update(fingerprint_key: Base64.strict_encode64(signing_key.verify_key.to_bytes))
-    Base64.strict_encode64(signing_key.sign(attrs[:key]))
-  end
-end
diff --git a/spec/fabricators/setting_fabricator.rb b/spec/fabricators/setting_fabricator.rb
index 336d7c3551..ce9a48e901 100644
--- a/spec/fabricators/setting_fabricator.rb
+++ b/spec/fabricators/setting_fabricator.rb
@@ -1,4 +1,5 @@
 # frozen_string_literal: true
 
 Fabricator(:setting) do
+  var 'var'
 end
diff --git a/spec/fabricators/site_upload_fabricator.rb b/spec/fabricators/site_upload_fabricator.rb
index ad1b777cc4..87553ccb8a 100644
--- a/spec/fabricators/site_upload_fabricator.rb
+++ b/spec/fabricators/site_upload_fabricator.rb
@@ -2,4 +2,5 @@
 
 Fabricator(:site_upload) do
   file { Rails.root.join('spec', 'fabricators', 'assets', 'utah_teapot.png').open }
+  var 'thumbnail'
 end
diff --git a/spec/fabricators/status_edit_fabricator.rb b/spec/fabricators/status_edit_fabricator.rb
deleted file mode 100644
index 33735a4592..0000000000
--- a/spec/fabricators/status_edit_fabricator.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:status_edit) do
-  status                    nil
-  account                   nil
-  text                      'MyText'
-  spoiler_text              'MyText'
-  media_attachments_changed false
-end
diff --git a/spec/fabricators/status_stat_fabricator.rb b/spec/fabricators/status_stat_fabricator.rb
deleted file mode 100644
index 8a358c51a9..0000000000
--- a/spec/fabricators/status_stat_fabricator.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:status_stat) do
-  status_id        nil
-  replies_count    ''
-  reblogs_count    ''
-  favourites_count ''
-end
diff --git a/spec/fabricators/unavailable_domain_fabricator.rb b/spec/fabricators/unavailable_domain_fabricator.rb
index 300a9e7a14..cb9707020a 100644
--- a/spec/fabricators/unavailable_domain_fabricator.rb
+++ b/spec/fabricators/unavailable_domain_fabricator.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 Fabricator(:unavailable_domain) do
-  domain { Faker::Internet.domain }
+  domain { Faker::Internet.domain_name }
 end
diff --git a/spec/fabricators/user_invite_request_fabricator.rb b/spec/fabricators/user_invite_request_fabricator.rb
deleted file mode 100644
index 7736263e45..0000000000
--- a/spec/fabricators/user_invite_request_fabricator.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:user_invite_request) do
-  user
-  text { Faker::Lorem.sentence }
-end
diff --git a/spec/fabricators/web_setting_fabricator.rb b/spec/fabricators/web_setting_fabricator.rb
deleted file mode 100644
index 7c9f300798..0000000000
--- a/spec/fabricators/web_setting_fabricator.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:web_setting, from: Web::Setting) do
-end
diff --git a/spec/fabricators_spec.rb b/spec/fabricators_spec.rb
new file mode 100644
index 0000000000..3b76c56ce6
--- /dev/null
+++ b/spec/fabricators_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+Fabrication.manager.load_definitions if Fabrication.manager.empty?
+
+Fabrication.manager.schematics.map(&:first).each do |factory_name|
+  describe "The #{factory_name} factory" do
+    it 'is valid' do
+      factory = Fabricate(factory_name)
+      expect(factory).to be_valid
+    end
+  end
+end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 1e5a80963f..ae4e5ee321 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -704,12 +704,6 @@ RSpec.describe Account, type: :model do
   end
 
   describe 'validations' do
-    it 'has a valid fabricator' do
-      account = Fabricate.build(:account)
-      account.valid?
-      expect(account).to be_valid
-    end
-
     it 'is invalid without a username' do
       account = Fabricate.build(:account, username: nil)
       account.valid?
diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb
index 64c39fce60..6e31786d04 100644
--- a/spec/models/block_spec.rb
+++ b/spec/models/block_spec.rb
@@ -4,11 +4,6 @@ require 'rails_helper'
 
 RSpec.describe Block, type: :model do
   describe 'validations' do
-    it 'has a valid fabricator' do
-      block = Fabricate.build(:block)
-      expect(block).to be_valid
-    end
-
     it 'is invalid without an account' do
       block = Fabricate.build(:block, account: nil)
       block.valid?
diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb
index 6a5925b896..9839ee9d4e 100644
--- a/spec/models/domain_block_spec.rb
+++ b/spec/models/domain_block_spec.rb
@@ -4,11 +4,6 @@ require 'rails_helper'
 
 RSpec.describe DomainBlock, type: :model do
   describe 'validations' do
-    it 'has a valid fabricator' do
-      domain_block = Fabricate.build(:domain_block)
-      expect(domain_block).to be_valid
-    end
-
     it 'is invalid without a domain' do
       domain_block = Fabricate.build(:domain_block, domain: nil)
       domain_block.valid?
diff --git a/spec/models/email_domain_block_spec.rb b/spec/models/email_domain_block_spec.rb
index 01a7a0f0ed..3321ffc819 100644
--- a/spec/models/email_domain_block_spec.rb
+++ b/spec/models/email_domain_block_spec.rb
@@ -3,13 +3,6 @@
 require 'rails_helper'
 
 RSpec.describe EmailDomainBlock, type: :model do
-  describe 'validations' do
-    it 'has a valid fabricator' do
-      email_domain_block = Fabricate.build(:email_domain_block)
-      expect(email_domain_block).to be_valid
-    end
-  end
-
   describe 'block?' do
     let(:input) { nil }
 
diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb
index f49d585329..a9a9af88ad 100644
--- a/spec/models/follow_spec.rb
+++ b/spec/models/follow_spec.rb
@@ -9,11 +9,6 @@ RSpec.describe Follow, type: :model do
   describe 'validations' do
     subject { Follow.new(account: alice, target_account: bob, rate_limit: true) }
 
-    it 'has a valid fabricator' do
-      follow = Fabricate.build(:follow)
-      expect(follow).to be_valid
-    end
-
     it 'is invalid without an account' do
       follow = Fabricate.build(:follow, account: nil)
       follow.valid?
diff --git a/spec/models/mention_spec.rb b/spec/models/mention_spec.rb
index 3de2b4a072..044bb80cf6 100644
--- a/spec/models/mention_spec.rb
+++ b/spec/models/mention_spec.rb
@@ -4,11 +4,6 @@ require 'rails_helper'
 
 RSpec.describe Mention, type: :model do
   describe 'validations' do
-    it 'has a valid fabricator' do
-      mention = Fabricate.build(:mention)
-      expect(mention).to be_valid
-    end
-
     it 'is invalid without an account' do
       mention = Fabricate.build(:mention, account: nil)
       mention.valid?
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
index d5d40a34f9..20a048c334 100644
--- a/spec/models/report_spec.rb
+++ b/spec/models/report_spec.rb
@@ -121,12 +121,6 @@ describe Report do
   end
 
   describe 'validations' do
-    it 'has a valid fabricator' do
-      report = Fabricate(:report)
-      report.valid?
-      expect(report).to be_valid
-    end
-
     it 'is invalid if comment is longer than 1000 characters' do
       report = Fabricate.build(:report, comment: Faker::Lorem.characters(number: 1001))
       report.valid?

From ad585fb1959abe38839c30137cf3343737f1d247 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 11:12:54 -0500
Subject: [PATCH 29/40] Specs api v1 controllers (#23930)

---
 .../familiar_followers_controller_spec.rb     | 23 +++++++++++++
 .../accounts/featured_tags_controller_spec.rb | 23 +++++++++++++
 .../identity_proofs_controller_spec.rb        | 23 +++++++++++++
 .../api/v1/accounts/lookup_controller_spec.rb | 23 +++++++++++++
 .../canonical_email_blocks_controller_spec.rb | 23 +++++++++++++
 .../v1/admin/dimensions_controller_spec.rb    | 23 +++++++++++++
 .../email_domain_blocks_controller_spec.rb    | 23 +++++++++++++
 .../api/v1/admin/ip_blocks_controller_spec.rb | 23 +++++++++++++
 .../api/v1/admin/measures_controller_spec.rb  | 23 +++++++++++++
 .../api/v1/admin/retention_controller_spec.rb | 23 +++++++++++++
 .../v1/admin/trends/links_controller_spec.rb  | 23 +++++++++++++
 .../admin/trends/statuses_controller_spec.rb  | 23 +++++++++++++
 .../v1/admin/trends/tags_controller_spec.rb   | 23 +++++++++++++
 .../api/v1/directories_controller_spec.rb     | 23 +++++++++++++
 .../suggestions_controller_spec.rb            | 23 +++++++++++++
 .../api/v1/featured_tags_controller_spec.rb   | 23 +++++++++++++
 .../domain_blocks_controller_spec.rb          | 16 ++++++++++
 .../extended_descriptions_controller_spec.rb  | 15 +++++++++
 .../privacy_policies_controller_spec.rb       | 15 +++++++++
 .../api/v1/instances/rules_controller_spec.rb | 15 +++++++++
 .../api/v1/preferences_controller_spec.rb     | 23 +++++++++++++
 .../v1/scheduled_statuses_controller_spec.rb  | 23 +++++++++++++
 .../statuses/translations_controller_spec.rb  | 32 +++++++++++++++++++
 .../api/v1/trends/links_controller_spec.rb    | 15 +++++++++
 .../api/v1/trends/statuses_controller_spec.rb | 15 +++++++++
 25 files changed, 537 insertions(+)
 create mode 100644 spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/accounts/lookup_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/admin/dimensions_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/admin/measures_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/admin/retention_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/admin/trends/links_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/admin/trends/tags_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/directories_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/featured_tags_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/instances/domain_blocks_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/instances/extended_descriptions_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/instances/privacy_policies_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/instances/rules_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/preferences_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/scheduled_statuses_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/statuses/translations_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/trends/links_controller_spec.rb
 create mode 100644 spec/controllers/api/v1/trends/statuses_controller_spec.rb

diff --git a/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb b/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb
new file mode 100644
index 0000000000..bb075261f3
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Accounts::FamiliarFollowersController do
+  render_views
+
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb b/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb
new file mode 100644
index 0000000000..53ac1e2a7a
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Accounts::FeaturedTagsController do
+  render_views
+
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb b/spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb
new file mode 100644
index 0000000000..6351de7616
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Accounts::IdentityProofsController do
+  render_views
+
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/accounts/lookup_controller_spec.rb b/spec/controllers/api/v1/accounts/lookup_controller_spec.rb
new file mode 100644
index 0000000000..37407766f2
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/lookup_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Accounts::LookupController do
+  render_views
+
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show, params: { account_id: account.id, acct: account.acct }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb
new file mode 100644
index 0000000000..3acae843ad
--- /dev/null
+++ b/spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Admin::CanonicalEmailBlocksController do
+  render_views
+
+  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/dimensions_controller_spec.rb b/spec/controllers/api/v1/admin/dimensions_controller_spec.rb
new file mode 100644
index 0000000000..ea18efe383
--- /dev/null
+++ b/spec/controllers/api/v1/admin/dimensions_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Admin::DimensionsController do
+  render_views
+
+  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'POST #create' do
+    it 'returns http success' do
+      post :create, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb
new file mode 100644
index 0000000000..a92a298699
--- /dev/null
+++ b/spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Admin::EmailDomainBlocksController do
+  render_views
+
+  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb
new file mode 100644
index 0000000000..50e2ae9687
--- /dev/null
+++ b/spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Admin::IpBlocksController do
+  render_views
+
+  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/measures_controller_spec.rb b/spec/controllers/api/v1/admin/measures_controller_spec.rb
new file mode 100644
index 0000000000..03727a6329
--- /dev/null
+++ b/spec/controllers/api/v1/admin/measures_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Admin::MeasuresController do
+  render_views
+
+  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'POST #create' do
+    it 'returns http success' do
+      post :create, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/retention_controller_spec.rb b/spec/controllers/api/v1/admin/retention_controller_spec.rb
new file mode 100644
index 0000000000..2381dbcb48
--- /dev/null
+++ b/spec/controllers/api/v1/admin/retention_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Admin::RetentionController do
+  render_views
+
+  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'POST #create' do
+    it 'returns http success' do
+      post :create, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/trends/links_controller_spec.rb b/spec/controllers/api/v1/admin/trends/links_controller_spec.rb
new file mode 100644
index 0000000000..a64292f067
--- /dev/null
+++ b/spec/controllers/api/v1/admin/trends/links_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Admin::Trends::LinksController do
+  render_views
+
+  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb b/spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb
new file mode 100644
index 0000000000..821cc499f4
--- /dev/null
+++ b/spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Admin::Trends::StatusesController do
+  render_views
+
+  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/admin/trends/tags_controller_spec.rb b/spec/controllers/api/v1/admin/trends/tags_controller_spec.rb
new file mode 100644
index 0000000000..480306ce7e
--- /dev/null
+++ b/spec/controllers/api/v1/admin/trends/tags_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Admin::Trends::TagsController do
+  render_views
+
+  let(:user)    { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/directories_controller_spec.rb b/spec/controllers/api/v1/directories_controller_spec.rb
new file mode 100644
index 0000000000..b18aedc4d1
--- /dev/null
+++ b/spec/controllers/api/v1/directories_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::DirectoriesController do
+  render_views
+
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb b/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb
new file mode 100644
index 0000000000..54c63dcc6f
--- /dev/null
+++ b/spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::FeaturedTags::SuggestionsController do
+  render_views
+
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/featured_tags_controller_spec.rb b/spec/controllers/api/v1/featured_tags_controller_spec.rb
new file mode 100644
index 0000000000..aac9429015
--- /dev/null
+++ b/spec/controllers/api/v1/featured_tags_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::FeaturedTagsController do
+  render_views
+
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index, params: { account_id: account.id, limit: 2 }
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/instances/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/instances/domain_blocks_controller_spec.rb
new file mode 100644
index 0000000000..08f505c3d4
--- /dev/null
+++ b/spec/controllers/api/v1/instances/domain_blocks_controller_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Instances::DomainBlocksController do
+  render_views
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      Setting.show_domain_blocks = 'all'
+      get :index
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/instances/extended_descriptions_controller_spec.rb b/spec/controllers/api/v1/instances/extended_descriptions_controller_spec.rb
new file mode 100644
index 0000000000..58c0d4b8f1
--- /dev/null
+++ b/spec/controllers/api/v1/instances/extended_descriptions_controller_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Instances::ExtendedDescriptionsController do
+  render_views
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/instances/privacy_policies_controller_spec.rb b/spec/controllers/api/v1/instances/privacy_policies_controller_spec.rb
new file mode 100644
index 0000000000..ac0bed9dc6
--- /dev/null
+++ b/spec/controllers/api/v1/instances/privacy_policies_controller_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Instances::PrivacyPoliciesController do
+  render_views
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/instances/rules_controller_spec.rb b/spec/controllers/api/v1/instances/rules_controller_spec.rb
new file mode 100644
index 0000000000..5af50239b0
--- /dev/null
+++ b/spec/controllers/api/v1/instances/rules_controller_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Instances::RulesController do
+  render_views
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/preferences_controller_spec.rb b/spec/controllers/api/v1/preferences_controller_spec.rb
new file mode 100644
index 0000000000..79cc3066e1
--- /dev/null
+++ b/spec/controllers/api/v1/preferences_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::PreferencesController do
+  render_views
+
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/scheduled_statuses_controller_spec.rb b/spec/controllers/api/v1/scheduled_statuses_controller_spec.rb
new file mode 100644
index 0000000000..256c4b272a
--- /dev/null
+++ b/spec/controllers/api/v1/scheduled_statuses_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::ScheduledStatusesController do
+  render_views
+
+  let(:user)    { Fabricate(:user) }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') }
+  let(:account) { Fabricate(:account) }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/statuses/translations_controller_spec.rb b/spec/controllers/api/v1/statuses/translations_controller_spec.rb
new file mode 100644
index 0000000000..9575ac448d
--- /dev/null
+++ b/spec/controllers/api/v1/statuses/translations_controller_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Statuses::TranslationsController do
+  render_views
+
+  let(:user)  { Fabricate(:user) }
+  let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
+  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses', application: app) }
+
+  context 'with an oauth token' do
+    before do
+      allow(controller).to receive(:doorkeeper_token) { token }
+    end
+
+    describe 'POST #create' do
+      let(:status) { Fabricate(:status, account: user.account) }
+
+      before do
+        translation = TranslationService::Translation.new(text: 'Hello')
+        service = instance_double(TranslationService::DeepL, translate: translation)
+        allow(TranslationService).to receive(:configured).and_return(service)
+        post :create, params: { status_id: status.id }
+      end
+
+      it 'returns http success' do
+        expect(response).to have_http_status(200)
+      end
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/trends/links_controller_spec.rb b/spec/controllers/api/v1/trends/links_controller_spec.rb
new file mode 100644
index 0000000000..71a7e2e477
--- /dev/null
+++ b/spec/controllers/api/v1/trends/links_controller_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Trends::LinksController do
+  render_views
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/trends/statuses_controller_spec.rb b/spec/controllers/api/v1/trends/statuses_controller_spec.rb
new file mode 100644
index 0000000000..e9892bb140
--- /dev/null
+++ b/spec/controllers/api/v1/trends/statuses_controller_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Api::V1::Trends::StatusesController do
+  render_views
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(200)
+    end
+  end
+end

From 42ddc451338b7902318ab081218319d56d7150eb Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 11:13:28 -0500
Subject: [PATCH 30/40] Admin controllers specs (#23917)

---
 .../admin/account_actions_controller_spec.rb  | 23 +++++++++++++++++++
 .../admin/announcements_controller_spec.rb    | 21 +++++++++++++++++
 .../follow_recommendations_controller_spec.rb | 21 +++++++++++++++++
 .../admin/ip_blocks_controller_spec.rb        | 21 +++++++++++++++++
 .../admin/relationships_controller_spec.rb    | 23 +++++++++++++++++++
 .../admin/relays_controller_spec.rb           | 21 +++++++++++++++++
 .../admin/rules_controller_spec.rb            | 21 +++++++++++++++++
 .../admin/settings/about_controller_spec.rb   | 21 +++++++++++++++++
 .../settings/appearance_controller_spec.rb    | 21 +++++++++++++++++
 .../content_retention_controller_spec.rb      | 21 +++++++++++++++++
 .../settings/discovery_controller_spec.rb     | 21 +++++++++++++++++
 .../settings/registrations_controller_spec.rb | 21 +++++++++++++++++
 .../admin/site_uploads_controller_spec.rb     | 23 +++++++++++++++++++
 .../preview_card_providers_controller_spec.rb | 21 +++++++++++++++++
 .../admin/trends/links_controller_spec.rb     | 21 +++++++++++++++++
 .../admin/trends/statuses_controller_spec.rb  | 21 +++++++++++++++++
 .../admin/trends/tags_controller_spec.rb      | 21 +++++++++++++++++
 .../admin/warning_presets_controller_spec.rb  | 21 +++++++++++++++++
 .../admin/webhooks/secrets_controller_spec.rb | 23 +++++++++++++++++++
 .../admin/webhooks_controller_spec.rb         | 21 +++++++++++++++++
 20 files changed, 428 insertions(+)
 create mode 100644 spec/controllers/admin/account_actions_controller_spec.rb
 create mode 100644 spec/controllers/admin/announcements_controller_spec.rb
 create mode 100644 spec/controllers/admin/follow_recommendations_controller_spec.rb
 create mode 100644 spec/controllers/admin/ip_blocks_controller_spec.rb
 create mode 100644 spec/controllers/admin/relationships_controller_spec.rb
 create mode 100644 spec/controllers/admin/relays_controller_spec.rb
 create mode 100644 spec/controllers/admin/rules_controller_spec.rb
 create mode 100644 spec/controllers/admin/settings/about_controller_spec.rb
 create mode 100644 spec/controllers/admin/settings/appearance_controller_spec.rb
 create mode 100644 spec/controllers/admin/settings/content_retention_controller_spec.rb
 create mode 100644 spec/controllers/admin/settings/discovery_controller_spec.rb
 create mode 100644 spec/controllers/admin/settings/registrations_controller_spec.rb
 create mode 100644 spec/controllers/admin/site_uploads_controller_spec.rb
 create mode 100644 spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb
 create mode 100644 spec/controllers/admin/trends/links_controller_spec.rb
 create mode 100644 spec/controllers/admin/trends/statuses_controller_spec.rb
 create mode 100644 spec/controllers/admin/trends/tags_controller_spec.rb
 create mode 100644 spec/controllers/admin/warning_presets_controller_spec.rb
 create mode 100644 spec/controllers/admin/webhooks/secrets_controller_spec.rb
 create mode 100644 spec/controllers/admin/webhooks_controller_spec.rb

diff --git a/spec/controllers/admin/account_actions_controller_spec.rb b/spec/controllers/admin/account_actions_controller_spec.rb
new file mode 100644
index 0000000000..4eae51c7b5
--- /dev/null
+++ b/spec/controllers/admin/account_actions_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::AccountActionsController do
+  render_views
+
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #new' do
+    let(:account) { Fabricate(:account) }
+
+    it 'returns http success' do
+      get :new, params: { account_id: account.id }
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/announcements_controller_spec.rb b/spec/controllers/admin/announcements_controller_spec.rb
new file mode 100644
index 0000000000..288ac1d713
--- /dev/null
+++ b/spec/controllers/admin/announcements_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::AnnouncementsController 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
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/follow_recommendations_controller_spec.rb b/spec/controllers/admin/follow_recommendations_controller_spec.rb
new file mode 100644
index 0000000000..f62aa6e4b2
--- /dev/null
+++ b/spec/controllers/admin/follow_recommendations_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::FollowRecommendationsController do
+  render_views
+
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/ip_blocks_controller_spec.rb b/spec/controllers/admin/ip_blocks_controller_spec.rb
new file mode 100644
index 0000000000..873888afc7
--- /dev/null
+++ b/spec/controllers/admin/ip_blocks_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::IpBlocksController 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
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/relationships_controller_spec.rb b/spec/controllers/admin/relationships_controller_spec.rb
new file mode 100644
index 0000000000..1099a37a3b
--- /dev/null
+++ b/spec/controllers/admin/relationships_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::RelationshipsController 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
+    let(:account) { Fabricate(:account) }
+
+    it 'returns http success' do
+      get :index, params: { account_id: account.id }
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/relays_controller_spec.rb b/spec/controllers/admin/relays_controller_spec.rb
new file mode 100644
index 0000000000..dfb9f3c048
--- /dev/null
+++ b/spec/controllers/admin/relays_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::RelaysController 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
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/rules_controller_spec.rb b/spec/controllers/admin/rules_controller_spec.rb
new file mode 100644
index 0000000000..d7b633c049
--- /dev/null
+++ b/spec/controllers/admin/rules_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::RulesController 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
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/settings/about_controller_spec.rb b/spec/controllers/admin/settings/about_controller_spec.rb
new file mode 100644
index 0000000000..2ae26090b6
--- /dev/null
+++ b/spec/controllers/admin/settings/about_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Settings::AboutController do
+  render_views
+
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/settings/appearance_controller_spec.rb b/spec/controllers/admin/settings/appearance_controller_spec.rb
new file mode 100644
index 0000000000..65b29acc3e
--- /dev/null
+++ b/spec/controllers/admin/settings/appearance_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Settings::AppearanceController do
+  render_views
+
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/settings/content_retention_controller_spec.rb b/spec/controllers/admin/settings/content_retention_controller_spec.rb
new file mode 100644
index 0000000000..53ce84d189
--- /dev/null
+++ b/spec/controllers/admin/settings/content_retention_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Settings::ContentRetentionController do
+  render_views
+
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/settings/discovery_controller_spec.rb b/spec/controllers/admin/settings/discovery_controller_spec.rb
new file mode 100644
index 0000000000..c7307ffc88
--- /dev/null
+++ b/spec/controllers/admin/settings/discovery_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Settings::DiscoveryController do
+  render_views
+
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/settings/registrations_controller_spec.rb b/spec/controllers/admin/settings/registrations_controller_spec.rb
new file mode 100644
index 0000000000..3fc1f9d132
--- /dev/null
+++ b/spec/controllers/admin/settings/registrations_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Settings::RegistrationsController do
+  render_views
+
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/site_uploads_controller_spec.rb b/spec/controllers/admin/site_uploads_controller_spec.rb
new file mode 100644
index 0000000000..4ea37f396a
--- /dev/null
+++ b/spec/controllers/admin/site_uploads_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::SiteUploadsController do
+  render_views
+
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'DELETE #destroy' do
+    let(:site_upload) { Fabricate(:site_upload, var: 'thumbnail') }
+
+    it 'returns http success' do
+      delete :destroy, params: { id: site_upload.id }
+
+      expect(response).to redirect_to(admin_settings_path)
+    end
+  end
+end
diff --git a/spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb b/spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb
new file mode 100644
index 0000000000..95ed38d6b1
--- /dev/null
+++ b/spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Trends::Links::PreviewCardProvidersController 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
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/trends/links_controller_spec.rb b/spec/controllers/admin/trends/links_controller_spec.rb
new file mode 100644
index 0000000000..7c67f5e5aa
--- /dev/null
+++ b/spec/controllers/admin/trends/links_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Trends::LinksController 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
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/trends/statuses_controller_spec.rb b/spec/controllers/admin/trends/statuses_controller_spec.rb
new file mode 100644
index 0000000000..b752234d3c
--- /dev/null
+++ b/spec/controllers/admin/trends/statuses_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Trends::StatusesController 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
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/trends/tags_controller_spec.rb b/spec/controllers/admin/trends/tags_controller_spec.rb
new file mode 100644
index 0000000000..4f74a55455
--- /dev/null
+++ b/spec/controllers/admin/trends/tags_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Trends::TagsController 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
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/warning_presets_controller_spec.rb b/spec/controllers/admin/warning_presets_controller_spec.rb
new file mode 100644
index 0000000000..6b48fc28bb
--- /dev/null
+++ b/spec/controllers/admin/warning_presets_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::WarningPresetsController 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
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/admin/webhooks/secrets_controller_spec.rb b/spec/controllers/admin/webhooks/secrets_controller_spec.rb
new file mode 100644
index 0000000000..291a10fba5
--- /dev/null
+++ b/spec/controllers/admin/webhooks/secrets_controller_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Webhooks::SecretsController do
+  render_views
+
+  let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
+
+  before do
+    sign_in user, scope: :user
+  end
+
+  describe 'POST #rotate' do
+    let(:webhook) { Fabricate(:webhook) }
+
+    it 'returns http success' do
+      post :rotate, params: { webhook_id: webhook.id }
+
+      expect(response).to redirect_to(admin_webhook_path(webhook))
+    end
+  end
+end
diff --git a/spec/controllers/admin/webhooks_controller_spec.rb b/spec/controllers/admin/webhooks_controller_spec.rb
new file mode 100644
index 0000000000..12727e142b
--- /dev/null
+++ b/spec/controllers/admin/webhooks_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::WebhooksController 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
+    it 'returns http success' do
+      get :index
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end

From 7f4412eeeb1d35c9345c213b2cdfbbb9ce97dabb Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 11:16:11 -0500
Subject: [PATCH 31/40] User mailer spec coverage improvements (#23905)

---
 .../user_mailer/appeal_rejected.html.haml     |  2 +-
 spec/mailers/user_mailer_spec.rb              | 48 +++++++++++++++++++
 2 files changed, 49 insertions(+), 1 deletion(-)

diff --git a/app/views/user_mailer/appeal_rejected.html.haml b/app/views/user_mailer/appeal_rejected.html.haml
index 75cd9d023b..c316a73fb5 100644
--- a/app/views/user_mailer/appeal_rejected.html.haml
+++ b/app/views/user_mailer/appeal_rejected.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('media/images/mailer/icon_warning.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_flag.png'), alt: ''
 
                               %h1= t 'user_mailer.appeal_rejected.title'
 
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index 9c22f60f1d..30824e7b4d 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -94,4 +94,52 @@ describe UserMailer, type: :mailer do
       expect(mail.body.encoded).to include strike.text
     end
   end
+
+  describe 'webauthn_credential_deleted' do
+    let(:credential) { Fabricate(:webauthn_credential, user_id: receiver.id) }
+    let(:mail) { UserMailer.webauthn_credential_deleted(receiver, credential) }
+
+    it 'renders webauthn credential deleted notification' do
+      receiver.update!(locale: nil)
+      expect(mail.body.encoded).to include I18n.t('devise.mailer.webauthn_credential.deleted.title')
+    end
+
+    include_examples 'localized subject',
+                     'devise.mailer.webauthn_credential.deleted.subject'
+  end
+
+  describe 'suspicious_sign_in' do
+    let(:ip) { '192.168.0.1' }
+    let(:agent) { 'NCSA_Mosaic/2.0 (Windows 3.1)' }
+    let(:timestamp) { Time.now.utc }
+    let(:mail) { UserMailer.suspicious_sign_in(receiver, ip, agent, timestamp) }
+
+    it 'renders suspicious sign in notification' do
+      receiver.update!(locale: nil)
+      expect(mail.body.encoded).to include I18n.t('user_mailer.suspicious_sign_in.explanation')
+    end
+
+    include_examples 'localized subject',
+                     'user_mailer.suspicious_sign_in.subject'
+  end
+
+  describe 'appeal_approved' do
+    let(:appeal) { Fabricate(:appeal, account: receiver.account, approved_at: Time.now.utc) }
+    let(:mail) { UserMailer.appeal_approved(receiver, appeal) }
+
+    it 'renders appeal_approved notification' do
+      expect(mail.subject).to eq I18n.t('user_mailer.appeal_approved.subject', date: I18n.l(appeal.created_at))
+      expect(mail.body.encoded).to include I18n.t('user_mailer.appeal_approved.title')
+    end
+  end
+
+  describe 'appeal_rejected' do
+    let(:appeal) { Fabricate(:appeal, account: receiver.account, rejected_at: Time.now.utc) }
+    let(:mail) { UserMailer.appeal_rejected(receiver, appeal) }
+
+    it 'renders appeal_rejected notification' do
+      expect(mail.subject).to eq I18n.t('user_mailer.appeal_rejected.subject', date: I18n.l(appeal.created_at))
+      expect(mail.body.encoded).to include I18n.t('user_mailer.appeal_rejected.title')
+    end
+  end
 end

From 506b16cf595bf441b9a85db608b6d19e8934fd7d Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 11:16:45 -0500
Subject: [PATCH 32/40] Pending example models minimal coverage (#23912)

---
 spec/models/appeal_spec.rb                | 35 +++++++++++++++++++++--
 spec/models/custom_emoji_category_spec.rb | 11 +++++--
 spec/models/domain_allow_spec.rb          | 15 ++++++++--
 spec/models/ip_block_spec.rb              | 12 ++++++--
 spec/models/marker_spec.rb                | 13 +++++++--
 spec/models/poll_spec.rb                  | 29 +++++++++++++++++--
 spec/models/rule_spec.rb                  | 16 +++++++++--
 spec/models/status_edit_spec.rb           | 10 +++++--
 spec/models/trends/tags_spec.rb           |  4 ++-
 9 files changed, 128 insertions(+), 17 deletions(-)

diff --git a/spec/models/appeal_spec.rb b/spec/models/appeal_spec.rb
index 6aa013aba9..12373a9494 100644
--- a/spec/models/appeal_spec.rb
+++ b/spec/models/appeal_spec.rb
@@ -2,6 +2,37 @@
 
 require 'rails_helper'
 
-RSpec.describe Appeal, type: :model do
-  pending "add some examples to (or delete) #{__FILE__}"
+describe Appeal do
+  describe 'scopes' do
+    describe 'approved' do
+      let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) }
+      let(:not_approved_appeal) { Fabricate(:appeal, approved_at: nil) }
+
+      it 'finds the correct records' do
+        results = described_class.approved
+        expect(results).to eq([approved_appeal])
+      end
+    end
+
+    describe 'rejected' do
+      let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) }
+      let(:not_rejected_appeal) { Fabricate(:appeal, rejected_at: nil) }
+
+      it 'finds the correct records' do
+        results = described_class.rejected
+        expect(results).to eq([rejected_appeal])
+      end
+    end
+
+    describe 'pending' do
+      let(:approved_appeal) { Fabricate(:appeal, approved_at: 10.days.ago) }
+      let(:rejected_appeal) { Fabricate(:appeal, rejected_at: 10.days.ago) }
+      let(:pending_appeal) { Fabricate(:appeal, rejected_at: nil, approved_at: nil) }
+
+      it 'finds the correct records' do
+        results = described_class.pending
+        expect(results).to eq([pending_appeal])
+      end
+    end
+  end
 end
diff --git a/spec/models/custom_emoji_category_spec.rb b/spec/models/custom_emoji_category_spec.rb
index 74881b26c2..30de07bd81 100644
--- a/spec/models/custom_emoji_category_spec.rb
+++ b/spec/models/custom_emoji_category_spec.rb
@@ -2,6 +2,13 @@
 
 require 'rails_helper'
 
-RSpec.describe CustomEmojiCategory, type: :model do
-  pending "add some examples to (or delete) #{__FILE__}"
+describe CustomEmojiCategory do
+  describe 'validations' do
+    it 'validates name presence' do
+      record = described_class.new(name: nil)
+
+      expect(record).to_not be_valid
+      expect(record).to model_have_error_on_field(:name)
+    end
+  end
 end
diff --git a/spec/models/domain_allow_spec.rb b/spec/models/domain_allow_spec.rb
index 18cf5fe4c7..49e16376ea 100644
--- a/spec/models/domain_allow_spec.rb
+++ b/spec/models/domain_allow_spec.rb
@@ -2,6 +2,17 @@
 
 require 'rails_helper'
 
-RSpec.describe DomainAllow, type: :model do
-  pending "add some examples to (or delete) #{__FILE__}"
+describe DomainAllow do
+  describe 'scopes' do
+    describe 'matches_domain' do
+      let(:domain) { Fabricate(:domain_allow, domain: 'example.com') }
+      let(:other_domain) { Fabricate(:domain_allow, domain: 'example.biz') }
+
+      it 'returns the correct records' do
+        results = described_class.matches_domain('example.com')
+
+        expect(results).to eq([domain])
+      end
+    end
+  end
 end
diff --git a/spec/models/ip_block_spec.rb b/spec/models/ip_block_spec.rb
index 4c4028576a..ed58826672 100644
--- a/spec/models/ip_block_spec.rb
+++ b/spec/models/ip_block_spec.rb
@@ -2,6 +2,14 @@
 
 require 'rails_helper'
 
-RSpec.describe IpBlock, type: :model do
-  pending "add some examples to (or delete) #{__FILE__}"
+describe IpBlock do
+  describe 'to_log_human_identifier' do
+    let(:ip_block) { described_class.new(ip: '192.168.0.1') }
+
+    it 'combines the IP and prefix into a string' do
+      result = ip_block.to_log_human_identifier
+
+      expect(result).to eq('192.168.0.1/32')
+    end
+  end
 end
diff --git a/spec/models/marker_spec.rb b/spec/models/marker_spec.rb
index e8561c4c63..51dd584388 100644
--- a/spec/models/marker_spec.rb
+++ b/spec/models/marker_spec.rb
@@ -2,6 +2,15 @@
 
 require 'rails_helper'
 
-RSpec.describe Marker, type: :model do
-  pending "add some examples to (or delete) #{__FILE__}"
+describe Marker do
+  describe 'validations' do
+    describe 'timeline' do
+      it 'must be included in valid list' do
+        record = described_class.new(timeline: 'not real timeline')
+
+        expect(record).to_not be_valid
+        expect(record).to model_have_error_on_field(:timeline)
+      end
+    end
+  end
 end
diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb
index 474399bf68..8ae04ca41f 100644
--- a/spec/models/poll_spec.rb
+++ b/spec/models/poll_spec.rb
@@ -2,6 +2,31 @@
 
 require 'rails_helper'
 
-RSpec.describe Poll, type: :model do
-  pending "add some examples to (or delete) #{__FILE__}"
+describe Poll do
+  describe 'scopes' do
+    let(:status) { Fabricate(:status) }
+    let(:attached_poll) { Fabricate(:poll, status: status) }
+    let(:not_attached_poll) do
+      Fabricate(:poll).tap do |poll|
+        poll.status = nil
+        poll.save(validate: false)
+      end
+    end
+
+    describe 'attached' do
+      it 'finds the correct records' do
+        results = described_class.attached
+
+        expect(results).to eq([attached_poll])
+      end
+    end
+
+    describe 'unattached' do
+      it 'finds the correct records' do
+        results = described_class.unattached
+
+        expect(results).to eq([not_attached_poll])
+      end
+    end
+  end
 end
diff --git a/spec/models/rule_spec.rb b/spec/models/rule_spec.rb
index d5ec13ddf8..c9b9c55028 100644
--- a/spec/models/rule_spec.rb
+++ b/spec/models/rule_spec.rb
@@ -2,6 +2,18 @@
 
 require 'rails_helper'
 
-RSpec.describe Rule, type: :model do
-  pending "add some examples to (or delete) #{__FILE__}"
+describe Rule do
+  describe 'scopes' do
+    describe 'ordered' do
+      let(:deleted_rule) { Fabricate(:rule, deleted_at: 10.days.ago) }
+      let(:first_rule) { Fabricate(:rule, deleted_at: nil, priority: 1) }
+      let(:last_rule) { Fabricate(:rule, deleted_at: nil, priority: 10) }
+
+      it 'finds the correct records' do
+        results = described_class.ordered
+
+        expect(results).to eq([first_rule, last_rule])
+      end
+    end
+  end
 end
diff --git a/spec/models/status_edit_spec.rb b/spec/models/status_edit_spec.rb
index 0b9fa70873..2d33514522 100644
--- a/spec/models/status_edit_spec.rb
+++ b/spec/models/status_edit_spec.rb
@@ -2,6 +2,12 @@
 
 require 'rails_helper'
 
-RSpec.describe StatusEdit, type: :model do
-  pending "add some examples to (or delete) #{__FILE__}"
+describe StatusEdit do
+  describe '#reblog?' do
+    it 'returns false' do
+      record = described_class.new
+
+      expect(record).to_not be_a_reblog
+    end
+  end
 end
diff --git a/spec/models/trends/tags_spec.rb b/spec/models/trends/tags_spec.rb
index a9473e15c4..09ac918d00 100644
--- a/spec/models/trends/tags_spec.rb
+++ b/spec/models/trends/tags_spec.rb
@@ -24,7 +24,9 @@ RSpec.describe Trends::Tags do
   end
 
   describe '#query' do
-    pending
+    it 'returns a composable query scope' do
+      expect(subject.query).to be_a Trends::Query
+    end
   end
 
   describe '#refresh' do

From 2d1ccb3d8b9427cdc8ec1e187980458ab8a0890f Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 12:31:13 -0500
Subject: [PATCH 33/40] Restore missing fabricators (#23951)

---
 .../account_warning_preset_fabricator.rb            |  5 +++++
 spec/fabricators/list_account_fabricator.rb         |  7 +++++++
 spec/fabricators/one_time_key_fabricator.rb         | 13 +++++++++++++
 3 files changed, 25 insertions(+)
 create mode 100644 spec/fabricators/account_warning_preset_fabricator.rb
 create mode 100644 spec/fabricators/list_account_fabricator.rb
 create mode 100644 spec/fabricators/one_time_key_fabricator.rb

diff --git a/spec/fabricators/account_warning_preset_fabricator.rb b/spec/fabricators/account_warning_preset_fabricator.rb
new file mode 100644
index 0000000000..c50e08bf4f
--- /dev/null
+++ b/spec/fabricators/account_warning_preset_fabricator.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Fabricator(:account_warning_preset) do
+  text { Faker::Lorem.paragraph }
+end
diff --git a/spec/fabricators/list_account_fabricator.rb b/spec/fabricators/list_account_fabricator.rb
new file mode 100644
index 0000000000..00dde83cdf
--- /dev/null
+++ b/spec/fabricators/list_account_fabricator.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+Fabricator(:list_account) do
+  list
+  account
+  before_create { |list_account, _| list_account.list.account.follow!(account) }
+end
diff --git a/spec/fabricators/one_time_key_fabricator.rb b/spec/fabricators/one_time_key_fabricator.rb
new file mode 100644
index 0000000000..cfb365cabb
--- /dev/null
+++ b/spec/fabricators/one_time_key_fabricator.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+Fabricator(:one_time_key) do
+  device
+  key_id { Faker::Alphanumeric.alphanumeric(number: 10) }
+  key { Base64.strict_encode64(Ed25519::SigningKey.generate.verify_key.to_bytes) }
+
+  signature do |attrs|
+    signing_key = Ed25519::SigningKey.generate
+    attrs[:device].update(fingerprint_key: Base64.strict_encode64(signing_key.verify_key.to_bytes))
+    Base64.strict_encode64(signing_key.sign(attrs[:key]))
+  end
+end

From c599e289d806f6d13e3383879750db053a654821 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 17:17:46 -0500
Subject: [PATCH 34/40] Fix spec for api/v1/statuses/translations (#23956)

---
 .../api/v1/statuses/translations_controller_spec.rb          | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/spec/controllers/api/v1/statuses/translations_controller_spec.rb b/spec/controllers/api/v1/statuses/translations_controller_spec.rb
index 9575ac448d..2deea9fc0c 100644
--- a/spec/controllers/api/v1/statuses/translations_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/translations_controller_spec.rb
@@ -15,11 +15,12 @@ describe Api::V1::Statuses::TranslationsController do
     end
 
     describe 'POST #create' do
-      let(:status) { Fabricate(:status, account: user.account) }
+      let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') }
 
       before do
         translation = TranslationService::Translation.new(text: 'Hello')
-        service = instance_double(TranslationService::DeepL, translate: translation)
+        service = instance_double(TranslationService::DeepL, translate: translation, supported?: true)
+        allow(TranslationService).to receive(:configured?).and_return(true)
         allow(TranslationService).to receive(:configured).and_return(service)
         post :create, params: { status_id: status.id }
       end

From cb868228bde5351b79041f341a151bb08118d31f Mon Sep 17 00:00:00 2001
From: Christian Schmidt <github@chsc.dk>
Date: Sat, 4 Mar 2023 23:18:19 +0100
Subject: [PATCH 35/40] =?UTF-8?q?Do=20not=20leave=20Mastodon=20when=20clic?=
 =?UTF-8?q?king=20=E2=80=9CBack=E2=80=9D=20(#23953)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mastodon/components/column_back_button.jsx     |  6 +++---
 .../mastodon/components/column_header.jsx          | 14 +++++---------
 app/javascript/mastodon/features/ui/index.jsx      |  6 +++---
 3 files changed, 11 insertions(+), 15 deletions(-)

diff --git a/app/javascript/mastodon/components/column_back_button.jsx b/app/javascript/mastodon/components/column_back_button.jsx
index 5bbf11652b..5c5226b7ea 100644
--- a/app/javascript/mastodon/components/column_back_button.jsx
+++ b/app/javascript/mastodon/components/column_back_button.jsx
@@ -15,10 +15,10 @@ export default class ColumnBackButton extends React.PureComponent {
   };
 
   handleClick = () => {
-    if (window.history && window.history.length === 1) {
-      this.context.router.history.push('/');
-    } else {
+    if (window.history && window.history.state) {
       this.context.router.history.goBack();
+    } else {
+      this.context.router.history.push('/');
     }
   };
 
diff --git a/app/javascript/mastodon/components/column_header.jsx b/app/javascript/mastodon/components/column_header.jsx
index 38f6ad60ff..9ba783d903 100644
--- a/app/javascript/mastodon/components/column_header.jsx
+++ b/app/javascript/mastodon/components/column_header.jsx
@@ -43,14 +43,6 @@ class ColumnHeader extends React.PureComponent {
     animating: false,
   };
 
-  historyBack = () => {
-    if (window.history && window.history.length === 1) {
-      this.context.router.history.push('/');
-    } else {
-      this.context.router.history.goBack();
-    }
-  };
-
   handleToggleClick = (e) => {
     e.stopPropagation();
     this.setState({ collapsed: !this.state.collapsed, animating: true });
@@ -69,7 +61,11 @@ class ColumnHeader extends React.PureComponent {
   };
 
   handleBackClick = () => {
-    this.historyBack();
+    if (window.history && window.history.state) {
+      this.context.router.history.goBack();
+    } else {
+      this.context.router.history.push('/');
+    }
   };
 
   handleTransitionEnd = () => {
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 4f0ea04504..2dd59f95d4 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -474,10 +474,10 @@ class UI extends React.PureComponent {
   };
 
   handleHotkeyBack = () => {
-    if (window.history && window.history.length === 1) {
-      this.context.router.history.push('/');
-    } else {
+    if (window.history && window.history.state) {
       this.context.router.history.goBack();
+    } else {
+      this.context.router.history.push('/');
     }
   };
 

From 14f0b48fb60fec103d3b95d5f1cd4880a41c1a42 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Sat, 4 Mar 2023 18:33:08 -0500
Subject: [PATCH 36/40] Update browser gem to version 5.3.1 (#23945)

---
 Gemfile.lock          | 2 +-
 config/locales/en.yml | 5 ++++-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index b8b0943256..6f75f8f769 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -144,7 +144,7 @@ GEM
     bootsnap (1.16.0)
       msgpack (~> 1.2)
     brakeman (5.4.0)
-    browser (4.2.0)
+    browser (5.3.1)
     brpoplpush-redis_script (0.1.3)
       concurrent-ruby (~> 1.0, >= 1.0.5)
       redis (>= 1.0, < 6)
diff --git a/config/locales/en.yml b/config/locales/en.yml
index d142962b5a..97d0999e4a 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1448,6 +1448,7 @@ en:
       electron: Electron
       firefox: Firefox
       generic: Unknown browser
+      huawei_browser: Huawei Browser
       ie: Internet Explorer
       micro_messenger: MicroMessenger
       nokia: Nokia S40 Ovi Browser
@@ -1457,6 +1458,7 @@ en:
       qq: QQ Browser
       safari: Safari
       uc_browser: UC Browser
+      unknown_browser: Unknown Browser
       weibo: Weibo
     current_session: Current session
     description: "%{browser} on %{platform}"
@@ -1469,9 +1471,10 @@ en:
       chrome_os: ChromeOS
       firefox_os: Firefox OS
       ios: iOS
+      kai_os: KaiOS
       linux: Linux
       mac: macOS
-      other: unknown platform
+      unknown_platform: Unknown Platform
       windows: Windows
       windows_mobile: Windows Mobile
       windows_phone: Windows Phone

From 82a6cf401244e0cc51bb5fa2aa20e58b9996a640 Mon Sep 17 00:00:00 2001
From: Nick Schonning <nschonni@gmail.com>
Date: Sat, 4 Mar 2023 18:33:52 -0500
Subject: [PATCH 37/40] Enable Rubocop for app/views (#23874)

---
 .rubocop.yml                                 |  1 -
 app/views/accounts/show.rss.ruby             | 12 ++++++------
 app/views/tags/show.rss.ruby                 | 12 ++++++------
 app/views/well_known/host_meta/show.xml.ruby |  2 +-
 4 files changed, 13 insertions(+), 14 deletions(-)

diff --git a/.rubocop.yml b/.rubocop.yml
index f7ed79b76d..b4387a4611 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -19,7 +19,6 @@ AllCops:
   NewCops: enable
   Exclude:
     - db/schema.rb
-    - 'app/views/**/*'
     - 'config/**/*'
     - 'bin/*'
     - 'Rakefile'
diff --git a/app/views/accounts/show.rss.ruby b/app/views/accounts/show.rss.ruby
index 34e29d483f..7a77511ceb 100644
--- a/app/views/accounts/show.rss.ruby
+++ b/app/views/accounts/show.rss.ruby
@@ -5,7 +5,7 @@ RSS::Builder.build do |doc|
   doc.image(full_asset_url(@account.avatar.url(:original)), display_name(@account), params[:tag].present? ? short_account_tag_url(@account, params[:tag]) : short_account_url(@account))
   doc.last_build_date(@statuses.first.created_at) if @statuses.any?
   doc.icon(full_asset_url(@account.avatar.url(:original)))
-  doc.generator("Mastodon v#{Mastodon::Version.to_s}")
+  doc.generator("Mastodon v#{Mastodon::Version}")
 
   @statuses.each do |status|
     doc.item do |item|
@@ -18,12 +18,12 @@ RSS::Builder.build do |doc|
         item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
       end
 
-      status.ordered_media_attachments.each do |media|
-        item.media_content(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) do |media_content|
-          media_content.medium(media.gifv? ? 'image' : media.type.to_s)
+      status.ordered_media_attachments.each do |media_attachment|
+        item.media_content(full_asset_url(media_attachment.file.url(:original, false)), media_attachment.file.content_type, media_attachment.file.size) do |media_content|
+          media_content.medium(media_attachment.gifv? ? 'image' : media_attachment.type.to_s)
           media_content.rating(status.sensitive? ? 'adult' : 'nonadult')
-          media_content.description(media.description) if media.description.present?
-          media_content.thumbnail(media.thumbnail.url(:original, false)) if media.thumbnail?
+          media_content.description(media_attachment.description) if media_attachment.description.present?
+          media_content.thumbnail(media_attachment.thumbnail.url(:original, false)) if media_attachment.thumbnail?
         end
       end
 
diff --git a/app/views/tags/show.rss.ruby b/app/views/tags/show.rss.ruby
index 8e0c2327b5..bbda1ad4b5 100644
--- a/app/views/tags/show.rss.ruby
+++ b/app/views/tags/show.rss.ruby
@@ -3,7 +3,7 @@ RSS::Builder.build do |doc|
   doc.description(I18n.t('rss.descriptions.tag', hashtag: @tag.display_name))
   doc.link(tag_url(@tag))
   doc.last_build_date(@statuses.first.created_at) if @statuses.any?
-  doc.generator("Mastodon v#{Mastodon::Version.to_s}")
+  doc.generator("Mastodon v#{Mastodon::Version}")
 
   @statuses.each do |status|
     doc.item do |item|
@@ -16,12 +16,12 @@ RSS::Builder.build do |doc|
         item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
       end
 
-      status.ordered_media_attachments.each do |media|
-        item.media_content(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) do |media_content|
-          media_content.medium(media.gifv? ? 'image' : media.type.to_s)
+      status.ordered_media_attachments.each do |media_attachment|
+        item.media_content(full_asset_url(media_attachment.file.url(:original, false)), media_attachment.file.content_type, media_attachment.file.size) do |media_content|
+          media_content.medium(media_attachment.gifv? ? 'image' : media_attachment.type.to_s)
           media_content.rating(status.sensitive? ? 'adult' : 'nonadult')
-          media_content.description(media.description) if media.description.present?
-          media_content.thumbnail(media.thumbnail.url(:original, false)) if media.thumbnail?
+          media_content.description(media_attachment.description) if media_attachment.description.present?
+          media_content.thumbnail(media_attachment.thumbnail.url(:original, false)) if media_attachment.thumbnail?
         end
       end
 
diff --git a/app/views/well_known/host_meta/show.xml.ruby b/app/views/well_known/host_meta/show.xml.ruby
index b4e867c5f8..bb5a01a1b5 100644
--- a/app/views/well_known/host_meta/show.xml.ruby
+++ b/app/views/well_known/host_meta/show.xml.ruby
@@ -9,4 +9,4 @@ doc << Ox::Element.new('XRD').tap do |xrd|
   end
 end
 
-('<?xml version="1.0" encoding="UTF-8"?>' + Ox.dump(doc, effort: :tolerant)).force_encoding('UTF-8')
+"<?xml version=\"1.0\" encoding=\"UTF-8\"?>#{Ox.dump(doc, effort: :tolerant)}".force_encoding('UTF-8')

From ade3c5d40c1f2bd534d85424a878f1d28b9352f4 Mon Sep 17 00:00:00 2001
From: Samruddhi Khandale <samruddhikhandale@github.com>
Date: Sat, 4 Mar 2023 15:35:00 -0800
Subject: [PATCH 38/40] Updates dev container and adds doc for getting started
 with GitHub Codespaces (#23872)

---
 .devcontainer/Dockerfile          | 12 +++++-----
 .devcontainer/devcontainer.json   | 38 ++++++++++++-------------------
 .devcontainer/docker-compose.yml  | 10 +-------
 .devcontainer/welcome-message.txt |  8 +++++++
 README.md                         | 10 ++++++++
 5 files changed, 40 insertions(+), 38 deletions(-)
 create mode 100644 .devcontainer/welcome-message.txt

diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 425b86a6bb..04ac9560ca 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,16 +1,14 @@
-# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster
-ARG VARIANT=3.1-bullseye
-FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
+# For details, see https://github.com/devcontainers/images/tree/main/src/ruby
+FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye
 
 # Install Rails
 # RUN gem install rails webdrivers
 
 # Default value to allow debug server to serve content over GitHub Codespace's port forwarding service
 # The value is a comma-separated list of allowed domains
-ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev"
+ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev"
 
-# [Choice] Node.js version: lts/*, 18, 16, 14
-ARG NODE_VERSION="lts/*"
+ARG NODE_VERSION="16"
 RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
 
 # [Optional] Uncomment this section to install additional OS packages.
@@ -22,3 +20,5 @@ RUN gem install foreman
 
 # [Optional] Uncomment this line to install global node packages.
 RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1
+
+COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 6ac6993ee9..d628fd1bd1 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,30 +1,13 @@
+// For more details, see https://aka.ms/devcontainer.json.
 {
   "name": "Mastodon",
   "dockerComposeFile": "docker-compose.yml",
   "service": "app",
-  "workspaceFolder": "/mastodon",
-
-  // Configure tool-specific properties.
-  "customizations": {
-    // Configure properties specific to VS Code.
-    "vscode": {
-      // Set *default* container specific settings.json values on container create.
-      "settings": {},
-
-      // Add the IDs of extensions you want installed when the container is created.
-      "extensions": [
-        "EditorConfig.EditorConfig",
-        "dbaeumer.vscode-eslint",
-        "rebornix.Ruby",
-        "webben.browserslist"
-      ]
-    }
-  },
+  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
 
+  // Features to add to the dev container. More info: https://containers.dev/features.
   "features": {
-    "ghcr.io/devcontainers/features/sshd:1": {
-      "version": "latest"
-    }
+    "ghcr.io/devcontainers/features/sshd:1": {}
   },
 
   // Use 'forwardPorts' to make a list of ports inside the container available locally.
@@ -33,7 +16,16 @@
 
   // Use 'postCreateCommand' to run commands after the container is created.
   "postCreateCommand": ".devcontainer/post-create.sh",
+  "waitFor": "postCreateCommand",
 
-  // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
-  "remoteUser": "vscode"
+  // Configure tool-specific properties.
+  "customizations": {
+    // Configure properties specific to VS Code.
+    "vscode": {
+      // Set *default* container specific settings.json values on container create.
+      "settings": {},
+      // Add the IDs of extensions you want installed when the container is created.
+      "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
+    }
+  }
 }
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 95f401379c..19f9c0b5b3 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -5,15 +5,8 @@ services:
     build:
       context: .
       dockerfile: Dockerfile
-      args:
-        # Update 'VARIANT' to pick a version of Ruby: 3, 3.1, 3.0, 2, 2.7, 2.6
-        # Append -bullseye or -buster to pin to an OS version.
-        # Use -bullseye variants on local arm64/Apple Silicon.
-        VARIANT: '3.0-bullseye'
-        # Optional Node.js version to install
-        NODE_VERSION: '16'
     volumes:
-      - ..:/mastodon:cached
+      - ../..:/workspaces:cached
     environment:
       RAILS_ENV: development
       NODE_ENV: development
@@ -33,7 +26,6 @@ services:
     networks:
       - external_network
       - internal_network
-    user: vscode
 
   db:
     image: postgres:14-alpine
diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt
new file mode 100644
index 0000000000..488cf92857
--- /dev/null
+++ b/.devcontainer/welcome-message.txt
@@ -0,0 +1,8 @@
+👋 Welcome to "Mastodon" in GitHub Codespaces!
+
+🛠️  Your environment is fully setup with all the required software.
+
+🔍 To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1).
+
+📝 Edit away, run your app as usual, and we'll automatically make it available for you to access.
+
diff --git a/README.md b/README.md
index 1b5db92a84..306665e9a6 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,16 @@ A **Vagrant** configuration is included for development purposes. To use it, com
 - Run `vagrant ssh -c "cd /vagrant && foreman start"`
 - Open `http://mastodon.local` in your browser
 
+### Getting Started with GitHub Codespaces
+
+To get started, create a codespace for this repository by clicking this 👇
+
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283)
+
+A codespace will open in a web-based version of Visual Studio Code. The [dev container](.devcontainer/devcontainer.json) is fully configured with software needed for this project.
+
+**Note**: Dev containers is an open spec which is supported by [GitHub Codespaces](https://github.com/codespaces) and [other tools](https://containers.dev/supporting).
+
 ## Contributing
 
 Mastodon is **free, open-source software** licensed under **AGPLv3**.

From be1792e1d45fd32c037195d3d03f09fbd4dbf00e Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Sun, 5 Mar 2023 01:52:12 +0100
Subject: [PATCH 39/40] Fix streaming API not being usable without
 `DATABASE_URL` (#23960)

---
 streaming/index.js | 48 +++++++++++++++++++++++++++++++++++++---------
 1 file changed, 39 insertions(+), 9 deletions(-)

diff --git a/streaming/index.js b/streaming/index.js
index ba7cfea191..2cc4934ee9 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -81,9 +81,10 @@ const startMaster = () => {
   log.warn(`Starting streaming API server master with ${numWorkers} workers`);
 };
 
-const startWorker = async (workerId) => {
-  log.warn(`Starting worker ${workerId}`);
-
+/**
+ * @return {Object.<string, any>}
+ */
+const pgConfigFromEnv = () => {
   const pgConfigs = {
     development: {
       user:     process.env.DB_USER || pg.defaults.user,
@@ -102,16 +103,45 @@ const startWorker = async (workerId) => {
     },
   };
 
+  let baseConfig;
+
+  if (process.env.DATABASE_URL) {
+    baseConfig = dbUrlToConfig(process.env.DATABASE_URL);
+  } else {
+    baseConfig = pgConfigs[env];
+
+    if (process.env.DB_SSLMODE) {
+      switch(process.env.DB_SSLMODE) {
+      case 'disable':
+      case '':
+        baseConfig.ssl = false;
+        break;
+      case 'no-verify':
+        baseConfig.ssl = { rejectUnauthorized: false };
+        break;
+      default:
+        baseConfig.ssl = {};
+        break;
+      }
+    }
+  }
+
+  return {
+    ...baseConfig,
+    max: process.env.DB_POOL || 10,
+    connectionTimeoutMillis: 15000,
+    application_name: '',
+  };
+};
+
+const startWorker = async (workerId) => {
+  log.warn(`Starting worker ${workerId}`);
+
   const app = express();
 
   app.set('trust proxy', process.env.TRUSTED_PROXY_IP ? process.env.TRUSTED_PROXY_IP.split(/(?:\s*,\s*|\s+)/) : 'loopback,uniquelocal');
 
-  const pgPool = new pg.Pool(Object.assign(pgConfigs[env], dbUrlToConfig(process.env.DATABASE_URL), {
-    max: process.env.DB_POOL || 10,
-    connectionTimeoutMillis: 15000,
-    ssl: !!process.env.DB_SSLMODE && process.env.DB_SSLMODE !== 'disable',
-  }));
-
+  const pgPool = new pg.Pool(pgConfigFromEnv());
   const server = http.createServer(app);
   const redisNamespace = process.env.REDIS_NAMESPACE || null;
 

From dfa9843ac85d04e1facb2f757fd9288d8bb9fb2c Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Sun, 5 Mar 2023 01:52:42 +0100
Subject: [PATCH 40/40] Fix pgBouncer resetting application name on every
 transaction (#23958)

---
 config/database.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/config/database.yml b/config/database.yml
index bfb53f21b4..34acf2f19a 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -5,6 +5,7 @@ default: &default
   connect_timeout: 15
   encoding: unicode
   sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %>
+  application_name: ''
 
 development:
   <<: *default