diff options
Diffstat (limited to 'spec')
5 files changed, 804 insertions, 0 deletions
diff --git a/spec/controllers/statuses_cleanup_controller_spec.rb b/spec/controllers/statuses_cleanup_controller_spec.rb new file mode 100644 index 000000000..924709260 --- /dev/null +++ b/spec/controllers/statuses_cleanup_controller_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe StatusesCleanupController, type: :controller do + render_views + + before do + @user = Fabricate(:user) + 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 + + describe 'PUT #update' do + it 'updates the account status cleanup policy' do + put :update, params: { account_statuses_cleanup_policy: { enabled: true, min_status_age: 2.weeks.seconds, keep_direct: false, keep_polls: true } } + expect(response).to redirect_to(statuses_cleanup_path) + expect(@user.account.statuses_cleanup_policy.enabled).to eq true + expect(@user.account.statuses_cleanup_policy.keep_direct).to eq false + expect(@user.account.statuses_cleanup_policy.keep_polls).to eq true + end + end +end diff --git a/spec/fabricators/account_statuses_cleanup_policy_fabricator.rb b/spec/fabricators/account_statuses_cleanup_policy_fabricator.rb new file mode 100644 index 000000000..29cf1d133 --- /dev/null +++ b/spec/fabricators/account_statuses_cleanup_policy_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:account_statuses_cleanup_policy) do + account +end diff --git a/spec/models/account_statuses_cleanup_policy_spec.rb b/spec/models/account_statuses_cleanup_policy_spec.rb new file mode 100644 index 000000000..63e9c5d20 --- /dev/null +++ b/spec/models/account_statuses_cleanup_policy_spec.rb @@ -0,0 +1,546 @@ +require 'rails_helper' + +RSpec.describe AccountStatusesCleanupPolicy, type: :model do + let(:account) { Fabricate(:account, username: 'alice', domain: nil) } + + describe 'validation' do + it 'disallow remote accounts' do + account.update(domain: 'example.com') + account_statuses_cleanup_policy = Fabricate.build(:account_statuses_cleanup_policy, account: account) + account_statuses_cleanup_policy.valid? + expect(account_statuses_cleanup_policy).to model_have_error_on_field(:account) + end + end + + describe 'save hooks' do + context 'when widening a policy' do + let!(:account_statuses_cleanup_policy) do + Fabricate(:account_statuses_cleanup_policy, + account: account, + keep_direct: true, + keep_pinned: true, + keep_polls: true, + keep_media: true, + keep_self_fav: true, + keep_self_bookmark: true, + min_favs: 1, + min_reblogs: 1 + ) + end + + before do + account_statuses_cleanup_policy.record_last_inspected(42) + end + + it 'invalidates last_inspected when widened because of keep_direct' do + account_statuses_cleanup_policy.keep_direct = false + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to be nil + end + + it 'invalidates last_inspected when widened because of keep_pinned' do + account_statuses_cleanup_policy.keep_pinned = false + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to be nil + end + + it 'invalidates last_inspected when widened because of keep_polls' do + account_statuses_cleanup_policy.keep_polls = false + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to be nil + end + + it 'invalidates last_inspected when widened because of keep_media' do + account_statuses_cleanup_policy.keep_media = false + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to be nil + end + + it 'invalidates last_inspected when widened because of keep_self_fav' do + account_statuses_cleanup_policy.keep_self_fav = false + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to be nil + end + + it 'invalidates last_inspected when widened because of keep_self_bookmark' do + account_statuses_cleanup_policy.keep_self_bookmark = false + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to be nil + end + + it 'invalidates last_inspected when widened because of higher min_favs' do + account_statuses_cleanup_policy.min_favs = 5 + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to be nil + end + + it 'invalidates last_inspected when widened because of disabled min_favs' do + account_statuses_cleanup_policy.min_favs = nil + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to be nil + end + + it 'invalidates last_inspected when widened because of higher min_reblogs' do + account_statuses_cleanup_policy.min_reblogs = 5 + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to be nil + end + + it 'invalidates last_inspected when widened because of disable min_reblogs' do + account_statuses_cleanup_policy.min_reblogs = nil + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to be nil + end + end + + context 'when narrowing a policy' do + let!(:account_statuses_cleanup_policy) do + Fabricate(:account_statuses_cleanup_policy, + account: account, + keep_direct: false, + keep_pinned: false, + keep_polls: false, + keep_media: false, + keep_self_fav: false, + keep_self_bookmark: false, + min_favs: nil, + min_reblogs: nil + ) + end + + it 'does not unnecessarily invalidate last_inspected' do + account_statuses_cleanup_policy.record_last_inspected(42) + account_statuses_cleanup_policy.keep_direct = true + account_statuses_cleanup_policy.keep_pinned = true + account_statuses_cleanup_policy.keep_polls = true + account_statuses_cleanup_policy.keep_media = true + account_statuses_cleanup_policy.keep_self_fav = true + account_statuses_cleanup_policy.keep_self_bookmark = true + account_statuses_cleanup_policy.min_favs = 5 + account_statuses_cleanup_policy.min_reblogs = 5 + account_statuses_cleanup_policy.save + expect(account_statuses_cleanup_policy.last_inspected).to eq 42 + end + end + end + + describe '#record_last_inspected' do + let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } + + it 'records the given id' do + account_statuses_cleanup_policy.record_last_inspected(42) + expect(account_statuses_cleanup_policy.last_inspected).to eq 42 + end + end + + describe '#invalidate_last_inspected' do + let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } + let(:status) { Fabricate(:status, id: 10, account: account) } + subject { account_statuses_cleanup_policy.invalidate_last_inspected(status, action) } + + before do + account_statuses_cleanup_policy.record_last_inspected(42) + end + + context 'when the action is :unbookmark' do + let(:action) { :unbookmark } + + context 'when the policy is not to keep self-bookmarked toots' do + before do + account_statuses_cleanup_policy.keep_self_bookmark = false + end + + it 'does not change the recorded id' do + subject + expect(account_statuses_cleanup_policy.last_inspected).to eq 42 + end + end + + context 'when the policy is to keep self-bookmarked toots' do + before do + account_statuses_cleanup_policy.keep_self_bookmark = true + end + + it 'records the older id' do + subject + expect(account_statuses_cleanup_policy.last_inspected).to eq 10 + end + end + end + + context 'when the action is :unfav' do + let(:action) { :unfav } + + context 'when the policy is not to keep self-favourited toots' do + before do + account_statuses_cleanup_policy.keep_self_fav = false + end + + it 'does not change the recorded id' do + subject + expect(account_statuses_cleanup_policy.last_inspected).to eq 42 + end + end + + context 'when the policy is to keep self-favourited toots' do + before do + account_statuses_cleanup_policy.keep_self_fav = true + end + + it 'records the older id' do + subject + expect(account_statuses_cleanup_policy.last_inspected).to eq 10 + end + end + end + + context 'when the action is :unpin' do + let(:action) { :unpin } + + context 'when the policy is not to keep pinned toots' do + before do + account_statuses_cleanup_policy.keep_pinned = false + end + + it 'does not change the recorded id' do + subject + expect(account_statuses_cleanup_policy.last_inspected).to eq 42 + end + end + + context 'when the policy is to keep pinned toots' do + before do + account_statuses_cleanup_policy.keep_pinned = true + end + + it 'records the older id' do + subject + expect(account_statuses_cleanup_policy.last_inspected).to eq 10 + end + end + end + + context 'when the status is more recent than the recorded inspected id' do + let(:action) { :unfav } + let(:status) { Fabricate(:status, account: account) } + + it 'does not change the recorded id' do + subject + expect(account_statuses_cleanup_policy.last_inspected).to eq 42 + end + end + end + + describe '#compute_cutoff_id' do + let!(:unrelated_status) { Fabricate(:status, created_at: 3.years.ago) } + let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } + + subject { account_statuses_cleanup_policy.compute_cutoff_id } + + context 'when the account has posted multiple toots' do + let!(:very_old_status) { Fabricate(:status, created_at: 3.years.ago, account: account) } + let!(:old_status) { Fabricate(:status, created_at: 3.weeks.ago, account: account) } + let!(:recent_status) { Fabricate(:status, created_at: 2.days.ago, account: account) } + + it 'returns the most recent id that is still below policy age' do + expect(subject).to eq old_status.id + end + end + + context 'when the account has not posted anything' do + it 'returns nil' do + expect(subject).to be_nil + end + end + end + + describe '#statuses_to_delete' do + let!(:unrelated_status) { Fabricate(:status, created_at: 3.years.ago) } + let!(:very_old_status) { Fabricate(:status, created_at: 3.years.ago, account: account) } + let!(:pinned_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:direct_message) { Fabricate(:status, created_at: 1.year.ago, account: account, visibility: :direct) } + let!(:self_faved) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:self_bookmarked) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:status_with_poll) { Fabricate(:status, created_at: 1.year.ago, account: account, poll_attributes: { account: account, voters_count: 0, options: ['a', 'b'], expires_in: 2.days }) } + let!(:status_with_media) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:faved4) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:faved5) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:reblogged4) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:reblogged5) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:recent_status) { Fabricate(:status, created_at: 2.days.ago, account: account) } + + let!(:media_attachment) { Fabricate(:media_attachment, account: account, status: status_with_media) } + let!(:status_pin) { Fabricate(:status_pin, account: account, status: pinned_status) } + let!(:favourite) { Fabricate(:favourite, account: account, status: self_faved) } + let!(:bookmark) { Fabricate(:bookmark, account: account, status: self_bookmarked) } + + let(:account_statuses_cleanup_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } + + subject { account_statuses_cleanup_policy.statuses_to_delete } + + before do + 4.times { faved4.increment_count!(:favourites_count) } + 5.times { faved5.increment_count!(:favourites_count) } + 4.times { reblogged4.increment_count!(:reblogs_count) } + 5.times { reblogged5.increment_count!(:reblogs_count) } + end + + context 'when passed a max_id' do + let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) } + + subject { account_statuses_cleanup_policy.statuses_to_delete(50, old_status.id).pluck(:id) } + + it 'returns statuses including max_id' do + expect(subject).to include(old_status.id) + end + + it 'returns statuses including older than max_id' do + expect(subject).to include(very_old_status.id) + end + + it 'does not return statuses newer than max_id' do + expect(subject).to_not include(slightly_less_old_status.id) + end + end + + context 'when passed a min_id' do + let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:slightly_less_old_status) { Fabricate(:status, created_at: 6.months.ago, account: account) } + + subject { account_statuses_cleanup_policy.statuses_to_delete(50, recent_status.id, old_status.id).pluck(:id) } + + it 'returns statuses including min_id' do + expect(subject).to include(old_status.id) + end + + it 'returns statuses including newer than max_id' do + expect(subject).to include(slightly_less_old_status.id) + end + + it 'does not return statuses older than min_id' do + expect(subject).to_not include(very_old_status.id) + end + end + + context 'when passed a low limit' do + it 'only returns the limited number of items' do + expect(account_statuses_cleanup_policy.statuses_to_delete(1).count).to eq 1 + end + end + + context 'when policy is set to keep statuses more recent than 2 years' do + before do + account_statuses_cleanup_policy.min_status_age = 2.years.seconds + end + + it 'does not return unrelated old status' do + expect(subject.pluck(:id)).to_not include(unrelated_status.id) + end + + it 'returns only oldest status for deletion' do + expect(subject.pluck(:id)).to eq [very_old_status.id] + end + end + + context 'when policy is set to keep DMs and reject everything else' do + before do + account_statuses_cleanup_policy.keep_direct = true + account_statuses_cleanup_policy.keep_pinned = false + account_statuses_cleanup_policy.keep_polls = false + account_statuses_cleanup_policy.keep_media = false + account_statuses_cleanup_policy.keep_self_fav = false + account_statuses_cleanup_policy.keep_self_bookmark = false + end + + it 'does not return the old direct message for deletion' do + expect(subject.pluck(:id)).to_not include(direct_message.id) + end + + it 'returns every other old status for deletion' do + expect(subject.pluck(:id)).to include(very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) + end + end + + context 'when policy is set to keep self-bookmarked toots and reject everything else' do + before do + account_statuses_cleanup_policy.keep_direct = false + account_statuses_cleanup_policy.keep_pinned = false + account_statuses_cleanup_policy.keep_polls = false + account_statuses_cleanup_policy.keep_media = false + account_statuses_cleanup_policy.keep_self_fav = false + account_statuses_cleanup_policy.keep_self_bookmark = true + end + + it 'does not return the old self-bookmarked message for deletion' do + expect(subject.pluck(:id)).to_not include(self_bookmarked.id) + end + + it 'returns every other old status for deletion' do + expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, status_with_poll.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) + end + end + + context 'when policy is set to keep self-faved toots and reject everything else' do + before do + account_statuses_cleanup_policy.keep_direct = false + account_statuses_cleanup_policy.keep_pinned = false + account_statuses_cleanup_policy.keep_polls = false + account_statuses_cleanup_policy.keep_media = false + account_statuses_cleanup_policy.keep_self_fav = true + account_statuses_cleanup_policy.keep_self_bookmark = false + end + + it 'does not return the old self-bookmarked message for deletion' do + expect(subject.pluck(:id)).to_not include(self_faved.id) + end + + it 'returns every other old status for deletion' do + expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) + end + end + + context 'when policy is set to keep toots with media and reject everything else' do + before do + account_statuses_cleanup_policy.keep_direct = false + account_statuses_cleanup_policy.keep_pinned = false + account_statuses_cleanup_policy.keep_polls = false + account_statuses_cleanup_policy.keep_media = true + account_statuses_cleanup_policy.keep_self_fav = false + account_statuses_cleanup_policy.keep_self_bookmark = false + end + + it 'does not return the old message with media for deletion' do + expect(subject.pluck(:id)).to_not include(status_with_media.id) + end + + it 'returns every other old status for deletion' do + expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) + end + end + + context 'when policy is set to keep toots with polls and reject everything else' do + before do + account_statuses_cleanup_policy.keep_direct = false + account_statuses_cleanup_policy.keep_pinned = false + account_statuses_cleanup_policy.keep_polls = true + account_statuses_cleanup_policy.keep_media = false + account_statuses_cleanup_policy.keep_self_fav = false + account_statuses_cleanup_policy.keep_self_bookmark = false + end + + it 'does not return the old poll message for deletion' do + expect(subject.pluck(:id)).to_not include(status_with_poll.id) + end + + it 'returns every other old status for deletion' do + expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) + end + end + + context 'when policy is set to keep pinned toots and reject everything else' do + before do + account_statuses_cleanup_policy.keep_direct = false + account_statuses_cleanup_policy.keep_pinned = true + account_statuses_cleanup_policy.keep_polls = false + account_statuses_cleanup_policy.keep_media = false + account_statuses_cleanup_policy.keep_self_fav = false + account_statuses_cleanup_policy.keep_self_bookmark = false + end + + it 'does not return the old pinned message for deletion' do + expect(subject.pluck(:id)).to_not include(pinned_status.id) + end + + it 'returns every other old status for deletion' do + expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) + end + end + + context 'when policy is to not keep any special messages' do + before do + account_statuses_cleanup_policy.keep_direct = false + account_statuses_cleanup_policy.keep_pinned = false + account_statuses_cleanup_policy.keep_polls = false + account_statuses_cleanup_policy.keep_media = false + account_statuses_cleanup_policy.keep_self_fav = false + account_statuses_cleanup_policy.keep_self_bookmark = false + end + + it 'does not return the recent toot' do + expect(subject.pluck(:id)).to_not include(recent_status.id) + end + + it 'does not return the unrelated toot' do + expect(subject.pluck(:id)).to_not include(unrelated_status.id) + end + + it 'returns every other old status for deletion' do + expect(subject.pluck(:id)).to include(direct_message.id, very_old_status.id, pinned_status.id, self_faved.id, self_bookmarked.id, status_with_poll.id, status_with_media.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id) + end + end + + context 'when policy is set to keep every category of toots' do + before do + account_statuses_cleanup_policy.keep_direct = true + account_statuses_cleanup_policy.keep_pinned = true + account_statuses_cleanup_policy.keep_polls = true + account_statuses_cleanup_policy.keep_media = true + account_statuses_cleanup_policy.keep_self_fav = true + account_statuses_cleanup_policy.keep_self_bookmark = true + end + + it 'does not return unrelated old status' do + expect(subject.pluck(:id)).to_not include(unrelated_status.id) + end + + it 'returns only normal statuses for deletion' do + expect(subject.pluck(:id).sort).to eq [very_old_status.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id].sort + end + end + + context 'when policy is to keep statuses with more than 4 boosts' do + before do + account_statuses_cleanup_policy.min_reblogs = 4 + end + + it 'does not return the recent toot' do + expect(subject.pluck(:id)).to_not include(recent_status.id) + end + + it 'does not return the toot reblogged 5 times' do + expect(subject.pluck(:id)).to_not include(reblogged5.id) + end + + it 'does not return the unrelated toot' do + expect(subject.pluck(:id)).to_not include(unrelated_status.id) + end + + it 'returns old statuses not reblogged as much' do + expect(subject.pluck(:id)).to include(very_old_status.id, faved4.id, faved5.id, reblogged4.id) + end + end + + context 'when policy is to keep statuses with more than 4 favs' do + before do + account_statuses_cleanup_policy.min_favs = 4 + end + + it 'does not return the recent toot' do + expect(subject.pluck(:id)).to_not include(recent_status.id) + end + + it 'does not return the toot faved 5 times' do + expect(subject.pluck(:id)).to_not include(faved5.id) + end + + it 'does not return the unrelated toot' do + expect(subject.pluck(:id)).to_not include(unrelated_status.id) + end + + it 'returns old statuses not faved as much' do + expect(subject.pluck(:id)).to include(very_old_status.id, faved4.id, reblogged4.id, reblogged5.id) + end + end + end +end diff --git a/spec/services/account_statuses_cleanup_service_spec.rb b/spec/services/account_statuses_cleanup_service_spec.rb new file mode 100644 index 000000000..257655c41 --- /dev/null +++ b/spec/services/account_statuses_cleanup_service_spec.rb @@ -0,0 +1,101 @@ +require 'rails_helper' + +describe AccountStatusesCleanupService, type: :service do + let(:account) { Fabricate(:account, username: 'alice', domain: nil) } + let(:account_policy) { Fabricate(:account_statuses_cleanup_policy, account: account) } + let!(:unrelated_status) { Fabricate(:status, created_at: 3.years.ago) } + + describe '#call' do + context 'when the account has not posted anything' do + it 'returns 0 deleted toots' do + expect(subject.call(account_policy)).to eq 0 + end + end + + context 'when the account has posted several old statuses' do + let!(:very_old_status) { Fabricate(:status, created_at: 3.years.ago, account: account) } + let!(:old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:another_old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) } + let!(:recent_status) { Fabricate(:status, created_at: 1.day.ago, account: account) } + + context 'given a budget of 1' do + it 'reports 1 deleted toot' do + expect(subject.call(account_policy, 1)).to eq 1 + end + end + + context 'given a normal budget of 10' do + it 'reports 3 deleted statuses' do + expect(subject.call(account_policy, 10)).to eq 3 + end + + it 'records the last deleted id' do + subject.call(account_policy, 10) + expect(account_policy.last_inspected).to eq [old_status.id, another_old_status.id].max + end + + it 'actually deletes the statuses' do + subject.call(account_policy, 10) + expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil + end + end + + context 'when called repeatedly with a budget of 2' do + it 'reports 2 then 1 deleted statuses' do + expect(subject.call(account_policy, 2)).to eq 2 + expect(subject.call(account_policy, 2)).to eq 1 + end + + it 'actually deletes the statuses in the expected order' do + subject.call(account_policy, 2) + expect(Status.find_by(id: very_old_status.id)).to be_nil + subject.call(account_policy, 2) + expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil + end + end + + context 'when a self-faved toot is unfaved' do + let!(:self_faved) { Fabricate(:status, created_at: 6.months.ago, account: account) } + let!(:favourite) { Fabricate(:favourite, account: account, status: self_faved) } + + it 'deletes it once unfaved' do + expect(subject.call(account_policy, 20)).to eq 3 + expect(Status.find_by(id: self_faved.id)).to_not be_nil + expect(subject.call(account_policy, 20)).to eq 0 + favourite.destroy! + expect(subject.call(account_policy, 20)).to eq 1 + expect(Status.find_by(id: self_faved.id)).to be_nil + end + end + + context 'when there are more un-deletable old toots than the early search cutoff' do + before do + stub_const 'AccountStatusesCleanupPolicy::EARLY_SEARCH_CUTOFF', 5 + # Old statuses that should be cut-off + 10.times do + Fabricate(:status, created_at: 4.years.ago, visibility: :direct, account: account) + end + # New statuses that prevent cut-off id to reach the last status + 10.times do + Fabricate(:status, created_at: 4.seconds.ago, visibility: :direct, account: account) + end + end + + it 'reports 0 deleted statuses then 0 then 3 then 0 again' do + expect(subject.call(account_policy, 10)).to eq 0 + expect(subject.call(account_policy, 10)).to eq 0 + expect(subject.call(account_policy, 10)).to eq 3 + expect(subject.call(account_policy, 10)).to eq 0 + end + + it 'never causes the recorded id to get higher than oldest deletable toot' do + subject.call(account_policy, 10) + subject.call(account_policy, 10) + subject.call(account_policy, 10) + subject.call(account_policy, 10) + expect(account_policy.last_inspected).to be < Mastodon::Snowflake.id_at(account_policy.min_status_age.seconds.ago, with_random: false) + end + end + end + end +end diff --git a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb new file mode 100644 index 000000000..8f20725c8 --- /dev/null +++ b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb @@ -0,0 +1,127 @@ +require 'rails_helper' + +describe Scheduler::AccountsStatusesCleanupScheduler do + subject { described_class.new } + + let!(:account1) { Fabricate(:account, domain: nil) } + let!(:account2) { Fabricate(:account, domain: nil) } + let!(:account3) { Fabricate(:account, domain: nil) } + let!(:account4) { Fabricate(:account, domain: nil) } + let!(:remote) { Fabricate(:account) } + + let!(:policy1) { Fabricate(:account_statuses_cleanup_policy, account: account1) } + let!(:policy2) { Fabricate(:account_statuses_cleanup_policy, account: account3) } + let!(:policy3) { Fabricate(:account_statuses_cleanup_policy, account: account4, enabled: false) } + + let(:queue_size) { 0 } + let(:queue_latency) { 0 } + let(:process_set_stub) do + [ + { + 'concurrency' => 2, + 'queues' => ['push', 'default'], + }, + ] + end + let(:retry_size) { 0 } + + before do + queue_stub = double + allow(queue_stub).to receive(:size).and_return(queue_size) + allow(queue_stub).to receive(:latency).and_return(queue_latency) + allow(Sidekiq::Queue).to receive(:new).and_return(queue_stub) + allow(Sidekiq::ProcessSet).to receive(:new).and_return(process_set_stub) + + sidekiq_stats_stub = double + allow(sidekiq_stats_stub).to receive(:retry_size).and_return(retry_size) + allow(Sidekiq::Stats).to receive(:new).and_return(sidekiq_stats_stub) + + # Create a bunch of old statuses + 10.times do + Fabricate(:status, account: account1, created_at: 3.years.ago) + Fabricate(:status, account: account2, created_at: 3.years.ago) + Fabricate(:status, account: account3, created_at: 3.years.ago) + Fabricate(:status, account: account4, created_at: 3.years.ago) + Fabricate(:status, account: remote, created_at: 3.years.ago) + end + + # Create a bunch of newer statuses + 5.times do + Fabricate(:status, account: account1, created_at: 3.minutes.ago) + Fabricate(:status, account: account2, created_at: 3.minutes.ago) + Fabricate(:status, account: account3, created_at: 3.minutes.ago) + Fabricate(:status, account: account4, created_at: 3.minutes.ago) + Fabricate(:status, account: remote, created_at: 3.minutes.ago) + end + end + + describe '#under_load?' do + context 'when nothing is queued' do + it 'returns false' do + expect(subject.under_load?).to be false + end + end + + context 'when numerous jobs are queued' do + let(:queue_size) { 5 } + let(:queue_latency) { 120 } + + it 'returns true' do + expect(subject.under_load?).to be true + end + end + + context 'when there is a huge amount of jobs to retry' do + let(:retry_size) { 1_000_000 } + + it 'returns true' do + expect(subject.under_load?).to be true + end + end + end + + describe '#get_budget' do + context 'on a single thread' do + let(:process_set_stub) { [ { 'concurrency' => 1, 'queues' => ['push', 'default'] } ] } + + it 'returns a low value' do + expect(subject.compute_budget).to be < 10 + end + end + + context 'on a lot of threads' do + let(:process_set_stub) do + [ + { 'concurrency' => 2, 'queues' => ['push', 'default'] }, + { 'concurrency' => 2, 'queues' => ['push'] }, + { 'concurrency' => 2, 'queues' => ['push'] }, + { 'concurrency' => 2, 'queues' => ['push'] }, + ] + end + + it 'returns a larger value' do + expect(subject.compute_budget).to be > 10 + end + end + end + + describe '#perform' do + context 'when the budget is lower than the number of toots to delete' do + it 'deletes as many statuses as the given budget' do + expect { subject.perform }.to change { Status.count }.by(-subject.compute_budget) + end + + it 'does not delete from accounts with no cleanup policy' do + expect { subject.perform }.to_not change { account2.statuses.count } + end + + it 'does not delete from accounts with disabled cleanup policies' do + expect { subject.perform }.to_not change { account4.statuses.count } + end + + it 'eventually deletes every deletable toot' do + expect { subject.perform; subject.perform; subject.perform; subject.perform }.to change { Status.count }.by(-20) + end + end + end +end |