123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 |
- # frozen_string_literal: true
- require 'rails_helper'
- RSpec.describe FeedManager do
- subject { described_class.instance }
- before do |example|
- unless example.metadata[:skip_stub]
- stub_const 'FeedManager::MAX_ITEMS', 10
- stub_const 'FeedManager::REBLOG_FALLOFF', 4
- end
- end
- it 'tracks at least as many statuses as reblogs', :skip_stub do
- expect(described_class::REBLOG_FALLOFF).to be <= described_class::MAX_ITEMS
- end
- describe '#key' do
- subject { described_class.instance.key(:home, 1) }
- it 'returns a string' do
- expect(subject).to be_a String
- end
- end
- describe '#filter?' do
- let(:alice) { Fabricate(:account, username: 'alice') }
- let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') }
- let(:jeff) { Fabricate(:account, username: 'jeff') }
- let(:list) { Fabricate(:list, account: alice) }
- context 'with home feed' do
- it 'returns false for followee\'s status' do
- status = Fabricate(:status, text: 'Hello world', account: alice)
- bob.follow!(alice)
- expect(subject.filter?(:home, status, bob)).to be false
- end
- it 'returns false for reblog by followee' do
- status = Fabricate(:status, text: 'Hello world', account: jeff)
- reblog = Fabricate(:status, reblog: status, account: alice)
- bob.follow!(alice)
- expect(subject.filter?(:home, reblog, bob)).to be false
- end
- it 'returns true for post from account who blocked me' do
- status = Fabricate(:status, text: 'Hello, World', account: alice)
- alice.block!(bob)
- expect(subject.filter?(:home, status, bob)).to be true
- end
- it 'returns true for post from blocked account' do
- status = Fabricate(:status, text: 'Hello, World', account: alice)
- bob.block!(alice)
- expect(subject.filter?(:home, status, bob)).to be true
- end
- it 'returns true for reblog by followee of blocked account' do
- status = Fabricate(:status, text: 'Hello world', account: jeff)
- reblog = Fabricate(:status, reblog: status, account: alice)
- bob.follow!(alice)
- bob.block!(jeff)
- expect(subject.filter?(:home, reblog, bob)).to be true
- end
- it 'returns true for reblog by followee of muted account' do
- status = Fabricate(:status, text: 'Hello world', account: jeff)
- reblog = Fabricate(:status, reblog: status, account: alice)
- bob.follow!(alice)
- bob.mute!(jeff)
- expect(subject.filter?(:home, reblog, bob)).to be true
- end
- it 'returns true for reblog by followee of someone who is blocking recipient' do
- status = Fabricate(:status, text: 'Hello world', account: jeff)
- reblog = Fabricate(:status, reblog: status, account: alice)
- bob.follow!(alice)
- jeff.block!(bob)
- expect(subject.filter?(:home, reblog, bob)).to be true
- end
- it 'returns true for reblog from account with reblogs disabled' do
- status = Fabricate(:status, text: 'Hello world', account: jeff)
- reblog = Fabricate(:status, reblog: status, account: alice)
- bob.follow!(alice, reblogs: false)
- expect(subject.filter?(:home, reblog, bob)).to be true
- end
- it 'returns false for reply by followee to another followee' do
- status = Fabricate(:status, text: 'Hello world', account: jeff)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
- bob.follow!(alice)
- bob.follow!(jeff)
- expect(subject.filter?(:home, reply, bob)).to be false
- end
- it 'returns false for reply by followee to recipient' do
- status = Fabricate(:status, text: 'Hello world', account: bob)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
- bob.follow!(alice)
- expect(subject.filter?(:home, reply, bob)).to be false
- end
- it 'returns false for reply by followee to self' do
- status = Fabricate(:status, text: 'Hello world', account: alice)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
- bob.follow!(alice)
- expect(subject.filter?(:home, reply, bob)).to be false
- end
- it 'returns true for reply by followee to non-followed account' do
- status = Fabricate(:status, text: 'Hello world', account: jeff)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
- bob.follow!(alice)
- expect(subject.filter?(:home, reply, bob)).to be true
- end
- it 'returns true for the second reply by followee to a non-federated status' do
- reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice)
- second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice)
- bob.follow!(alice)
- expect(subject.filter?(:home, second_reply, bob)).to be true
- end
- it 'returns false for status by followee mentioning another account' do
- bob.follow!(alice)
- jeff.follow!(alice)
- status = PostStatusService.new.call(alice, text: 'Hey @jeff')
- expect(subject.filter?(:home, status, bob)).to be false
- end
- it 'returns true for status by followee mentioning blocked account' do
- bob.block!(jeff)
- bob.follow!(alice)
- status = PostStatusService.new.call(alice, text: 'Hey @jeff')
- expect(subject.filter?(:home, status, bob)).to be true
- end
- it 'returns true for reblog of a personally blocked domain' do
- alice.block_domain!('example.com')
- alice.follow!(jeff)
- status = Fabricate(:status, text: 'Hello world', account: bob)
- reblog = Fabricate(:status, reblog: status, account: jeff)
- expect(subject.filter?(:home, reblog, alice)).to be true
- end
- it 'returns true for German post when follow is set to English only' do
- alice.follow!(bob, languages: %w(en))
- status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
- expect(subject.filter?(:home, status, alice)).to be true
- end
- it 'returns false for German post when follow is set to German' do
- alice.follow!(bob, languages: %w(de))
- status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
- expect(subject.filter?(:home, status, alice)).to be false
- end
- it 'returns true for post from followee on exclusive list' do
- list.exclusive = true
- alice.follow!(bob)
- list.accounts << bob
- allow(List).to receive(:where).and_return(list)
- status = Fabricate(:status, text: 'I post a lot', account: bob)
- expect(subject.filter?(:home, status, alice)).to be true
- end
- it 'returns true for reblog from followee on exclusive list' do
- list.exclusive = true
- alice.follow!(jeff)
- list.accounts << jeff
- allow(List).to receive(:where).and_return(list)
- status = Fabricate(:status, text: 'I post a lot', account: bob)
- reblog = Fabricate(:status, reblog: status, account: jeff)
- expect(subject.filter?(:home, reblog, alice)).to be true
- end
- it 'returns false for post from followee on non-exclusive list' do
- list.exclusive = false
- alice.follow!(bob)
- list.accounts << bob
- status = Fabricate(:status, text: 'I post a lot', account: bob)
- expect(subject.filter?(:home, status, alice)).to be false
- end
- it 'returns false for reblog from followee on non-exclusive list' do
- list.exclusive = false
- alice.follow!(jeff)
- list.accounts << jeff
- status = Fabricate(:status, text: 'I post a lot', account: bob)
- reblog = Fabricate(:status, reblog: status, account: jeff)
- expect(subject.filter?(:home, reblog, alice)).to be false
- end
- end
- context 'with mentions feed' do
- it 'returns true for status that mentions blocked account' do
- bob.block!(jeff)
- status = PostStatusService.new.call(alice, text: 'Hey @jeff')
- expect(subject.filter?(:mentions, status, bob)).to be true
- end
- it 'returns true for status that replies to a blocked account' do
- status = Fabricate(:status, text: 'Hello world', account: jeff)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: alice)
- bob.block!(jeff)
- expect(subject.filter?(:mentions, reply, bob)).to be true
- end
- it 'returns false for status by limited account who recipient is not following' do
- status = Fabricate(:status, text: 'Hello world', account: alice)
- alice.silence!
- expect(subject.filter?(:mentions, status, bob)).to be false
- end
- it 'returns false for status by followed limited account' do
- status = Fabricate(:status, text: 'Hello world', account: alice)
- alice.silence!
- bob.follow!(alice)
- expect(subject.filter?(:mentions, status, bob)).to be false
- end
- end
- end
- describe '#push_to_home' do
- it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do
- account = Fabricate(:account)
- status = Fabricate(:status)
- members = Array.new(described_class::MAX_ITEMS) { |count| [count, count] }
- redis.zadd("feed:home:#{account.id}", members)
- subject.push_to_home(account, status)
- expect(redis.zcard("feed:home:#{account.id}")).to eq described_class::MAX_ITEMS
- end
- context 'with reblogs' do
- it 'saves reblogs of unseen statuses' do
- account = Fabricate(:account)
- reblogged = Fabricate(:status)
- reblog = Fabricate(:status, reblog: reblogged)
- expect(subject.push_to_home(account, reblog)).to be true
- end
- it 'does not save a new reblog of a recent status' do
- account = Fabricate(:account)
- reblogged = Fabricate(:status)
- reblog = Fabricate(:status, reblog: reblogged)
- subject.push_to_home(account, reblogged)
- expect(subject.push_to_home(account, reblog)).to be false
- end
- it 'saves a new reblog of an old status' do
- account = Fabricate(:account)
- reblogged = Fabricate(:status)
- reblog = Fabricate(:status, reblog: reblogged)
- subject.push_to_home(account, reblogged)
- # Fill the feed with intervening statuses
- described_class::REBLOG_FALLOFF.times do
- subject.push_to_home(account, Fabricate(:status))
- end
- expect(subject.push_to_home(account, reblog)).to be true
- end
- it 'does not save a new reblog of a recently-reblogged status' do
- account = Fabricate(:account)
- reblogged = Fabricate(:status)
- reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) }
- # The first reblog will be accepted
- subject.push_to_home(account, reblogs.first)
- # The second reblog should be ignored
- expect(subject.push_to_home(account, reblogs.last)).to be false
- end
- it 'saves a new reblog of a recently-reblogged status when previous reblog has been deleted' do
- account = Fabricate(:account)
- reblogged = Fabricate(:status)
- old_reblog = Fabricate(:status, reblog: reblogged)
- # The first reblog should be accepted
- expect(subject.push_to_home(account, old_reblog)).to be true
- # The first reblog should be successfully removed
- expect(subject.unpush_from_home(account, old_reblog)).to be true
- reblog = Fabricate(:status, reblog: reblogged)
- # The second reblog should be accepted
- expect(subject.push_to_home(account, reblog)).to be true
- end
- it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do
- account = Fabricate(:account)
- reblogged = Fabricate(:status)
- reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) }
- # Accept the reblogs
- subject.push_to_home(account, reblogs[0])
- subject.push_to_home(account, reblogs[1])
- # Unreblog the first one
- subject.unpush_from_home(account, reblogs[0])
- # The last reblog should still be ignored
- expect(subject.push_to_home(account, reblogs.last)).to be false
- end
- it 'saves a new reblog of a long-ago-reblogged status' do
- account = Fabricate(:account)
- reblogged = Fabricate(:status)
- reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) }
- # The first reblog will be accepted
- subject.push_to_home(account, reblogs.first)
- # Fill the feed with intervening statuses
- described_class::REBLOG_FALLOFF.times do
- subject.push_to_home(account, Fabricate(:status))
- end
- # The second reblog should also be accepted
- expect(subject.push_to_home(account, reblogs.last)).to be true
- end
- end
- it "does not push when the given status's reblog is already inserted" do
- account = Fabricate(:account)
- reblog = Fabricate(:status)
- status = Fabricate(:status, reblog: reblog)
- subject.push_to_home(account, status)
- expect(subject.push_to_home(account, reblog)).to be false
- end
- end
- describe '#push_to_list' do
- let(:list_owner) { Fabricate(:account, username: 'list_owner') }
- let(:alice) { Fabricate(:account, username: 'alice') }
- let(:bob) { Fabricate(:account, username: 'bob') }
- let(:eve) { Fabricate(:account, username: 'eve') }
- let(:list) { Fabricate(:list, account: list_owner) }
- before do
- list_owner.follow!(alice)
- list_owner.follow!(bob)
- list_owner.follow!(eve)
- list.accounts << alice
- list.accounts << bob
- end
- it "does not push when the given status's reblog is already inserted" do
- reblog = Fabricate(:status)
- status = Fabricate(:status, reblog: reblog)
- subject.push_to_list(list, status)
- expect(subject.push_to_list(list, reblog)).to be false
- end
- context 'when replies policy is set to no replies' do
- before do
- list.replies_policy = :none
- end
- it 'pushes statuses that are not replies' do
- status = Fabricate(:status, text: 'Hello world', account: bob)
- expect(subject.push_to_list(list, status)).to be true
- end
- it 'pushes statuses that are replies to list owner' do
- status = Fabricate(:status, text: 'Hello world', account: list_owner)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
- expect(subject.push_to_list(list, reply)).to be true
- end
- it 'does not push replies to another member of the list' do
- status = Fabricate(:status, text: 'Hello world', account: alice)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
- expect(subject.push_to_list(list, reply)).to be false
- end
- end
- context 'when replies policy is set to list-only replies' do
- before do
- list.replies_policy = :list
- end
- it 'pushes statuses that are not replies' do
- status = Fabricate(:status, text: 'Hello world', account: bob)
- expect(subject.push_to_list(list, status)).to be true
- end
- it 'pushes statuses that are replies to list owner' do
- status = Fabricate(:status, text: 'Hello world', account: list_owner)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
- expect(subject.push_to_list(list, reply)).to be true
- end
- it 'pushes replies to another member of the list' do
- status = Fabricate(:status, text: 'Hello world', account: alice)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
- expect(subject.push_to_list(list, reply)).to be true
- end
- it 'does not push replies to someone not a member of the list' do
- status = Fabricate(:status, text: 'Hello world', account: eve)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
- expect(subject.push_to_list(list, reply)).to be false
- end
- end
- context 'when replies policy is set to any reply' do
- before do
- list.replies_policy = :followed
- end
- it 'pushes statuses that are not replies' do
- status = Fabricate(:status, text: 'Hello world', account: bob)
- expect(subject.push_to_list(list, status)).to be true
- end
- it 'pushes statuses that are replies to list owner' do
- status = Fabricate(:status, text: 'Hello world', account: list_owner)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
- expect(subject.push_to_list(list, reply)).to be true
- end
- it 'pushes replies to another member of the list' do
- status = Fabricate(:status, text: 'Hello world', account: alice)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
- expect(subject.push_to_list(list, reply)).to be true
- end
- it 'pushes replies to someone not a member of the list' do
- status = Fabricate(:status, text: 'Hello world', account: eve)
- reply = Fabricate(:status, text: 'Nay', thread: status, account: bob)
- expect(subject.push_to_list(list, reply)).to be true
- end
- end
- end
- describe '#merge_into_home' do
- it "does not push source account's statuses whose reblogs are already inserted" do
- account = Fabricate(:account, id: 0)
- reblog = Fabricate(:status)
- status = Fabricate(:status, reblog: reblog)
- subject.push_to_home(account, status)
- subject.merge_into_home(account, reblog.account)
- expect(redis.zscore('feed:home:0', reblog.id)).to be_nil
- end
- end
- describe '#unpush_from_home' do
- let(:receiver) { Fabricate(:account) }
- it 'leaves a reblogged status if original was on feed' do
- reblogged = Fabricate(:status)
- status = Fabricate(:status, reblog: reblogged)
- subject.push_to_home(receiver, reblogged)
- described_class::REBLOG_FALLOFF.times { subject.push_to_home(receiver, Fabricate(:status)) }
- subject.push_to_home(receiver, status)
- # The reblogging status should show up under normal conditions.
- expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
- subject.unpush_from_home(receiver, status)
- # Restore original status
- expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
- expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(reblogged.id.to_s)
- end
- it 'removes a reblogged status if it was only reblogged once' do
- reblogged = Fabricate(:status)
- status = Fabricate(:status, reblog: reblogged)
- subject.push_to_home(receiver, status)
- # The reblogging status should show up under normal conditions.
- expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s]
- subject.unpush_from_home(receiver, status)
- expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty
- end
- it 'leaves a multiply-reblogged status if another reblog was in feed' do
- reblogged = Fabricate(:status)
- reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) }
- reblogs.each do |reblog|
- subject.push_to_home(receiver, reblog)
- end
- # The reblogging status should show up under normal conditions.
- expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s]
- reblogs[0...-1].each do |reblog|
- subject.unpush_from_home(receiver, reblog)
- end
- expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s]
- end
- it 'sends push updates' do
- status = Fabricate(:status)
- subject.push_to_home(receiver, status)
- allow(redis).to receive_messages(publish: nil)
- subject.unpush_from_home(receiver, status)
- deletion = Oj.dump(event: :delete, payload: status.id.to_s)
- expect(redis).to have_received(:publish).with("timeline:#{receiver.id}", deletion)
- end
- end
- describe '#unmerge_tag_from_home' do
- let(:receiver) { Fabricate(:account) }
- let(:tag) { Fabricate(:tag) }
- it 'leaves a tagged status' do
- status = Fabricate(:status)
- status.tags << tag
- subject.push_to_home(receiver, status)
- subject.unmerge_tag_from_home(tag, receiver)
- expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
- end
- it 'remains a tagged status written by receiver\'s followee' do
- followee = Fabricate(:account)
- receiver.follow!(followee)
- status = Fabricate(:status, account: followee)
- status.tags << tag
- subject.push_to_home(receiver, status)
- subject.unmerge_tag_from_home(tag, receiver)
- expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
- end
- it 'remains a tagged status written by receiver' do
- status = Fabricate(:status, account: receiver)
- status.tags << tag
- subject.push_to_home(receiver, status)
- subject.unmerge_tag_from_home(tag, receiver)
- expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
- end
- end
- describe '#clear_from_home' do
- let(:account) { Fabricate(:account) }
- let(:followed_account) { Fabricate(:account) }
- let(:target_account) { Fabricate(:account) }
- let(:status_from_followed_account_first) { Fabricate(:status, account: followed_account) }
- let(:status_from_target_account) { Fabricate(:status, account: target_account) }
- let(:status_from_followed_account_mentions_target_account) { Fabricate(:status, account: followed_account, mentions: [Fabricate(:mention, account: target_account)]) }
- let(:status_mentions_target_account) { Fabricate(:status, mentions: [Fabricate(:mention, account: target_account)]) }
- let(:status_from_followed_account_reblogs_status_mentions_target_account) { Fabricate(:status, account: followed_account, reblog: status_mentions_target_account) }
- let(:status_from_followed_account_reblogs_status_from_target_account) { Fabricate(:status, account: followed_account, reblog: status_from_target_account) }
- let(:status_from_followed_account_next) { Fabricate(:status, account: followed_account) }
- before do
- [
- status_from_followed_account_first,
- status_from_followed_account_mentions_target_account,
- status_from_followed_account_reblogs_status_mentions_target_account,
- status_from_followed_account_reblogs_status_from_target_account,
- status_from_followed_account_next,
- ].each do |status|
- redis.zadd("feed:home:#{account.id}", status.id, status.id)
- end
- end
- it 'correctly cleans the home timeline' do
- subject.clear_from_home(account, target_account)
- expect(redis.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_from_followed_account_first.id.to_s, status_from_followed_account_next.id.to_s]
- end
- end
- end
|