From 28e1a7a394db5586bdeb1d872f2bf4bfc18b5d0a Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 14 Dec 2023 05:29:10 -0500
Subject: [PATCH] Improve spec coverage for `models/announcement` class
 (#28350)

---
 spec/controllers/emojis_controller_spec.rb    |   2 +-
 .../announcement_mute_fabricator.rb           |   6 +
 .../announcement_reaction_fabricator.rb       |   7 +
 spec/fabricators/custom_emoji_fabricator.rb   |   2 +-
 spec/models/announcement_spec.rb              | 210 ++++++++++++++++++
 spec/models/custom_emoji_spec.rb              |   2 +-
 spec/requests/api/v1/custom_emojis_spec.rb    |   2 +-
 7 files changed, 227 insertions(+), 4 deletions(-)
 create mode 100644 spec/fabricators/announcement_mute_fabricator.rb
 create mode 100644 spec/fabricators/announcement_reaction_fabricator.rb
 create mode 100644 spec/models/announcement_spec.rb

diff --git a/spec/controllers/emojis_controller_spec.rb b/spec/controllers/emojis_controller_spec.rb
index 3fe19ee5c..dd139de93 100644
--- a/spec/controllers/emojis_controller_spec.rb
+++ b/spec/controllers/emojis_controller_spec.rb
@@ -5,7 +5,7 @@ require 'rails_helper'
 describe EmojisController do
   render_views
 
-  let(:emoji) { Fabricate(:custom_emoji) }
+  let(:emoji) { Fabricate(:custom_emoji, shortcode: 'coolcat') }
 
   describe 'GET #show' do
     let(:response) { get :show, params: { id: emoji.id, format: :json } }
diff --git a/spec/fabricators/announcement_mute_fabricator.rb b/spec/fabricators/announcement_mute_fabricator.rb
new file mode 100644
index 000000000..0adc1d60b
--- /dev/null
+++ b/spec/fabricators/announcement_mute_fabricator.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+Fabricator(:announcement_mute) do
+  announcement { Fabricate.build(:announcement) }
+  account { Fabricate.build(:account) }
+end
diff --git a/spec/fabricators/announcement_reaction_fabricator.rb b/spec/fabricators/announcement_reaction_fabricator.rb
new file mode 100644
index 000000000..e84579729
--- /dev/null
+++ b/spec/fabricators/announcement_reaction_fabricator.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+Fabricator(:announcement_reaction) do
+  account { Fabricate.build(:account) }
+  announcement { Fabricate.build(:announcement) }
+  name { Fabricate(:custom_emoji).shortcode }
+end
diff --git a/spec/fabricators/custom_emoji_fabricator.rb b/spec/fabricators/custom_emoji_fabricator.rb
index fa570eec6..7ef875286 100644
--- a/spec/fabricators/custom_emoji_fabricator.rb
+++ b/spec/fabricators/custom_emoji_fabricator.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 Fabricator(:custom_emoji) do
-  shortcode 'coolcat'
+  shortcode { sequence(:shortcode) { |i| "code_#{i}" } }
   domain    nil
   image     { Rails.root.join('spec', 'fixtures', 'files', 'emojo.png').open }
 end
diff --git a/spec/models/announcement_spec.rb b/spec/models/announcement_spec.rb
new file mode 100644
index 000000000..e37b81a52
--- /dev/null
+++ b/spec/models/announcement_spec.rb
@@ -0,0 +1,210 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Announcement do
+  describe 'Scopes' do
+    context 'with published and unpublished records' do
+      let!(:published) { Fabricate(:announcement, published: true) }
+      let!(:unpublished) { Fabricate(:announcement, published: false, scheduled_at: 10.days.from_now) }
+
+      describe '#unpublished' do
+        it 'returns records with published false' do
+          results = described_class.unpublished
+
+          expect(results).to eq([unpublished])
+        end
+      end
+
+      describe '#published' do
+        it 'returns records with published true' do
+          results = described_class.published
+
+          expect(results).to eq([published])
+        end
+      end
+    end
+
+    describe '#without_muted' do
+      let!(:announcement) { Fabricate(:announcement) }
+      let(:account) { Fabricate(:account) }
+      let(:muted_announcement) { Fabricate(:announcement) }
+
+      before do
+        Fabricate(:announcement_mute, account: account, announcement: muted_announcement)
+      end
+
+      it 'returns the announcements not muted by the account' do
+        results = described_class.without_muted(account)
+        expect(results).to include(announcement)
+        expect(results).to_not include(muted_announcement)
+      end
+    end
+
+    context 'with timestamped announcements' do
+      let!(:adam_announcement) { Fabricate(:announcement, starts_at: 100.days.ago, scheduled_at: 10.days.ago, published_at: 10.days.ago, ends_at: 5.days.from_now) }
+      let!(:brenda_announcement) { Fabricate(:announcement, starts_at: 10.days.ago, scheduled_at: 100.days.ago, published_at: 10.days.ago, ends_at: 5.days.from_now) }
+      let!(:clara_announcement) { Fabricate(:announcement, starts_at: 10.days.ago, scheduled_at: 10.days.ago, published_at: 100.days.ago, ends_at: 5.days.from_now) }
+      let!(:darnelle_announcement) { Fabricate(:announcement, starts_at: 10.days.ago, scheduled_at: 10.days.ago, published_at: 10.days.ago, ends_at: 5.days.from_now, created_at: 100.days.ago) }
+
+      describe '#chronological' do
+        it 'orders the records correctly' do
+          results = described_class.chronological
+
+          expect(results).to eq(
+            [
+              adam_announcement,
+              brenda_announcement,
+              clara_announcement,
+              darnelle_announcement,
+            ]
+          )
+        end
+      end
+
+      describe '#reverse_chronological' do
+        it 'orders the records correctly' do
+          results = described_class.reverse_chronological
+
+          expect(results).to eq(
+            [
+              darnelle_announcement,
+              clara_announcement,
+              brenda_announcement,
+              adam_announcement,
+            ]
+          )
+        end
+      end
+    end
+  end
+
+  describe 'Validations' do
+    describe 'text' do
+      it 'validates presence of attribute' do
+        record = Fabricate.build(:announcement, text: nil)
+
+        expect(record).to_not be_valid
+        expect(record.errors[:text]).to be_present
+      end
+    end
+
+    describe 'ends_at' do
+      it 'validates presence when starts_at is present' do
+        record = Fabricate.build(:announcement, starts_at: 1.day.ago)
+
+        expect(record).to_not be_valid
+        expect(record.errors[:ends_at]).to be_present
+      end
+
+      it 'does not validate presence when starts_at is missing' do
+        record = Fabricate.build(:announcement, starts_at: nil)
+
+        expect(record).to be_valid
+        expect(record.errors[:ends_at]).to_not be_present
+      end
+    end
+  end
+
+  describe '#publish!' do
+    it 'publishes an unpublished record' do
+      announcement = Fabricate(:announcement, published: false, scheduled_at: 10.days.from_now)
+
+      announcement.publish!
+
+      expect(announcement).to be_published
+      expect(announcement.published_at).to_not be_nil
+      expect(announcement.scheduled_at).to be_nil
+    end
+  end
+
+  describe '#unpublish!' do
+    it 'unpublishes a published record' do
+      announcement = Fabricate(:announcement, published: true)
+
+      announcement.unpublish!
+
+      expect(announcement).to_not be_published
+      expect(announcement.scheduled_at).to be_nil
+    end
+  end
+
+  describe '#time_range?' do
+    it 'returns false when starts_at and ends_at are missing' do
+      record = Fabricate.build(:announcement, starts_at: nil, ends_at: nil)
+
+      expect(record.time_range?).to be(false)
+    end
+
+    it 'returns false when starts_at is present and ends_at is missing' do
+      record = Fabricate.build(:announcement, starts_at: 5.days.from_now, ends_at: nil)
+
+      expect(record.time_range?).to be(false)
+    end
+
+    it 'returns false when starts_at is missing and ends_at is present' do
+      record = Fabricate.build(:announcement, starts_at: nil, ends_at: 5.days.from_now)
+
+      expect(record.time_range?).to be(false)
+    end
+
+    it 'returns true when starts_at and ends_at are present' do
+      record = Fabricate.build(:announcement, starts_at: 5.days.from_now, ends_at: 10.days.from_now)
+
+      expect(record.time_range?).to be(true)
+    end
+  end
+
+  describe '#reactions' do
+    context 'with announcement_reactions present' do
+      let!(:account) { Fabricate(:account) }
+      let!(:announcement) { Fabricate(:announcement) }
+      let!(:announcement_reaction) { Fabricate(:announcement_reaction, announcement: announcement, created_at: 10.days.ago) }
+      let!(:announcement_reaction_account) { Fabricate(:announcement_reaction, announcement: announcement, created_at: 5.days.ago, account: account) }
+
+      before do
+        Fabricate(:announcement_reaction)
+      end
+
+      it 'returns the announcement reactions for the announcement' do
+        results = announcement.reactions
+
+        expect(results.first.name).to eq(announcement_reaction.name)
+        expect(results.last.name).to eq(announcement_reaction_account.name)
+      end
+
+      it 'returns the announcement reactions for the announcement limited to account' do
+        results = announcement.reactions(account)
+
+        expect(results.first.name).to eq(announcement_reaction.name)
+      end
+    end
+  end
+
+  describe '#statuses' do
+    let(:announcement) { Fabricate(:announcement, status_ids: status_ids) }
+
+    context 'with empty status_ids' do
+      let(:status_ids) { nil }
+
+      it 'returns empty array' do
+        results = announcement.statuses
+
+        expect(results).to eq([])
+      end
+    end
+
+    context 'with relevant status_ids' do
+      let(:status) { Fabricate(:status, visibility: :public) }
+      let(:direct_status) { Fabricate(:status, visibility: :direct) }
+      let(:status_ids) { [status.id, direct_status.id] }
+
+      it 'returns public and unlisted statuses' do
+        results = announcement.statuses
+
+        expect(results).to include(status)
+        expect(results).to_not include(direct_status)
+      end
+    end
+  end
+end
diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb
index 8a6487c32..06affd634 100644
--- a/spec/models/custom_emoji_spec.rb
+++ b/spec/models/custom_emoji_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe CustomEmoji do
   describe '.from_text' do
     subject { described_class.from_text(text, nil) }
 
-    let!(:emojo) { Fabricate(:custom_emoji) }
+    let!(:emojo) { Fabricate(:custom_emoji, shortcode: 'coolcat') }
 
     context 'with plain text' do
       let(:text) { 'Hello :coolcat:' }
diff --git a/spec/requests/api/v1/custom_emojis_spec.rb b/spec/requests/api/v1/custom_emojis_spec.rb
index 5de0dda0b..2f0dc7294 100644
--- a/spec/requests/api/v1/custom_emojis_spec.rb
+++ b/spec/requests/api/v1/custom_emojis_spec.rb
@@ -9,7 +9,7 @@ describe 'Custom Emojis' do
 
   describe 'GET /api/v1/custom_emojis' do
     before do
-      Fabricate(:custom_emoji, domain: nil, disabled: false, visible_in_picker: true)
+      Fabricate(:custom_emoji, domain: nil, disabled: false, visible_in_picker: true, shortcode: 'coolcat')
     end
 
     context 'when logged out' do