Merge commit '9c2d5b534f
' into upstream-20250314
This commit is contained in:
commit
6548462ecb
84 changed files with 1719 additions and 418 deletions
|
@ -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
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
127
spec/models/concerns/status/fetch_replies_concern_spec.rb
Normal file
127
spec/models/concerns/status/fetch_replies_concern_spec.rb
Normal 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
|
19
spec/requests/admin/settings/branding_spec.rb
Normal file
19
spec/requests/admin/settings/branding_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
124
spec/services/activitypub/fetch_all_replies_service_spec.rb
Normal file
124
spec/services/activitypub/fetch_all_replies_service_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
8
spec/support/feature_flags.rb
Normal file
8
spec/support/feature_flags.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
281
spec/workers/activitypub/fetch_all_replies_worker_spec.rb
Normal file
281
spec/workers/activitypub/fetch_all_replies_worker_spec.rb
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue