From 155fb8414150e78b4e61aa33d483cc7713161134 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 16 Nov 2023 09:36:59 -0500
Subject: [PATCH] Improve spec coverage for collection of `workers/` classes
 (#27874)

---
 .../account_deletion_request_fabricator.rb    |  5 ++
 spec/fabricators/import_fabricator.rb         |  7 +++
 spec/workers/account_refresh_worker_spec.rb   | 52 ++++++++++++++++
 .../activitypub/post_upgrade_worker_spec.rb   | 18 ++++++
 ...ze_featured_tags_collection_worker_spec.rb | 29 +++++++++
 spec/workers/admin/suspension_worker_spec.rb  | 28 +++++++++
 .../after_account_domain_block_worker_spec.rb | 29 +++++++++
 spec/workers/backup_worker_spec.rb            | 36 +++++++++++
 spec/workers/delete_mute_worker_spec.rb       | 42 +++++++++++++
 spec/workers/feed_insert_worker_spec.rb       | 21 ++++++-
 spec/workers/import_worker_spec.rb            | 23 +++++++
 .../workers/post_process_media_worker_spec.rb | 34 +++++++++--
 ...blish_announcement_reaction_worker_spec.rb | 38 ++++++++++++
 spec/workers/removal_worker_spec.rb           | 28 +++++++++
 .../scheduler/self_destruct_scheduler_spec.rb | 60 +++++++++++++++++++
 spec/workers/webhooks/delivery_worker_spec.rb | 18 +++++-
 16 files changed, 460 insertions(+), 8 deletions(-)
 create mode 100644 spec/fabricators/account_deletion_request_fabricator.rb
 create mode 100644 spec/fabricators/import_fabricator.rb
 create mode 100644 spec/workers/account_refresh_worker_spec.rb
 create mode 100644 spec/workers/activitypub/post_upgrade_worker_spec.rb
 create mode 100644 spec/workers/activitypub/synchronize_featured_tags_collection_worker_spec.rb
 create mode 100644 spec/workers/admin/suspension_worker_spec.rb
 create mode 100644 spec/workers/after_account_domain_block_worker_spec.rb
 create mode 100644 spec/workers/backup_worker_spec.rb
 create mode 100644 spec/workers/delete_mute_worker_spec.rb
 create mode 100644 spec/workers/import_worker_spec.rb
 create mode 100644 spec/workers/publish_announcement_reaction_worker_spec.rb
 create mode 100644 spec/workers/removal_worker_spec.rb
 create mode 100644 spec/workers/scheduler/self_destruct_scheduler_spec.rb

diff --git a/spec/fabricators/account_deletion_request_fabricator.rb b/spec/fabricators/account_deletion_request_fabricator.rb
new file mode 100644
index 000000000..3d3d37398
--- /dev/null
+++ b/spec/fabricators/account_deletion_request_fabricator.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Fabricator(:account_deletion_request) do
+  account
+end
diff --git a/spec/fabricators/import_fabricator.rb b/spec/fabricators/import_fabricator.rb
new file mode 100644
index 000000000..4951bb9a4
--- /dev/null
+++ b/spec/fabricators/import_fabricator.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+Fabricator(:import) do
+  account
+  type :following
+  data { attachment_fixture('imports.txt') }
+end
diff --git a/spec/workers/account_refresh_worker_spec.rb b/spec/workers/account_refresh_worker_spec.rb
new file mode 100644
index 000000000..361d69aa0
--- /dev/null
+++ b/spec/workers/account_refresh_worker_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AccountRefreshWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(ResolveAccountService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(ResolveAccountService).to receive(:new).and_return(service)
+    end
+
+    context 'when account does not exist' do
+      it 'returns immediately without processing' do
+        worker.perform(123_123_123)
+
+        expect(service).to_not have_received(:call)
+      end
+    end
+
+    context 'when account exists' do
+      context 'when account does not need refreshing' do
+        let(:account) { Fabricate(:account, last_webfingered_at: recent_webfinger_at) }
+
+        it 'returns immediately without processing' do
+          worker.perform(account.id)
+
+          expect(service).to_not have_received(:call)
+        end
+      end
+
+      context 'when account needs refreshing' do
+        let(:account) { Fabricate(:account, last_webfingered_at: outdated_webfinger_at) }
+
+        it 'schedules an account update' do
+          worker.perform(account.id)
+
+          expect(service).to have_received(:call)
+        end
+      end
+
+      def recent_webfinger_at
+        (Account::BACKGROUND_REFRESH_INTERVAL - 3.days).ago
+      end
+
+      def outdated_webfinger_at
+        (Account::BACKGROUND_REFRESH_INTERVAL + 3.days).ago
+      end
+    end
+  end
+end
diff --git a/spec/workers/activitypub/post_upgrade_worker_spec.rb b/spec/workers/activitypub/post_upgrade_worker_spec.rb
new file mode 100644
index 000000000..08de150ad
--- /dev/null
+++ b/spec/workers/activitypub/post_upgrade_worker_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ActivityPub::PostUpgradeWorker do
+  let(:worker) { described_class.new }
+
+  describe '#perform' do
+    let(:domain) { 'host.example' }
+
+    it 'updates relevant values' do
+      account = Fabricate(:account, domain: domain, last_webfingered_at: 1.day.ago, protocol: :ostatus)
+      worker.perform(domain)
+
+      expect(account.reload.last_webfingered_at).to be_nil
+    end
+  end
+end
diff --git a/spec/workers/activitypub/synchronize_featured_tags_collection_worker_spec.rb b/spec/workers/activitypub/synchronize_featured_tags_collection_worker_spec.rb
new file mode 100644
index 000000000..8cf13cb90
--- /dev/null
+++ b/spec/workers/activitypub/synchronize_featured_tags_collection_worker_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ActivityPub::SynchronizeFeaturedTagsCollectionWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(ActivityPub::FetchFeaturedTagsCollectionService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(ActivityPub::FetchFeaturedTagsCollectionService).to receive(:new).and_return(service)
+    end
+
+    let(:account) { Fabricate(:account) }
+    let(:url) { 'https://host.example' }
+
+    it 'sends the account and url to the service' do
+      worker.perform(account.id, url)
+
+      expect(service).to have_received(:call).with(account, url)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123, url)
+
+      expect(result).to be(true)
+    end
+  end
+end
diff --git a/spec/workers/admin/suspension_worker_spec.rb b/spec/workers/admin/suspension_worker_spec.rb
new file mode 100644
index 000000000..da12037ed
--- /dev/null
+++ b/spec/workers/admin/suspension_worker_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::SuspensionWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(SuspendAccountService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(SuspendAccountService).to receive(:new).and_return(service)
+    end
+
+    let(:account) { Fabricate(:account) }
+
+    it 'sends the account to the service' do
+      worker.perform(account.id)
+
+      expect(service).to have_received(:call).with(account)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123)
+
+      expect(result).to be(true)
+    end
+  end
+end
diff --git a/spec/workers/after_account_domain_block_worker_spec.rb b/spec/workers/after_account_domain_block_worker_spec.rb
new file mode 100644
index 000000000..54a113a2b
--- /dev/null
+++ b/spec/workers/after_account_domain_block_worker_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AfterAccountDomainBlockWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(AfterBlockDomainFromAccountService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(AfterBlockDomainFromAccountService).to receive(:new).and_return(service)
+    end
+
+    let(:account) { Fabricate(:account) }
+    let(:domain) { 'host.example' }
+
+    it 'sends the account and domain to the service' do
+      worker.perform(account.id, domain)
+
+      expect(service).to have_received(:call).with(account, domain)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123, domain)
+
+      expect(result).to be(true)
+    end
+  end
+end
diff --git a/spec/workers/backup_worker_spec.rb b/spec/workers/backup_worker_spec.rb
new file mode 100644
index 000000000..1a169513e
--- /dev/null
+++ b/spec/workers/backup_worker_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe BackupWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(BackupService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(BackupService).to receive(:new).and_return(service)
+    end
+
+    let(:backup) { Fabricate(:backup) }
+    let!(:other_backup) { Fabricate(:backup, user: backup.user) }
+
+    it 'sends the backup to the service and removes other backups' do
+      expect do
+        worker.perform(backup.id)
+      end.to change(UserMailer.deliveries, :size).by(1)
+
+      expect(service).to have_received(:call).with(backup)
+      expect { other_backup.reload }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+
+    context 'when sidekiq retries are exhausted' do
+      it 'destroys the backup' do
+        described_class.within_sidekiq_retries_exhausted_block({ 'args' => [backup.id] }) do
+          worker.perform(backup.id)
+        end
+
+        expect { backup.reload }.to raise_error(ActiveRecord::RecordNotFound)
+      end
+    end
+  end
+end
diff --git a/spec/workers/delete_mute_worker_spec.rb b/spec/workers/delete_mute_worker_spec.rb
new file mode 100644
index 000000000..1fc84491c
--- /dev/null
+++ b/spec/workers/delete_mute_worker_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe DeleteMuteWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(UnmuteService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(UnmuteService).to receive(:new).and_return(service)
+    end
+
+    context 'with an expired mute' do
+      let(:mute) { Fabricate(:mute, expires_at: 1.day.ago) }
+
+      it 'sends the mute to the service' do
+        worker.perform(mute.id)
+
+        expect(service).to have_received(:call).with(mute.account, mute.target_account)
+      end
+    end
+
+    context 'with an unexpired mute' do
+      let(:mute) { Fabricate(:mute, expires_at: 1.day.from_now) }
+
+      it 'does not send the mute to the service' do
+        worker.perform(mute.id)
+
+        expect(service).to_not have_received(:call)
+      end
+    end
+
+    context 'with a non-existent mute' do
+      it 'does not send the mute to the service' do
+        worker.perform(123_123_123)
+
+        expect(service).to_not have_received(:call)
+      end
+    end
+  end
+end
diff --git a/spec/workers/feed_insert_worker_spec.rb b/spec/workers/feed_insert_worker_spec.rb
index 97c73c599..e9484879f 100644
--- a/spec/workers/feed_insert_worker_spec.rb
+++ b/spec/workers/feed_insert_worker_spec.rb
@@ -8,6 +8,7 @@ describe FeedInsertWorker do
   describe 'perform' do
     let(:follower) { Fabricate(:account) }
     let(:status) { Fabricate(:status) }
+    let(:list) { Fabricate(:list) }
 
     context 'when there are no records' do
       it 'skips push with missing status' do
@@ -42,11 +43,29 @@ describe FeedInsertWorker do
       it 'pushes the status onto the home timeline without filter' do
         instance = instance_double(FeedManager, push_to_home: nil, filter?: false)
         allow(FeedManager).to receive(:instance).and_return(instance)
-        result = subject.perform(status.id, follower.id)
+        result = subject.perform(status.id, follower.id, :home)
 
         expect(result).to be_nil
         expect(instance).to have_received(:push_to_home).with(follower, status, update: nil)
       end
+
+      it 'pushes the status onto the tags timeline without filter' do
+        instance = instance_double(FeedManager, push_to_home: nil, filter?: false)
+        allow(FeedManager).to receive(:instance).and_return(instance)
+        result = subject.perform(status.id, follower.id, :tags)
+
+        expect(result).to be_nil
+        expect(instance).to have_received(:push_to_home).with(follower, status, update: nil)
+      end
+
+      it 'pushes the status onto the list timeline without filter' do
+        instance = instance_double(FeedManager, push_to_list: nil, filter?: false)
+        allow(FeedManager).to receive(:instance).and_return(instance)
+        result = subject.perform(status.id, list.id, :list)
+
+        expect(result).to be_nil
+        expect(instance).to have_received(:push_to_list).with(list, status, update: nil)
+      end
     end
   end
 end
diff --git a/spec/workers/import_worker_spec.rb b/spec/workers/import_worker_spec.rb
new file mode 100644
index 000000000..4095a5d35
--- /dev/null
+++ b/spec/workers/import_worker_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ImportWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(ImportService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(ImportService).to receive(:new).and_return(service)
+    end
+
+    let(:import) { Fabricate(:import) }
+
+    it 'sends the import to the service' do
+      worker.perform(import.id)
+
+      expect(service).to have_received(:call).with(import)
+      expect { import.reload }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+  end
+end
diff --git a/spec/workers/post_process_media_worker_spec.rb b/spec/workers/post_process_media_worker_spec.rb
index 33072704b..828da5244 100644
--- a/spec/workers/post_process_media_worker_spec.rb
+++ b/spec/workers/post_process_media_worker_spec.rb
@@ -2,12 +2,38 @@
 
 require 'rails_helper'
 
-describe PostProcessMediaWorker do
+describe PostProcessMediaWorker, :paperclip_processing 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
+  describe '#perform' do
+    let(:media_attachment) { Fabricate(:media_attachment) }
+
+    it 'reprocesses and updates the media attachment' do
+      worker.perform(media_attachment.id)
+
+      expect(media_attachment.processing).to eq('complete')
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123)
+
+      expect(result).to be(true)
+    end
+
+    context 'when sidekiq retries are exhausted' do
+      it 'sets state to failed' do
+        described_class.within_sidekiq_retries_exhausted_block({ 'args' => [media_attachment.id] }) do
+          worker.perform(media_attachment.id)
+        end
+
+        expect(media_attachment.reload.processing).to eq('failed')
+      end
+
+      it 'returns true for non-existent record' do
+        described_class.within_sidekiq_retries_exhausted_block({ 'args' => [123_123_123] }) do
+          expect(worker.perform(123_123_123)).to be(true)
+        end
+      end
     end
   end
 end
diff --git a/spec/workers/publish_announcement_reaction_worker_spec.rb b/spec/workers/publish_announcement_reaction_worker_spec.rb
new file mode 100644
index 000000000..91668b5ad
--- /dev/null
+++ b/spec/workers/publish_announcement_reaction_worker_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PublishAnnouncementReactionWorker do
+  let(:worker) { described_class.new }
+
+  describe '#perform' do
+    before { Fabricate(:account, user: Fabricate(:user, current_sign_in_at: 1.hour.ago)) }
+
+    let(:announcement) { Fabricate(:announcement) }
+    let(:name) { 'name value' }
+
+    it 'sends the announcement and name to the service when subscribed' do
+      allow(redis).to receive(:exists?).and_return(true)
+      allow(redis).to receive(:publish)
+
+      worker.perform(announcement.id, name)
+
+      expect(redis).to have_received(:publish)
+    end
+
+    it 'does not send the announcement and name to the service when not subscribed' do
+      allow(redis).to receive(:exists?).and_return(false)
+      allow(redis).to receive(:publish)
+
+      worker.perform(announcement.id, name)
+
+      expect(redis).to_not have_received(:publish)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123, name)
+
+      expect(result).to be(true)
+    end
+  end
+end
diff --git a/spec/workers/removal_worker_spec.rb b/spec/workers/removal_worker_spec.rb
new file mode 100644
index 000000000..5071e882b
--- /dev/null
+++ b/spec/workers/removal_worker_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe RemovalWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(RemoveStatusService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(RemoveStatusService).to receive(:new).and_return(service)
+    end
+
+    let(:status) { Fabricate(:status) }
+
+    it 'sends the status to the service' do
+      worker.perform(status.id)
+
+      expect(service).to have_received(:call).with(status)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123)
+
+      expect(result).to be(true)
+    end
+  end
+end
diff --git a/spec/workers/scheduler/self_destruct_scheduler_spec.rb b/spec/workers/scheduler/self_destruct_scheduler_spec.rb
new file mode 100644
index 000000000..2bf578357
--- /dev/null
+++ b/spec/workers/scheduler/self_destruct_scheduler_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::SelfDestructScheduler do
+  let(:worker) { described_class.new }
+
+  describe '#perform' do
+    let!(:account) { Fabricate(:account, domain: nil, suspended_at: nil) }
+
+    context 'when not in self destruct mode' do
+      before do
+        allow(SelfDestructHelper).to receive(:self_destruct?).and_return(false)
+      end
+
+      it 'returns without processing' do
+        worker.perform
+
+        expect(account.reload.suspended_at).to be_nil
+      end
+    end
+
+    context 'when in self-destruct mode' do
+      before do
+        allow(SelfDestructHelper).to receive(:self_destruct?).and_return(true)
+      end
+
+      context 'when sidekiq is overwhelmed' do
+        before do
+          stats = instance_double(Sidekiq::Stats, enqueued: described_class::MAX_ENQUEUED**2)
+          allow(Sidekiq::Stats).to receive(:new).and_return(stats)
+        end
+
+        it 'returns without processing' do
+          worker.perform
+
+          expect(account.reload.suspended_at).to be_nil
+        end
+      end
+
+      context 'when sidekiq is operational' do
+        it 'suspends local non-suspended accounts' do
+          worker.perform
+
+          expect(account.reload.suspended_at).to_not be_nil
+        end
+
+        it 'suspends local suspended accounts marked for deletion' do
+          account.update(suspended_at: 10.days.ago)
+          deletion_request = Fabricate(:account_deletion_request, account: account)
+
+          worker.perform
+
+          expect(account.reload.suspended_at).to be > 1.day.ago
+          expect { deletion_request.reload }.to raise_error(ActiveRecord::RecordNotFound)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/workers/webhooks/delivery_worker_spec.rb b/spec/workers/webhooks/delivery_worker_spec.rb
index daf8a3e28..6a5483d1d 100644
--- a/spec/workers/webhooks/delivery_worker_spec.rb
+++ b/spec/workers/webhooks/delivery_worker_spec.rb
@@ -5,9 +5,21 @@ 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
+  describe '#perform' do
+    let(:webhook) { Fabricate(:webhook) }
+
+    it 'reprocesses and updates the webhook' do
+      stub_request(:post, webhook.url).to_return(status: 200, body: '')
+
+      worker.perform(webhook.id, 'body')
+
+      expect(a_request(:post, webhook.url)).to have_been_made.at_least_once
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123, '')
+
+      expect(result).to be(true)
     end
   end
 end