Merge commit '9c2d5b534f' into upstream-20250314

This commit is contained in:
KMY 2025-03-14 08:35:27 +09:00
commit 6548462ecb
84 changed files with 1719 additions and 418 deletions

View file

@ -1,25 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::Settings::BrandingController do
render_views
describe 'When signed in as an admin' do
before do
sign_in Fabricate(:admin_user), scope: :user
end
describe 'PUT #update' do
it 'cannot create a setting value for a non-admin key' do
expect(Setting.new_setting_key).to be_blank
patch :update, params: { form_admin_settings: { new_setting_key: 'New key value' } }
expect(response)
.to have_http_status(400)
expect(Setting.new_setting_key).to be_nil
end
end
end
end

View file

@ -15,6 +15,7 @@ RSpec.describe Api::Web::PushSubscriptionsController do
p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=',
auth: 'eH_C8rq2raXqlcBVDa1gLg==',
},
standard: standard,
},
}
end
@ -36,6 +37,7 @@ RSpec.describe Api::Web::PushSubscriptionsController do
},
}
end
let(:standard) { '1' }
before do
sign_in(user)
@ -51,14 +53,27 @@ RSpec.describe Api::Web::PushSubscriptionsController do
user.reload
expect(created_push_subscription).to have_attributes(
endpoint: eq(create_payload[:subscription][:endpoint]),
key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]),
key_auth: eq(create_payload[:subscription][:keys][:auth])
)
expect(created_push_subscription)
.to have_attributes(
endpoint: eq(create_payload[:subscription][:endpoint]),
key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]),
key_auth: eq(create_payload[:subscription][:keys][:auth])
)
.and be_standard
expect(user.session_activations.first.web_push_subscription).to eq(created_push_subscription)
end
context 'when standard is provided as false value' do
let(:standard) { '0' }
it 'saves push subscription with standard as false' do
post :create, format: :json, params: create_payload
expect(created_push_subscription)
.to_not be_standard
end
end
context 'with a user who has a session with a prior subscription' do
let!(:prior_subscription) { Fabricate(:web_push_subscription, session_activation: user.session_activations.last) }

View file

@ -3,28 +3,23 @@
require 'rails_helper'
RSpec.describe Mastodon::Feature do
around do |example|
original_value = Rails.configuration.x.mastodon.experimental_features
Rails.configuration.x.mastodon.experimental_features = 'fasp,fetch_all_replies'
example.run
Rails.configuration.x.mastodon.experimental_features = original_value
end
describe '::fasp_enabled?' do
subject { described_class.fasp_enabled? }
it { is_expected.to be true }
end
describe '::fetch_all_replies_enabled?' do
subject { described_class.fetch_all_replies_enabled? }
describe '::testing_only_enabled?' do
subject { described_class.testing_only_enabled? }
it { is_expected.to be true }
end
describe '::unspecified_feature_enabled?' do
subject { described_class.unspecified_feature_enabled? }
context 'when example is not tagged with a feature' do
subject { described_class.unspecified_feature_enabled? }
it { is_expected.to be false }
it { is_expected.to be false }
end
context 'when example is tagged with a feature', feature: 'unspecified_feature' do
subject { described_class.unspecified_feature_enabled? }
it { is_expected.to be true }
end
end
end

View file

@ -0,0 +1,127 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Status::FetchRepliesConcern do
ActiveRecord.verbose_query_logs = true
let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:bob) { Fabricate(:account, username: 'bob', domain: 'other.com') }
let!(:account) { alice }
let!(:status_old) { Fabricate(:status, account: account, fetched_replies_at: 1.year.ago, created_at: 1.year.ago) }
let!(:status_fetched_recently) { Fabricate(:status, account: account, fetched_replies_at: 1.second.ago, created_at: 1.year.ago) }
let!(:status_created_recently) { Fabricate(:status, account: account, created_at: 1.second.ago) }
let!(:status_never_fetched) { Fabricate(:status, account: account, created_at: 1.year.ago) }
describe 'should_fetch_replies' do
let!(:statuses) { Status.should_fetch_replies.all }
context 'with a local status' do
it 'never fetches local replies' do
expect(statuses).to eq([])
end
end
context 'with a remote status' do
let(:account) { bob }
it 'fetches old statuses' do
expect(statuses).to include(status_old)
end
it 'fetches statuses that have never been fetched and weren\'t created recently' do
expect(statuses).to include(status_never_fetched)
end
it 'does not fetch statuses that were fetched recently' do
expect(statuses).to_not include(status_fetched_recently)
end
it 'does not fetch statuses that were created recently' do
expect(statuses).to_not include(status_created_recently)
end
end
end
describe 'should_not_fetch_replies' do
let!(:statuses) { Status.should_not_fetch_replies.all }
context 'with a local status' do
it 'does not fetch local statuses' do
expect(statuses).to include(status_old, status_never_fetched, status_fetched_recently, status_never_fetched)
end
end
context 'with a remote status' do
let(:account) { bob }
it 'fetches old statuses' do
expect(statuses).to_not include(status_old)
end
it 'fetches statuses that have never been fetched and weren\'t created recently' do
expect(statuses).to_not include(status_never_fetched)
end
it 'does not fetch statuses that were fetched recently' do
expect(statuses).to include(status_fetched_recently)
end
it 'does not fetch statuses that were created recently' do
expect(statuses).to include(status_created_recently)
end
end
end
describe 'unsubscribed' do
let!(:spike) { Fabricate(:account, username: 'spike', domain: 'other.com') }
let!(:status) { Fabricate(:status, account: bob, updated_at: 1.day.ago) }
context 'when the status is from an account with only remote followers after last update' do
before do
Fabricate(:follow, account: spike, target_account: bob)
end
it 'shows the status as unsubscribed' do
expect(Status.unsubscribed).to eq([status])
end
end
context 'when the status is from an account with only remote followers before last update' do
before do
Fabricate(:follow, account: spike, target_account: bob, created_at: 2.days.ago)
end
it 'shows the status as unsubscribed' do
expect(Status.unsubscribed).to eq([status])
end
end
context 'when status is from account with local followers after last update' do
before do
Fabricate(:follow, account: alice, target_account: bob)
end
it 'shows the status as unsubscribed' do
expect(Status.unsubscribed).to eq([status])
end
end
context 'when status is from account with local followers before last update' do
before do
Fabricate(:follow, account: alice, target_account: bob, created_at: 2.days.ago)
end
it 'does not show the status as unsubscribed' do
expect(Status.unsubscribed).to eq([])
end
end
context 'when the status has no followers' do
it 'shows the status as unsubscribed' do
expect(Status.unsubscribed).to eq([status])
end
end
end
end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Admin Settings Branding' do
describe 'When signed in as an admin' do
before { sign_in Fabricate(:admin_user) }
describe 'PUT /admin/settings/branding' do
it 'cannot create a setting value for a non-admin key' do
expect { put admin_settings_branding_path, params: { form_admin_settings: { new_setting_key: 'New key value' } } }
.to_not change(Setting, :new_setting_key).from(nil)
expect(response)
.to have_http_status(400)
end
end
end
end

View file

@ -53,8 +53,6 @@ RSpec.describe 'credentials API' do
patch '/api/v1/accounts/update_credentials', headers: headers, params: params
end
before { allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) }
let(:params) do
{
avatar: fixture_file_upload('avatar.gif', 'image/gif'),
@ -113,7 +111,7 @@ RSpec.describe 'credentials API' do
})
expect(ActivityPub::UpdateDistributionWorker)
.to have_received(:perform_async).with(user.account_id)
.to have_enqueued_sidekiq_job(user.account_id)
end
def expect_account_updates

View file

@ -15,10 +15,6 @@ RSpec.describe 'Deleting profile images' do
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
describe 'DELETE /api/v1/profile' do
before do
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
end
context 'when deleting an avatar' do
context 'with wrong scope' do
before do
@ -38,7 +34,8 @@ RSpec.describe 'Deleting profile images' do
account.reload
expect(account.avatar).to_not exist
expect(account.header).to exist
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
expect(ActivityPub::UpdateDistributionWorker)
.to have_enqueued_sidekiq_job(account.id)
end
end
@ -61,7 +58,8 @@ RSpec.describe 'Deleting profile images' do
account.reload
expect(account.avatar).to exist
expect(account.header).to_not exist
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
expect(ActivityPub::UpdateDistributionWorker)
.to have_enqueued_sidekiq_job(account.id)
end
end
end

View file

@ -16,6 +16,7 @@ RSpec.describe 'API V1 Push Subscriptions' do
subscription: {
endpoint: endpoint,
keys: keys,
standard: standard,
},
}.with_indifferent_access
end
@ -36,6 +37,7 @@ RSpec.describe 'API V1 Push Subscriptions' do
},
}.with_indifferent_access
end
let(:standard) { '1' }
let(:scopes) { 'push' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
@ -66,6 +68,7 @@ RSpec.describe 'API V1 Push Subscriptions' do
user_id: eq(user.id),
access_token_id: eq(token.id)
)
.and be_standard
expect(response.parsed_body.with_indifferent_access)
.to include(
@ -73,6 +76,17 @@ RSpec.describe 'API V1 Push Subscriptions' do
)
end
context 'when standard is provided as false value' do
let(:standard) { '0' }
it 'saves push subscription with standard as false' do
subject
expect(endpoint_push_subscription)
.to_not be_standard
end
end
it 'replaces old subscription on repeat calls' do
2.times { subject }

View file

@ -0,0 +1,124 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ActivityPub::FetchAllRepliesService do
subject { described_class.new }
let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
let(:status) { Fabricate(:status, account: actor) }
let(:collection_uri) { 'http://example.com/replies/1' }
let(:items) do
%w(
http://example.com/self-reply-1
http://example.com/self-reply-2
http://example.com/self-reply-3
http://other.com/other-reply-1
http://other.com/other-reply-2
http://other.com/other-reply-3
http://example.com/self-reply-4
http://example.com/self-reply-5
http://example.com/self-reply-6
)
end
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Collection',
id: collection_uri,
items: items,
}.with_indifferent_access
end
describe '#call' do
it 'fetches more than the default maximum and from multiple domains' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status.uri, payload)
expect(FetchReplyWorker).to have_received(:push_bulk).with(
%w(
http://example.com/self-reply-1
http://example.com/self-reply-2
http://example.com/self-reply-3
http://other.com/other-reply-1
http://other.com/other-reply-2
http://other.com/other-reply-3
http://example.com/self-reply-4
http://example.com/self-reply-5
http://example.com/self-reply-6
)
)
end
context 'with a recent status' do
before do
Fabricate(:status, uri: 'http://example.com/self-reply-2', fetched_replies_at: 1.second.ago, local: false)
end
it 'skips statuses that have been updated recently' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status.uri, payload)
expect(FetchReplyWorker).to have_received(:push_bulk).with(
%w(
http://example.com/self-reply-1
http://example.com/self-reply-3
http://other.com/other-reply-1
http://other.com/other-reply-2
http://other.com/other-reply-3
http://example.com/self-reply-4
http://example.com/self-reply-5
http://example.com/self-reply-6
)
)
end
end
context 'with an old status' do
before do
Fabricate(:status, uri: 'http://other.com/other-reply-1', fetched_replies_at: 1.year.ago, created_at: 1.year.ago, account: actor)
end
it 'updates the time that fetched statuses were last fetched' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status.uri, payload)
expect(Status.find_by(uri: 'http://other.com/other-reply-1').fetched_replies_at).to be >= 1.minute.ago
end
end
context 'with unsubscribed replies' do
before do
remote_actor = Fabricate(:account, domain: 'other.com', uri: 'http://other.com/account')
# reply not in the collection from the remote instance, but we know about anyway without anyone following the account
Fabricate(:status, account: remote_actor, in_reply_to_id: status.id, uri: 'http://other.com/account/unsubscribed', fetched_replies_at: 1.year.ago, created_at: 1.year.ago)
end
it 'updates the unsubscribed replies' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status.uri, payload)
expect(FetchReplyWorker).to have_received(:push_bulk).with(
%w(
http://example.com/self-reply-1
http://example.com/self-reply-2
http://example.com/self-reply-3
http://other.com/other-reply-1
http://other.com/other-reply-2
http://other.com/other-reply-3
http://example.com/self-reply-4
http://example.com/self-reply-5
http://example.com/self-reply-6
http://other.com/account/unsubscribed
)
)
end
end
end
end

View file

@ -9,6 +9,9 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
let!(:sender) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar') }
let(:follower) { Fabricate(:account, username: 'alice') }
let(:follow) { nil }
let(:response) { { body: Oj.dump(object), headers: { 'content-type': 'application/activity+json' } } }
let(:existing_status) { nil }
let(:note) do
@ -23,13 +26,14 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
before do
stub_request(:get, 'https://foo.bar/watch?v=12345').to_return(status: 404, body: '')
stub_request(:get, object[:id]).to_return(body: Oj.dump(object))
stub_request(:get, object[:id]).to_return(**response)
end
describe '#call' do
before do
follow
existing_status
subject.call(object[:id], prefetched_body: Oj.dump(object))
subject.call(object[:id])
end
context 'with Note object' do
@ -254,6 +258,45 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
expect(existing_status.text).to eq 'Lorem ipsum'
expect(existing_status.edits).to_not be_empty
end
context 'when the status appears to have been deleted at source' do
let(:response) { { status: 404, body: '' } }
shared_examples 'no delete' do
it 'does not delete the status' do
existing_status.reload
expect(existing_status.text).to eq 'Foo'
expect(existing_status.edits).to be_empty
end
end
context 'when the status is orphaned/unsubscribed' do
it 'deletes the orphaned status' do
expect { existing_status.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'when the status is from an account with only remote followers' do
let(:follower) { Fabricate(:account, username: 'alice', domain: 'foo.bar') }
let(:follow) { Fabricate(:follow, account: follower, target_account: sender, created_at: 2.days.ago) }
it 'deletes the orphaned status' do
expect { existing_status.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
context 'when the status is private' do
let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id], visibility: :private) }
it_behaves_like 'no delete'
end
context 'when the status is direct' do
let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id], visibility: :direct) }
it_behaves_like 'no delete'
end
end
end
end
context 'with a Create activity' do

View file

@ -40,7 +40,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
it 'queues the expected worker' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status, payload)
subject.call(status.account.uri, payload)
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1'])
end
@ -50,7 +50,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
it 'spawns workers for up to 5 replies on the same server' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status, payload)
subject.call(status.account.uri, payload)
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
end
@ -64,7 +64,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
it 'spawns workers for up to 5 replies on the same server' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status, collection_uri)
subject.call(status.account.uri, collection_uri)
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
end
@ -85,7 +85,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
it 'spawns workers for up to 5 replies on the same server' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status, payload)
subject.call(status.account.uri, payload)
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
end
@ -99,7 +99,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
it 'spawns workers for up to 5 replies on the same server' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status, collection_uri)
subject.call(status.account.uri, collection_uri)
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
end
@ -124,7 +124,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
it 'spawns workers for up to 5 replies on the same server' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status, payload)
subject.call(status.account.uri, payload)
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
end
@ -138,7 +138,7 @@ RSpec.describe ActivityPub::FetchRepliesService do
it 'spawns workers for up to 5 replies on the same server' do
allow(FetchReplyWorker).to receive(:push_bulk)
subject.call(status, collection_uri)
subject.call(status.account.uri, collection_uri)
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
RSpec.configure do |config|
config.before(:example, :feature) do |example|
feature = example.metadata[:feature]
allow(Mastodon::Feature).to receive(:"#{feature}_enabled?").and_return(true)
end
end

View file

@ -23,7 +23,11 @@ module ProfileStories
def as_a_logged_in_user
as_a_registered_user
visit new_user_session_path
expect(page)
.to have_title(I18n.t('auth.login'))
fill_in_auth_details(email, password)
expect(page)
.to have_css('.app-holder')
end
def as_a_logged_in_admin

View file

@ -5,8 +5,6 @@ require 'rails_helper'
RSpec.describe 'NewStatuses', :inline_jobs, :js, :streaming do
include ProfileStories
subject { page }
let(:email) { 'test@example.com' }
let(:password) { 'password' }
let(:confirmed_at) { Time.zone.now }
@ -14,13 +12,11 @@ RSpec.describe 'NewStatuses', :inline_jobs, :js, :streaming do
before do
as_a_logged_in_user
visit root_path
page.driver.browser.manage.window.resize_to(1600, 1050)
end
it 'can be posted' do
expect(subject).to have_css('div.app-holder')
visit_homepage
status_text = 'This is a new status!'
within('.compose-form') do
@ -28,12 +24,12 @@ RSpec.describe 'NewStatuses', :inline_jobs, :js, :streaming do
click_on 'Post'
end
expect(subject).to have_css('.status__content__text', text: status_text)
expect(page)
.to have_css('.status__content__text', text: status_text)
end
it 'can be posted again' do
expect(subject).to have_css('div.app-holder')
visit_homepage
status_text = 'This is a second status!'
within('.compose-form') do
@ -41,6 +37,15 @@ RSpec.describe 'NewStatuses', :inline_jobs, :js, :streaming do
click_on 'Post'
end
expect(subject).to have_css('.status__content__text', text: status_text)
expect(page)
.to have_css('.status__content__text', text: status_text)
end
def visit_homepage
visit root_path
expect(page)
.to have_css('div.app-holder')
.and have_css('form.compose-form')
end
end

View file

@ -40,5 +40,7 @@ RSpec.describe 'report interface', :attachment_processing, :js, :streaming do
within '.report-actions' do
click_on I18n.t('admin.reports.mark_as_resolved')
end
expect(page)
.to have_content(I18n.t('admin.reports.resolved_msg'))
end
end

View file

@ -11,8 +11,6 @@ RSpec.describe 'Settings Privacy' do
before { user.account.update(discoverable: false) }
context 'with a successful update' do
before { allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) }
it 'updates user profile information' do
# View settings page
visit settings_privacy_path
@ -29,14 +27,13 @@ RSpec.describe 'Settings Privacy' do
.to have_content(I18n.t('privacy.title'))
.and have_content(success_message)
expect(ActivityPub::UpdateDistributionWorker)
.to have_received(:perform_async).with(user.account.id)
.to have_enqueued_sidekiq_job(user.account.id)
end
end
context 'with a failed update' do
before do
allow(UpdateAccountService).to receive(:new).and_return(failing_update_service)
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
end
it 'updates user profile information' do
@ -54,7 +51,7 @@ RSpec.describe 'Settings Privacy' do
expect(page)
.to have_content(I18n.t('privacy.title'))
expect(ActivityPub::UpdateDistributionWorker)
.to_not have_received(:perform_async)
.to_not have_enqueued_sidekiq_job(anything)
end
private

View file

@ -7,7 +7,6 @@ RSpec.describe 'Settings profile page' do
let(:account) { user.account }
before do
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async)
sign_in user
end
@ -24,7 +23,7 @@ RSpec.describe 'Settings profile page' do
.to change { account.reload.display_name }.to('New name')
.and(change { account.reload.avatar.instance.avatar_file_name }.from(nil).to(be_present))
expect(ActivityPub::UpdateDistributionWorker)
.to have_received(:perform_async).with(account.id)
.to have_enqueued_sidekiq_job(account.id)
end
def display_name_field

View file

@ -0,0 +1,281 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ActivityPub::FetchAllRepliesWorker do
subject { described_class.new }
let(:top_items) do
[
'http://example.com/self-reply-1',
'http://other.com/other-reply-2',
'http://example.com/self-reply-3',
]
end
let(:top_items_paged) do
[
'http://example.com/self-reply-4',
'http://other.com/other-reply-5',
'http://example.com/self-reply-6',
]
end
let(:nested_items) do
[
'http://example.com/nested-self-reply-1',
'http://other.com/nested-other-reply-2',
'http://example.com/nested-self-reply-3',
]
end
let(:nested_items_paged) do
[
'http://example.com/nested-self-reply-4',
'http://other.com/nested-other-reply-5',
'http://example.com/nested-self-reply-6',
]
end
let(:all_items) do
top_items + top_items_paged + nested_items + nested_items_paged
end
let(:top_note_uri) do
'http://example.com/top-post'
end
let(:top_collection_uri) do
'http://example.com/top-post/replies'
end
# The reply uri that has the nested replies under it
let(:reply_note_uri) do
'http://other.com/other-reply-2'
end
# The collection uri of nested replies
let(:reply_collection_uri) do
'http://other.com/other-reply-2/replies'
end
let(:replies_top) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: top_collection_uri,
type: 'Collection',
items: top_items + top_items_paged,
}
end
let(:replies_nested) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: reply_collection_uri,
type: 'Collection',
items: nested_items + nested_items_paged,
}
end
# The status resource for the top post
let(:top_object) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: top_note_uri,
type: 'Note',
content: 'Lorem ipsum',
replies: replies_top,
attributedTo: 'https://example.com',
}
end
# The status resource that has the uri to the replies collection
let(:reply_object) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: reply_note_uri,
type: 'Note',
content: 'Lorem ipsum',
replies: replies_nested,
attributedTo: 'https://other.com',
}
end
let(:empty_object) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://example.com/empty',
type: 'Note',
content: 'Lorem ipsum',
replies: [],
attributedTo: 'https://example.com',
}
end
let(:account) { Fabricate(:account, domain: 'example.com') }
let(:status) do
Fabricate(
:status,
account: account,
uri: top_note_uri,
created_at: 1.day.ago - Status::FetchRepliesConcern::FETCH_REPLIES_INITIAL_WAIT_MINUTES
)
end
before do
stub_const('Status::FetchRepliesConcern::FETCH_REPLIES_ENABLED', true)
allow(FetchReplyWorker).to receive(:push_bulk)
all_items.each do |item|
next if [top_note_uri, reply_note_uri].include? item
stub_request(:get, item).to_return(status: 200, body: Oj.dump(empty_object), headers: { 'Content-Type': 'application/activity+json' })
end
stub_request(:get, top_note_uri).to_return(status: 200, body: Oj.dump(top_object), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, reply_note_uri).to_return(status: 200, body: Oj.dump(reply_object), headers: { 'Content-Type': 'application/activity+json' })
end
shared_examples 'fetches all replies' do
it 'fetches statuses recursively' do
got_uris = subject.perform(status.id)
expect(got_uris).to match_array(all_items)
end
it 'respects the maximum limits set by not recursing after the max is reached' do
stub_const('ActivityPub::FetchAllRepliesWorker::MAX_REPLIES', 5)
got_uris = subject.perform(status.id)
expect(got_uris).to match_array(top_items + top_items_paged)
end
end
describe 'perform' do
context 'when the payload is a Note with replies as a Collection of inlined replies' do
it_behaves_like 'fetches all replies'
end
context 'when the payload is a Note with replies as a URI to a Collection' do
let(:top_object) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: top_note_uri,
type: 'Note',
content: 'Lorem ipsum',
replies: top_collection_uri,
attributedTo: 'https://example.com',
}
end
let(:reply_object) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: reply_note_uri,
type: 'Note',
content: 'Lorem ipsum',
replies: reply_collection_uri,
attributedTo: 'https://other.com',
}
end
before do
stub_request(:get, top_collection_uri).to_return(status: 200, body: Oj.dump(replies_top), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, reply_collection_uri).to_return(status: 200, body: Oj.dump(replies_nested), headers: { 'Content-Type': 'application/activity+json' })
end
it_behaves_like 'fetches all replies'
end
context 'when the payload is a Note with replies as a paginated collection' do
let(:top_page_2_uri) do
"#{top_collection_uri}/2"
end
let(:reply_page_2_uri) do
"#{reply_collection_uri}/2"
end
let(:top_object) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: top_note_uri,
type: 'Note',
content: 'Lorem ipsum',
replies: {
type: 'Collection',
id: top_collection_uri,
first: {
type: 'CollectionPage',
partOf: top_collection_uri,
items: top_items,
next: top_page_2_uri,
},
},
attributedTo: 'https://example.com',
}
end
let(:reply_object) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: reply_note_uri,
type: 'Note',
content: 'Lorem ipsum',
replies: {
type: 'Collection',
id: reply_collection_uri,
first: {
type: 'CollectionPage',
partOf: reply_collection_uri,
items: nested_items,
next: reply_page_2_uri,
},
},
attributedTo: 'https://other.com',
}
end
let(:top_page_two) do
{
type: 'CollectionPage',
id: top_page_2_uri,
partOf: top_collection_uri,
items: top_items_paged,
}
end
let(:reply_page_two) do
{
type: 'CollectionPage',
id: reply_page_2_uri,
partOf: reply_collection_uri,
items: nested_items_paged,
}
end
before do
stub_request(:get, top_page_2_uri).to_return(status: 200, body: Oj.dump(top_page_two), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, reply_page_2_uri).to_return(status: 200, body: Oj.dump(reply_page_two), headers: { 'Content-Type': 'application/activity+json' })
end
it_behaves_like 'fetches all replies'
it 'limits by max pages' do
stub_const('ActivityPub::FetchAllRepliesWorker::MAX_PAGES', 3)
got_uris = subject.perform(status.id)
expect(got_uris).to match_array(top_items + top_items_paged + nested_items)
end
end
context 'when replies should not be fetched' do
# ensure that we should not fetch by setting the status to be created in the debounce window
let(:status) { Fabricate(:status, account: account, uri: top_note_uri, created_at: DateTime.now) }
before do
stub_const('Status::FetchRepliesConcern::FETCH_REPLIES_INITIAL_WAIT_MINUTES', 1.week)
end
it 'returns nil without fetching' do
got_uris = subject.perform(status.id)
expect(got_uris).to be_nil
assert_not_requested :get, top_note_uri
end
end
end
end