From 28d309fd860b28b0eaec8d62cab7f3c7b3971138 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 20 Dec 2024 03:21:34 -0500
Subject: [PATCH] Add shared example for `Expireable` concern (#33369)

---
 spec/models/custom_filter_spec.rb             |   2 +
 spec/models/invite_spec.rb                    |   2 +
 spec/models/ip_block_spec.rb                  |   2 +
 spec/models/mute_spec.rb                      |  18 +++
 spec/models/poll_spec.rb                      |   2 +
 .../examples/models/concerns/expireable.rb    | 110 ++++++++++++++++++
 6 files changed, 136 insertions(+)
 create mode 100644 spec/models/mute_spec.rb
 create mode 100644 spec/support/examples/models/concerns/expireable.rb

diff --git a/spec/models/custom_filter_spec.rb b/spec/models/custom_filter_spec.rb
index 517cc4f6a..168cbb7c9 100644
--- a/spec/models/custom_filter_spec.rb
+++ b/spec/models/custom_filter_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 RSpec.describe CustomFilter do
+  include_examples 'Expireable'
+
   describe 'Validations' do
     it { is_expected.to validate_presence_of(:title) }
     it { is_expected.to validate_presence_of(:context) }
diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb
index 4ad589f2c..e85885a8d 100644
--- a/spec/models/invite_spec.rb
+++ b/spec/models/invite_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 RSpec.describe Invite do
+  include_examples 'Expireable'
+
   describe '#valid_for_use?' do
     it 'returns true when there are no limitations' do
       invite = Fabricate(:invite, max_uses: nil, expires_at: nil)
diff --git a/spec/models/ip_block_spec.rb b/spec/models/ip_block_spec.rb
index b85856780..93ee72423 100644
--- a/spec/models/ip_block_spec.rb
+++ b/spec/models/ip_block_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 RSpec.describe IpBlock do
+  include_examples 'Expireable'
+
   describe 'Validations' do
     subject { Fabricate.build :ip_block }
 
diff --git a/spec/models/mute_spec.rb b/spec/models/mute_spec.rb
new file mode 100644
index 000000000..33aa4f15d
--- /dev/null
+++ b/spec/models/mute_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Mute do
+  include_examples 'Expireable'
+
+  describe 'Associations' do
+    it { is_expected.to belong_to(:account).required }
+    it { is_expected.to belong_to(:target_account).required }
+  end
+
+  describe 'Validations' do
+    subject { Fabricate.build :mute }
+
+    it { is_expected.to validate_uniqueness_of(:account_id).scoped_to(:target_account_id) }
+  end
+end
diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb
index 0a4892eba..e4e1a36c4 100644
--- a/spec/models/poll_spec.rb
+++ b/spec/models/poll_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 RSpec.describe Poll do
+  include_examples 'Expireable'
+
   describe 'Scopes' do
     let(:status) { Fabricate(:status) }
     let(:attached_poll) { Fabricate(:poll, status: status) }
diff --git a/spec/support/examples/models/concerns/expireable.rb b/spec/support/examples/models/concerns/expireable.rb
new file mode 100644
index 000000000..098b98375
--- /dev/null
+++ b/spec/support/examples/models/concerns/expireable.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Expireable' do
+  subject { described_class.new(expires_at: expires_at) }
+
+  let(:expires_at) { nil }
+
+  describe 'Scopes' do
+    let!(:expired_record) do
+      travel_to 2.days.ago do
+        Fabricate factory_name, expires_at: 1.day.from_now
+      end
+    end
+    let!(:un_expired_record) { Fabricate factory_name, expires_at: 10.days.from_now }
+
+    describe '.expired' do
+      it 'returns expired records' do
+        expect(described_class.expired)
+          .to include(expired_record)
+          .and not_include(un_expired_record)
+      end
+    end
+  end
+
+  describe '#expires_in' do
+    context 'when expires at is nil' do
+      let(:expires_at) { nil }
+
+      it 'returns nil' do
+        expect(subject.expires_in)
+          .to be_nil
+      end
+    end
+  end
+
+  describe '#expires_in=' do
+    let(:record) { Fabricate.build factory_name }
+
+    context 'when set to nil' do
+      it 'sets expires_at to nil' do
+        record.expires_in = nil
+        expect(record.expires_at)
+          .to be_nil
+      end
+    end
+
+    context 'when set to empty' do
+      it 'sets expires_at to nil' do
+        record.expires_in = ''
+        expect(record.expires_at)
+          .to be_nil
+      end
+    end
+
+    context 'when set to a value' do
+      it 'sets expires_at to expected future time' do
+        record.expires_in = 60
+        expect(record.expires_at)
+          .to be_within(0.1).of(60.seconds.from_now)
+      end
+    end
+  end
+
+  describe '#expired?' do
+    context 'when expires_at is nil' do
+      let(:expires_at) { nil }
+
+      it { is_expected.to_not be_expired }
+    end
+
+    context 'when expires_at is in the past' do
+      let(:expires_at) { 5.days.ago }
+
+      it { is_expected.to be_expired }
+    end
+
+    context 'when expires_at is in the future' do
+      let(:expires_at) { 5.days.from_now }
+
+      it { is_expected.to_not be_expired }
+    end
+
+    describe '#expire!' do
+      subject { Fabricate factory_name }
+
+      it 'updates the timestamp' do
+        expect { subject.expire! }
+          .to change(subject, :expires_at)
+      end
+    end
+
+    describe '#expires?' do
+      context 'when value is missing' do
+        let(:expires_at) { nil }
+
+        it { is_expected.to_not be_expires }
+      end
+
+      context 'when value is present' do
+        let(:expires_at) { 3.days.from_now }
+
+        it { is_expected.to be_expires }
+      end
+    end
+  end
+
+  def factory_name
+    described_class.name.underscore.to_sym
+  end
+end