diff options
Diffstat (limited to 'spec')
22 files changed, 866 insertions, 380 deletions
diff --git a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb index 3a382ff27..e233bd560 100644 --- a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb +++ b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb @@ -47,7 +47,7 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController, type: :controll it 'returns orderedItems with followers from example.com' do expect(body[:orderedItems]).to be_an Array - expect(body[:orderedItems].sort).to eq [follower_4.uri, follower_1.uri, follower_2.uri] + expect(body[:orderedItems]).to match_array([follower_4.uri, follower_1.uri, follower_2.uri]) end it 'returns private Cache-Control header' do diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb index bf82fd020..a2c7f336f 100644 --- a/spec/controllers/activitypub/replies_controller_spec.rb +++ b/spec/controllers/activitypub/replies_controller_spec.rb @@ -4,8 +4,9 @@ require 'rails_helper' RSpec.describe ActivityPub::RepliesController, type: :controller do let(:status) { Fabricate(:status, visibility: parent_visibility) } - let(:remote_reply_id) { nil } - let(:remote_account) { nil } + let(:remote_account) { Fabricate(:account, domain: 'foobar.com') } + let(:remote_reply_id) { 'https://foobar.com/statuses/1234' } + let(:remote_querier) { nil } shared_examples 'cachable response' do it 'does not set cookies' do @@ -23,224 +24,188 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do end end - before do - allow(controller).to receive(:signed_request_account).and_return(remote_account) + shared_examples 'common behavior' do + context 'when status is private' do + let(:parent_visibility) { :private } - Fabricate(:status, thread: status, visibility: :public) - Fabricate(:status, thread: status, visibility: :public) - Fabricate(:status, thread: status, visibility: :private) - Fabricate(:status, account: status.account, thread: status, visibility: :public) - Fabricate(:status, account: status.account, thread: status, visibility: :private) - - Fabricate(:status, account: remote_account, thread: status, visibility: :public, uri: remote_reply_id) if remote_reply_id - end + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end - describe 'GET #index' do - context 'with no signature' do - subject(:response) { get :index, params: { account_username: status.account.username, status_id: status.id } } - subject(:body) { body_as_json } + context 'when status is direct' do + let(:parent_visibility) { :direct } - context 'when account is permanently suspended' do - let(:parent_visibility) { :public } + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end - before do - status.account.suspend! - status.account.deletion_request.destroy - end + shared_examples 'disallowed access' do + context 'when status is public' do + let(:parent_visibility) { :public } - it 'returns http gone' do - expect(response).to have_http_status(410) - end + it 'returns http not found' do + expect(response).to have_http_status(404) end + end - context 'when account is temporarily suspended' do - let(:parent_visibility) { :public } + it_behaves_like 'common behavior' + end - before do - status.account.suspend! - end + shared_examples 'allowed access' do + context 'when account is permanently suspended' do + let(:parent_visibility) { :public } - it 'returns http forbidden' do - expect(response).to have_http_status(403) - end + before do + status.account.suspend! + status.account.deletion_request.destroy end - context 'when status is public' do - let(:parent_visibility) { :public } - - it 'returns http success' do - expect(response).to have_http_status(200) - end + it 'returns http gone' do + expect(response).to have_http_status(410) + end + end - it 'returns application/activity+json' do - expect(response.media_type).to eq 'application/activity+json' - end + context 'when account is temporarily suspended' do + let(:parent_visibility) { :public } - it_behaves_like 'cachable response' + before do + status.account.suspend! + end - it 'returns items with account\'s own replies' do - expect(body[:first]).to be_a Hash - expect(body[:first][:items]).to be_an Array - expect(body[:first][:items].size).to eq 1 - expect(body[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true - end + it 'returns http forbidden' do + expect(response).to have_http_status(403) end + end - context 'when status is private' do - let(:parent_visibility) { :private } + context 'when status is public' do + let(:parent_visibility) { :public } + let(:json) { body_as_json } + let(:page_json) { json[:first] } - it 'returns http not found' do - expect(response).to have_http_status(404) - end + it 'returns http success' do + expect(response).to have_http_status(200) end - context 'when status is direct' do - let(:parent_visibility) { :direct } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end + it 'returns application/activity+json' do + expect(response.media_type).to eq 'application/activity+json' end - end - context 'with signature' do - let(:remote_account) { Fabricate(:account, domain: 'example.com') } - let(:only_other_accounts) { nil } + it_behaves_like 'cachable response' - context do - before do - get :index, params: { account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts } + context 'without only_other_accounts' do + it "returns items with thread author's replies" do + expect(page_json).to be_a Hash + expect(page_json[:items]).to be_an Array + expect(page_json[:items].size).to eq 1 + expect(page_json[:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true end - context 'when status is public' do - let(:parent_visibility) { :public } - - it 'returns http success' do - expect(response).to have_http_status(200) + context 'when there are few self-replies' do + it 'points next to replies from other people' do + expect(page_json).to be_a Hash + expect(Addressable::URI.parse(page_json[:next]).query.split('&')).to include('only_other_accounts=true', 'page=true') end + end - it 'returns application/activity+json' do - expect(response.media_type).to eq 'application/activity+json' + context 'when there are many self-replies' do + before do + 10.times { Fabricate(:status, account: status.account, thread: status, visibility: :public) } end - it_behaves_like 'cachable response' - - context 'without only_other_accounts' do - it 'returns items with account\'s own replies' do - json = body_as_json - - expect(json[:first]).to be_a Hash - expect(json[:first][:items]).to be_an Array - expect(json[:first][:items].size).to eq 1 - expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true - end + it 'points next to other self-replies' do + expect(page_json).to be_a Hash + expect(Addressable::URI.parse(page_json[:next]).query.split('&')).to include('only_other_accounts=false', 'page=true') end + end + end - context 'with only_other_accounts' do - let(:only_other_accounts) { 'true' } - - it 'returns items with other public or unlisted replies' do - json = body_as_json - - expect(json[:first]).to be_a Hash - expect(json[:first][:items]).to be_an Array - expect(json[:first][:items].size).to eq 2 - expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true - end - - context 'with remote responses' do - let(:remote_reply_id) { 'foo' } + context 'with only_other_accounts' do + let(:only_other_accounts) { 'true' } - it 'returned items are all inlined local toots or are ids' do - json = body_as_json + it 'returns items with other public or unlisted replies' do + expect(page_json).to be_a Hash + expect(page_json[:items]).to be_an Array + expect(page_json[:items].size).to eq 3 + end - expect(json[:first]).to be_a Hash - expect(json[:first][:items]).to be_an Array - expect(json[:first][:items].size).to eq 3 - expect(json[:first][:items].all? { |item| item.is_a?(Hash) ? ActivityPub::TagManager.instance.local_uri?(item[:id]) : item.is_a?(String) }).to be true - expect(json[:first][:items]).to include remote_reply_id - end - end - end + it 'only inlines items that are local and public or unlisted replies' do + inlined_replies = page_json[:items].select { |x| x.is_a?(Hash) } + public_collection = ActivityPub::TagManager::COLLECTIONS[:public] + expect(inlined_replies.all? { |item| item[:to].include?(public_collection) || item[:cc].include?(public_collection) }).to be true + expect(inlined_replies.all? { |item| ActivityPub::TagManager.instance.local_uri?(item[:id]) }).to be true end - context 'when status is private' do - let(:parent_visibility) { :private } + it 'uses ids for remote toots' do + remote_replies = page_json[:items].select { |x| !x.is_a?(Hash) } + expect(remote_replies.all? { |item| item.is_a?(String) && !ActivityPub::TagManager.instance.local_uri?(item) }).to be true + end - it 'returns http not found' do - expect(response).to have_http_status(404) + context 'when there are few replies' do + it 'does not have a next page' do + expect(page_json).to be_a Hash + expect(page_json[:next]).to be_nil end end - context 'when status is direct' do - let(:parent_visibility) { :direct } + context 'when there are many replies' do + before do + 10.times { Fabricate(:status, thread: status, visibility: :public) } + end - it 'returns http not found' do - expect(response).to have_http_status(404) + it 'points next to other replies' do + expect(page_json).to be_a Hash + expect(Addressable::URI.parse(page_json[:next]).query.split('&')).to include('only_other_accounts=true', 'page=true') end end end + end - context 'when signed request account is blocked' do - before do - status.account.block!(remote_account) - get :index, params: { account_username: status.account.username, status_id: status.id } - end - - context 'when status is public' do - let(:parent_visibility) { :public } + it_behaves_like 'common behavior' + end - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end + before do + stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5 + allow(controller).to receive(:signed_request_account).and_return(remote_querier) - context 'when status is private' do - let(:parent_visibility) { :private } + Fabricate(:status, thread: status, visibility: :public) + Fabricate(:status, thread: status, visibility: :public) + Fabricate(:status, thread: status, visibility: :private) + Fabricate(:status, account: status.account, thread: status, visibility: :public) + Fabricate(:status, account: status.account, thread: status, visibility: :private) - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end + Fabricate(:status, account: remote_account, thread: status, visibility: :public, uri: remote_reply_id) + end - context 'when status is direct' do - let(:parent_visibility) { :direct } + describe 'GET #index' do + subject(:response) { get :index, params: { account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts } } + let(:only_other_accounts) { nil } - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end + context 'with no signature' do + it_behaves_like 'allowed access' + end - context 'when signed request account is domain blocked' do - before do - status.account.block_domain!(remote_account.domain) - get :index, params: { account_username: status.account.username, status_id: status.id } - end + context 'with signature' do + let(:remote_querier) { Fabricate(:account, domain: 'example.com') } - context 'when status is public' do - let(:parent_visibility) { :public } + it_behaves_like 'allowed access' - it 'returns http not found' do - expect(response).to have_http_status(404) - end + context 'when signed request account is blocked' do + before do + status.account.block!(remote_querier) end - context 'when status is private' do - let(:parent_visibility) { :private } + it_behaves_like 'disallowed access' + end - it 'returns http not found' do - expect(response).to have_http_status(404) - end + context 'when signed request account is domain blocked' do + before do + status.account.block_domain!(remote_querier.domain) end - context 'when status is direct' do - let(:parent_visibility) { :direct } - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end + it_behaves_like 'disallowed access' end end end diff --git a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb index 348de08c2..b962b3398 100644 --- a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb @@ -79,7 +79,7 @@ describe Api::V1::Accounts::StatusesController do it 'lists both the public and the private statuses' do get :index, params: { account_id: account.id, pinned: true } json = body_as_json - expect(json.map { |item| item[:id].to_i }.sort).to eq [status.id, private_status.id].sort + expect(json.map { |item| item[:id].to_i }).to match_array([status.id, private_status.id]) end end end diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb index d8d732630..a1f6ddb24 100644 --- a/spec/controllers/api/v1/media_controller_spec.rb +++ b/spec/controllers/api/v1/media_controller_spec.rb @@ -110,21 +110,24 @@ RSpec.describe Api::V1::MediaController, type: :controller do end end - context 'when not attached to a status' do - let(:media) { Fabricate(:media_attachment, status: nil, account: user.account) } + context 'when the author \'s' do + let(:status) { nil } + let(:media) { Fabricate(:media_attachment, status: status, account: user.account) } - it 'updates the description' do + before do put :update, params: { id: media.id, description: 'Lorem ipsum!!!' } + end + + it 'updates the description' do expect(media.reload.description).to eq 'Lorem ipsum!!!' end - end - context 'when attached to a status' do - let(:media) { Fabricate(:media_attachment, status: Fabricate(:status), account: user.account) } + context 'when already attached to a status' do + let(:status) { Fabricate(:status, account: user.account) } - it 'returns http not found' do - put :update, params: { id: media.id, description: 'Lorem ipsum!!!' } - expect(response).to have_http_status(:not_found) + it 'returns http not found' do + expect(response).to have_http_status(:not_found) + end end end end diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb index 2679ab017..190dfad11 100644 --- a/spec/controllers/api/v1/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/statuses_controller_spec.rb @@ -102,6 +102,23 @@ RSpec.describe Api::V1::StatusesController, type: :controller do expect(Status.find_by(id: status.id)).to be nil end end + + describe 'PUT #update' do + let(:scopes) { 'write:statuses' } + let(:status) { Fabricate(:status, account: user.account) } + + before do + put :update, params: { id: status.id, status: 'I am updated' } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'updates the status' do + expect(status.reload.text).to eq 'I am updated' + end + end end context 'without an oauth token' do diff --git a/spec/helpers/languages_helper_spec.rb b/spec/helpers/languages_helper_spec.rb index 6db617824..5587fc261 100644 --- a/spec/helpers/languages_helper_spec.rb +++ b/spec/helpers/languages_helper_spec.rb @@ -3,15 +3,21 @@ require 'rails_helper' describe LanguagesHelper do - describe 'the HUMAN_LOCALES constant' do - it 'includes all I18n locales' do - expect(described_class::HUMAN_LOCALES.keys).to include(*I18n.available_locales) + describe 'the SUPPORTED_LOCALES constant' do + it 'includes all i18n locales' do + expect(Set.new(described_class::SUPPORTED_LOCALES.keys + described_class::REGIONAL_LOCALE_NAMES.keys)).to include(*I18n.available_locales) end end - describe 'human_locale' do - it 'finds the human readable local description from a key' do - expect(helper.human_locale(:en)).to eq('English') + describe 'native_locale_name' do + it 'finds the human readable native name from a key' do + expect(helper.native_locale_name(:en)).to eq('English') + end + end + + describe 'standard_locale_name' do + it 'finds the human readable standard name from a key' do + expect(helper.standard_locale_name(:de)).to eq('German') end end end diff --git a/spec/lib/language_detector_spec.rb b/spec/lib/language_detector_spec.rb deleted file mode 100644 index b7ba0f6c4..000000000 --- a/spec/lib/language_detector_spec.rb +++ /dev/null @@ -1,134 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe LanguageDetector do - describe 'prepare_text' do - it 'returns unmodified string without special cases' do - string = 'just a regular string' - result = described_class.instance.send(:prepare_text, string) - - expect(result).to eq string - end - - it 'collapses spacing in strings' do - string = 'The formatting in this is very odd' - - result = described_class.instance.send(:prepare_text, string) - expect(result).to eq 'The formatting in this is very odd' - end - - it 'strips usernames from strings before detection' do - string = '@username Yeah, very surreal...! also @friend' - - result = described_class.instance.send(:prepare_text, string) - expect(result).to eq 'Yeah, very surreal...! also' - end - - it 'strips URLs from strings before detection' do - string = 'Our website is https://example.com and also http://localhost.dev' - - result = described_class.instance.send(:prepare_text, string) - expect(result).to eq 'Our website is and also' - end - - it 'converts #hashtags back to normal text before detection' do - string = 'Hey look at all the #animals and #FishAndChips' - - result = described_class.instance.send(:prepare_text, string) - expect(result).to eq 'Hey look at all the animals and fish and chips' - end - end - - describe 'detect' do - let(:account_without_user_locale) { Fabricate(:user, locale: nil).account } - let(:account_remote) { Fabricate(:account, domain: 'joinmastodon.org') } - - it 'detects english language for basic strings' do - strings = [ - "Hello and welcome to mastodon how are you today?", - "I'd rather not!", - "a lot of people just want to feel righteous all the time and that's all that matters", - ] - strings.each do |string| - result = described_class.instance.detect(string, account_without_user_locale) - - expect(result).to eq(:en), string - end - end - - it 'detects spanish language' do - string = 'Obtener un Hola y bienvenidos a Mastodon. Obtener un Hola y bienvenidos a Mastodon. Obtener un Hola y bienvenidos a Mastodon. Obtener un Hola y bienvenidos a Mastodon' - result = described_class.instance.detect(string, account_without_user_locale) - - expect(result).to eq :es - end - - describe 'when language can\'t be detected' do - it 'uses nil when sent an empty document' do - result = described_class.instance.detect('', account_without_user_locale) - expect(result).to eq nil - end - - describe 'because of a URL' do - it 'uses nil when sent just a URL' do - string = 'http://example.com/media/2kFTgOJLXhQf0g2nKB4' - cld_result = CLD3::NNetLanguageIdentifier.new(0, 2048).find_language(string) - expect(cld_result).not_to eq :en - - result = described_class.instance.detect(string, account_without_user_locale) - - expect(result).to eq nil - end - end - - describe 'with an account' do - it 'uses the account locale when present' do - account = double(user_locale: 'fr') - result = described_class.instance.detect('', account) - - expect(result).to eq nil - end - - it 'uses nil when account is present but has no locale' do - result = described_class.instance.detect('', account_without_user_locale) - - expect(result).to eq nil - end - end - - describe 'with an `en` default locale' do - it 'uses nil for undetectable string' do - result = described_class.instance.detect('', account_without_user_locale) - - expect(result).to eq nil - end - end - - describe 'remote user' do - it 'detects Korean language' do - string = '안녕하세요' - result = described_class.instance.detect(string, account_remote) - - expect(result).to eq :ko - end - end - - describe 'with a non-`en` default locale' do - around(:each) do |example| - before = I18n.default_locale - I18n.default_locale = :ja - example.run - I18n.default_locale = before - end - - it 'uses nil for undetectable string' do - string = '' - result = described_class.instance.detect(string, account_without_user_locale) - - expect(result).to eq nil - end - end - end - end -end diff --git a/spec/lib/link_details_extractor_spec.rb b/spec/lib/link_details_extractor_spec.rb index 850857b2d..84bb4579c 100644 --- a/spec/lib/link_details_extractor_spec.rb +++ b/spec/lib/link_details_extractor_spec.rb @@ -26,4 +26,126 @@ RSpec.describe LinkDetailsExtractor do end end end + + context 'when structured data is present' do + let(:original_url) { 'https://example.com/page.html' } + + context 'and is wrapped in CDATA tags' do + let(:html) { <<-HTML } +<!doctype html> +<html> +<head> + <script type="application/ld+json"> + //<![CDATA[ + {"@context":"http://schema.org","@type":"NewsArticle","mainEntityOfPage":"https://example.com/page.html","headline":"Foo","datePublished":"2022-01-31T19:53:00+00:00","url":"https://example.com/page.html","description":"Bar","author":{"@type":"Person","name":"Hoge"},"publisher":{"@type":"Organization","name":"Baz"}} + //]]> + </script> +</head> +</html> + HTML + + describe '#title' do + it 'returns the title from structured data' do + expect(subject.title).to eq 'Foo' + end + end + + describe '#description' do + it 'returns the description from structured data' do + expect(subject.description).to eq 'Bar' + end + end + + describe '#provider_name' do + it 'returns the provider name from structured data' do + expect(subject.provider_name).to eq 'Baz' + end + end + + describe '#author_name' do + it 'returns the author name from structured data' do + expect(subject.author_name).to eq 'Hoge' + end + end + end + + context 'but the first tag is invalid JSON' do + let(:html) { <<-HTML } +<!doctype html> +<html> +<body> + <script type="application/ld+json"> + { + "@context":"https://schema.org", + "@type":"ItemList", + "url":"https://example.com/page.html", + "name":"Foo", + "description":"Bar" + }, + { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement":[ + { + "@type":"ListItem", + "position":1, + "item":{ + "@id":"https://www.example.com", + "name":"Baz" + } + } + ] + } + </script> + <script type="application/ld+json"> + { + "@context":"https://schema.org", + "@type":"NewsArticle", + "mainEntityOfPage": { + "@type":"WebPage", + "@id": "http://example.com/page.html" + }, + "headline": "Foo", + "description": "Bar", + "datePublished": "2022-01-31T19:46:00+00:00", + "author": { + "@type": "Organization", + "name": "Hoge" + }, + "publisher": { + "@type": "NewsMediaOrganization", + "name":"Baz", + "url":"https://example.com/" + } + } + </script> +</body> +</html> + HTML + + describe '#title' do + it 'returns the title from structured data' do + expect(subject.title).to eq 'Foo' + end + end + + describe '#description' do + it 'returns the description from structured data' do + expect(subject.description).to eq 'Bar' + end + end + + describe '#provider_name' do + it 'returns the provider name from structured data' do + expect(subject.provider_name).to eq 'Baz' + end + end + + describe '#author_name' do + it 'returns the author name from structured data' do + expect(subject.author_name).to eq 'Hoge' + end + end + end + end end diff --git a/spec/models/account_statuses_cleanup_policy_spec.rb b/spec/models/account_statuses_cleanup_policy_spec.rb index 4732ad625..b01321a20 100644 --- a/spec/models/account_statuses_cleanup_policy_spec.rb +++ b/spec/models/account_statuses_cleanup_policy_spec.rb @@ -495,7 +495,7 @@ RSpec.describe AccountStatusesCleanupPolicy, type: :model do 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 + expect(subject.pluck(:id)).to match_array([very_old_status.id, faved4.id, faved5.id, reblogged4.id, reblogged5.id]) end end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 25c98d508..029789a11 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -366,17 +366,17 @@ RSpec.describe Status, type: :model do context 'when given one tag' do it 'returns the expected statuses' do - expect(Status.tagged_with([tag1.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status5.id] - expect(Status.tagged_with([tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status2.id, status5.id] - expect(Status.tagged_with([tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status3.id, status5.id] + expect(Status.tagged_with([tag1.id]).reorder(:id).pluck(:id).uniq).to match_array([status1.id, status5.id]) + expect(Status.tagged_with([tag2.id]).reorder(:id).pluck(:id).uniq).to match_array([status2.id, status5.id]) + expect(Status.tagged_with([tag3.id]).reorder(:id).pluck(:id).uniq).to match_array([status3.id, status5.id]) end end context 'when given multiple tags' do it 'returns the expected statuses' do - expect(Status.tagged_with([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status2.id, status5.id] - expect(Status.tagged_with([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status3.id, status5.id] - expect(Status.tagged_with([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status2.id, status3.id, status5.id] + expect(Status.tagged_with([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to match_array([status1.id, status2.id, status5.id]) + expect(Status.tagged_with([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to match_array([status1.id, status3.id, status5.id]) + expect(Status.tagged_with([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to match_array([status2.id, status3.id, status5.id]) end end end @@ -393,15 +393,15 @@ RSpec.describe Status, type: :model do context 'when given one tag' do it 'returns the expected statuses' do - expect(Status.tagged_with_all([tag1.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status5.id] - expect(Status.tagged_with_all([tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status2.id, status5.id] - expect(Status.tagged_with_all([tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status3.id] + expect(Status.tagged_with_all([tag1.id]).reorder(:id).pluck(:id).uniq).to match_array([status1.id, status5.id]) + expect(Status.tagged_with_all([tag2.id]).reorder(:id).pluck(:id).uniq).to match_array([status2.id, status5.id]) + expect(Status.tagged_with_all([tag3.id]).reorder(:id).pluck(:id).uniq).to match_array([status3.id]) end end context 'when given multiple tags' do it 'returns the expected statuses' do - expect(Status.tagged_with_all([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status5.id] + expect(Status.tagged_with_all([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to match_array([status5.id]) expect(Status.tagged_with_all([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [] expect(Status.tagged_with_all([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [] end @@ -420,17 +420,17 @@ RSpec.describe Status, type: :model do context 'when given one tag' do it 'returns the expected statuses' do - expect(Status.tagged_with_none([tag1.id]).reorder(:id).pluck(:id).uniq).to eq [status2.id, status3.id, status4.id] - expect(Status.tagged_with_none([tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status3.id, status4.id] - expect(Status.tagged_with_none([tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status2.id, status4.id] + expect(Status.tagged_with_none([tag1.id]).reorder(:id).pluck(:id).uniq).to match_array([status2.id, status3.id, status4.id]) + expect(Status.tagged_with_none([tag2.id]).reorder(:id).pluck(:id).uniq).to match_array([status1.id, status3.id, status4.id]) + expect(Status.tagged_with_none([tag3.id]).reorder(:id).pluck(:id).uniq).to match_array([status1.id, status2.id, status4.id]) end end context 'when given multiple tags' do it 'returns the expected statuses' do - expect(Status.tagged_with_none([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to eq [status3.id, status4.id] - expect(Status.tagged_with_none([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status2.id, status4.id] - expect(Status.tagged_with_none([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [status1.id, status4.id] + expect(Status.tagged_with_none([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to match_array([status3.id, status4.id]) + expect(Status.tagged_with_none([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to match_array([status2.id, status4.id]) + expect(Status.tagged_with_none([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to match_array([status1.id, status4.id]) end end end diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index 8bce29cad..865c693aa 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -137,7 +137,7 @@ RSpec.describe StatusPolicy, type: :model do end end - permissions :index?, :update? do + permissions :index? do it 'grants access if staff' do expect(subject).to permit(admin.account) end @@ -146,4 +146,18 @@ RSpec.describe StatusPolicy, type: :model do expect(subject).to_not permit(alice) end end + + permissions :update? do + it 'grants access if staff' do + expect(subject).to permit(admin.account, status) + end + + it 'grants access if owner' do + expect(subject).to permit(status.account, status) + end + + it 'denies access unless staff' do + expect(subject).to_not permit(bob, status) + end + end end diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb index 65c453341..fe49b18c1 100644 --- a/spec/services/activitypub/fetch_replies_service_spec.rb +++ b/spec/services/activitypub/fetch_replies_service_spec.rb @@ -34,9 +34,8 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do context 'when the payload is a Collection with inlined replies' do context 'when passing the collection itself' do it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) + expect(FetchReplyWorker).to receive(: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']) subject.call(status, 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 end @@ -46,9 +45,8 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do end it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) + expect(FetchReplyWorker).to receive(: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']) subject.call(status, 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 end end @@ -65,9 +63,8 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do context 'when passing the collection itself' do it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) + expect(FetchReplyWorker).to receive(: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']) subject.call(status, 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 end @@ -77,9 +74,8 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do end it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) + expect(FetchReplyWorker).to receive(: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']) subject.call(status, 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 end end @@ -100,9 +96,8 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do context 'when passing the collection itself' do it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) + expect(FetchReplyWorker).to receive(: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']) subject.call(status, 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 end @@ -112,9 +107,8 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do end it 'spawns workers for up to 5 replies on the same server' do - allow(FetchReplyWorker).to receive(:push_bulk) + expect(FetchReplyWorker).to receive(: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']) subject.call(status, 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 end end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb new file mode 100644 index 000000000..6ee1dcb43 --- /dev/null +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -0,0 +1,248 @@ +require 'rails_helper' + +RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do + let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) } + + let(:alice) { Fabricate(:account) } + let(:bob) { Fabricate(:account) } + + let(:mentions) { [] } + let(:tags) { [] } + let(:media_attachments) { [] } + + before do + mentions.each { |a| Fabricate(:mention, status: status, account: a) } + tags.each { |t| status.tags << t } + media_attachments.each { |m| status.media_attachments << m } + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + tag: [ + { type: 'Hashtag', name: 'hoge' }, + { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, + ], + } + end + + let(:json) { Oj.load(Oj.dump(payload)) } + + subject { described_class.new } + + describe '#call' do + it 'updates text' do + subject.call(status, json) + expect(status.reload.text).to eq 'Hello universe' + end + + it 'updates content warning' do + subject.call(status, json) + expect(status.reload.spoiler_text).to eq 'Show more' + end + + context 'originally without tags' do + before do + subject.call(status, json) + end + + it 'updates tags' do + expect(status.tags.reload.map(&:name)).to eq %w(hoge) + end + end + + context 'originally with tags' do + let(:tags) { [Fabricate(:tag, name: 'test'), Fabricate(:tag, name: 'foo')] } + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + tag: [ + { type: 'Hashtag', name: 'foo' }, + ], + } + end + + before do + subject.call(status, json) + end + + it 'updates tags' do + expect(status.tags.reload.map(&:name)).to eq %w(foo) + end + end + + context 'originally without mentions' do + before do + subject.call(status, json) + end + + it 'updates mentions' do + expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id] + end + end + + context 'originally with mentions' do + let(:mentions) { [alice, bob] } + + before do + subject.call(status, json) + end + + it 'updates mentions' do + expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id] + end + end + + context 'originally without media attachments' do + before do + allow(RedownloadMediaWorker).to receive(:perform_async) + subject.call(status, json) + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Note', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + attachment: [ + { type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png' }, + ] + } + end + + it 'updates media attachments' do + media_attachment = status.media_attachments.reload.first + + expect(media_attachment).to_not be_nil + expect(media_attachment.remote_url).to eq 'https://example.com/foo.png' + end + + it 'queues download of media attachments' do + expect(RedownloadMediaWorker).to have_received(:perform_async) + end + + it 'records media change in edit' do + expect(status.edits.reload.last.media_attachments_changed).to be true + end + end + + context 'originally with media attachments' do + let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] } + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Note', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + attachment: [ + { type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png', name: 'A picture' }, + ] + } + end + + before do + allow(RedownloadMediaWorker).to receive(:perform_async) + subject.call(status, json) + end + + it 'updates the existing media attachment in-place' do + media_attachment = status.media_attachments.reload.first + + expect(media_attachment).to_not be_nil + expect(media_attachment.remote_url).to eq 'https://example.com/foo.png' + expect(media_attachment.description).to eq 'A picture' + end + + it 'does not queue redownload for the existing media attachment' do + expect(RedownloadMediaWorker).to_not have_received(:perform_async) + end + + it 'updates media attachments' do + expect(status.media_attachments.reload.map(&:remote_url)).to eq %w(https://example.com/foo.png) + end + + it 'records media change in edit' do + expect(status.edits.reload.last.media_attachments_changed).to be true + end + end + + context 'originally with a poll' do + before do + poll = Fabricate(:poll, status: status) + status.update(preloadable_poll: poll) + subject.call(status, json) + end + + it 'removes poll' do + expect(status.reload.poll).to eq nil + end + + it 'records media change in edit' do + expect(status.edits.reload.last.media_attachments_changed).to be true + end + end + + context 'originally without a poll' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Question', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + closed: true, + oneOf: [ + { type: 'Note', name: 'Foo' }, + { type: 'Note', name: 'Bar' }, + { type: 'Note', name: 'Baz' }, + ], + } + end + + before do + subject.call(status, json) + end + + it 'creates a poll' do + poll = status.reload.poll + + expect(poll).to_not be_nil + expect(poll.options).to eq %w(Foo Bar Baz) + end + + it 'records media change in edit' do + expect(status.edits.reload.last.media_attachments_changed).to be true + end + end + + it 'creates edit history' do + subject.call(status, json) + expect(status.edits.reload.map(&:text)).to eq ['Hello world', 'Hello universe'] + end + + it 'sets edited timestamp' do + subject.call(status, json) + expect(status.reload.edited_at.to_s).to eq '2021-09-08 22:39:25 UTC' + end + + it 'records that no media has been changed in edit' do + subject.call(status, json) + expect(status.edits.reload.last.media_attachments_changed).to be false + end + end +end diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index fb7c6b462..482068d58 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -15,35 +15,97 @@ RSpec.describe RemoveStatusService, type: :service do jeff.follow!(alice) hank.follow!(alice) - - @status = PostStatusService.new.call(alice, text: 'Hello @bob@example.com') - FavouriteService.new.call(jeff, @status) - Fabricate(:status, account: bill, reblog: @status, uri: 'hoge') end - it 'removes status from author\'s home feed' do - subject.call(@status) - expect(HomeFeed.new(alice).get(10)).to_not include(@status.id) - end + context 'when removed status is not a reblog' do + before do + @status = PostStatusService.new.call(alice, text: 'Hello @bob@example.com ThisIsASecret') + FavouriteService.new.call(jeff, @status) + Fabricate(:status, account: bill, reblog: @status, uri: 'hoge') + end - it 'removes status from local follower\'s home feed' do - subject.call(@status) - expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id) - end + it 'removes status from author\'s home feed' do + subject.call(@status) + expect(HomeFeed.new(alice).get(10)).to_not include(@status.id) + end + + it 'removes status from local follower\'s home feed' do + subject.call(@status) + expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id) + end - it 'sends delete activity to followers' do - subject.call(@status) - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice + it 'sends Delete activity to followers' do + subject.call(@status) + expect(a_request(:post, 'http://example.com/inbox').with( + body: hash_including({ + 'type' => 'Delete', + 'object' => { + 'type' => 'Tombstone', + 'id' => ActivityPub::TagManager.instance.uri_for(@status), + 'atomUri' => OStatus::TagManager.instance.uri_for(@status), + }, + }) + )).to have_been_made.once + end + + it 'sends Delete activity to rebloggers' do + subject.call(@status) + expect(a_request(:post, 'http://example2.com/inbox').with( + body: hash_including({ + 'type' => 'Delete', + 'object' => { + 'type' => 'Tombstone', + 'id' => ActivityPub::TagManager.instance.uri_for(@status), + 'atomUri' => OStatus::TagManager.instance.uri_for(@status), + }, + }) + )).to have_been_made.once + end + + it 'remove status from notifications' do + expect { subject.call(@status) }.to change { + Notification.where(activity_type: 'Favourite', from_account: jeff, account: alice).count + }.from(1).to(0) + end end - it 'sends delete activity to rebloggers' do - subject.call(@status) - expect(a_request(:post, 'http://example2.com/inbox')).to have_been_made + context 'when removed status is a private self-reblog' do + before do + @original_status = Fabricate(:status, account: alice, text: 'Hello ThisIsASecret', visibility: :private) + @status = ReblogService.new.call(alice, @original_status) + end + + it 'sends Undo activity to followers' do + subject.call(@status) + expect(a_request(:post, 'http://example.com/inbox').with( + body: hash_including({ + 'type' => 'Undo', + 'object' => hash_including({ + 'type' => 'Announce', + 'object' => ActivityPub::TagManager.instance.uri_for(@original_status), + }), + }) + )).to have_been_made.once + end end - it 'remove status from notifications' do - expect { subject.call(@status) }.to change { - Notification.where(activity_type: 'Favourite', from_account: jeff, account: alice).count - }.from(1).to(0) + context 'when removed status is public self-reblog' do + before do + @original_status = Fabricate(:status, account: alice, text: 'Hello ThisIsASecret', visibility: :public) + @status = ReblogService.new.call(alice, @original_status) + end + + it 'sends Undo activity to followers' do + subject.call(@status) + expect(a_request(:post, 'http://example.com/inbox').with( + body: hash_including({ + 'type' => 'Undo', + 'object' => hash_including({ + 'type' => 'Announce', + 'object' => ActivityPub::TagManager.instance.uri_for(@original_status), + }), + }) + )).to have_been_made.once + end end end diff --git a/spec/services/update_status_service_spec.rb b/spec/services/update_status_service_spec.rb new file mode 100644 index 000000000..4fd4837c6 --- /dev/null +++ b/spec/services/update_status_service_spec.rb @@ -0,0 +1,140 @@ +require 'rails_helper' + +RSpec.describe UpdateStatusService, type: :service do + subject { described_class.new } + + context 'when text changes' do + let!(:status) { Fabricate(:status, text: 'Foo') } + let(:preview_card) { Fabricate(:preview_card) } + + before do + status.preview_cards << preview_card + subject.call(status, status.account_id, text: 'Bar') + end + + it 'updates text' do + expect(status.reload.text).to eq 'Bar' + end + + it 'resets preview card' do + expect(status.reload.preview_card).to be_nil + end + + it 'saves edit history' do + expect(status.edits.pluck(:text, :media_attachments_changed)).to eq [['Foo', false], ['Bar', false]] + end + end + + context 'when content warning changes' do + let!(:status) { Fabricate(:status, text: 'Foo', spoiler_text: '') } + let(:preview_card) { Fabricate(:preview_card) } + + before do + status.preview_cards << preview_card + subject.call(status, status.account_id, text: 'Foo', spoiler_text: 'Bar') + end + + it 'updates content warning' do + expect(status.reload.spoiler_text).to eq 'Bar' + end + + it 'saves edit history' do + expect(status.edits.pluck(:text, :spoiler_text, :media_attachments_changed)).to eq [['Foo', '', false], ['Foo', 'Bar', false]] + end + end + + context 'when media attachments change' do + let!(:status) { Fabricate(:status, text: 'Foo') } + let!(:detached_media_attachment) { Fabricate(:media_attachment, account: status.account) } + let!(:attached_media_attachment) { Fabricate(:media_attachment, account: status.account) } + + before do + status.media_attachments << detached_media_attachment + subject.call(status, status.account_id, text: 'Foo', media_ids: [attached_media_attachment.id]) + end + + it 'updates media attachments' do + expect(status.media_attachments.to_a).to eq [attached_media_attachment] + end + + it 'detaches detached media attachments' do + expect(detached_media_attachment.reload.status_id).to be_nil + end + + it 'attaches attached media attachments' do + expect(attached_media_attachment.reload.status_id).to eq status.id + end + + it 'saves edit history' do + expect(status.edits.pluck(:text, :media_attachments_changed)).to eq [['Foo', false], ['Foo', true]] + end + end + + context 'when poll changes' do + let(:account) { Fabricate(:account) } + let!(:status) { Fabricate(:status, text: 'Foo', account: account, poll_attributes: {options: %w(Foo Bar), account: account, multiple: false, hide_totals: false, expires_at: 7.days.from_now }) } + let!(:poll) { status.poll } + let!(:voter) { Fabricate(:account) } + + before do + status.update(poll: poll) + VoteService.new.call(voter, poll, [0]) + subject.call(status, status.account_id, text: 'Foo', poll: { options: %w(Bar Baz Foo), expires_in: 5.days.to_i }) + end + + it 'updates poll' do + poll = status.poll.reload + expect(poll.options).to eq %w(Bar Baz Foo) + end + + it 'resets votes' do + poll = status.poll.reload + expect(poll.votes_count).to eq 0 + expect(poll.votes.count).to eq 0 + expect(poll.cached_tallies).to eq [0, 0, 0] + end + + it 'saves edit history' do + expect(status.edits.pluck(:text, :media_attachments_changed)).to eq [['Foo', false], ['Foo', true]] + end + end + + context 'when mentions in text change' do + let!(:account) { Fabricate(:account) } + let!(:alice) { Fabricate(:account, username: 'alice') } + let!(:bob) { Fabricate(:account, username: 'bob') } + let!(:status) { PostStatusService.new.call(account, text: 'Hello @alice') } + + before do + subject.call(status, status.account_id, text: 'Hello @bob') + end + + it 'changes mentions' do + expect(status.active_mentions.pluck(:account_id)).to eq [bob.id] + end + + it 'keeps old mentions as silent mentions' do + expect(status.mentions.pluck(:account_id)).to match_array([alice.id, bob.id]) + end + end + + context 'when hashtags in text change' do + let!(:account) { Fabricate(:account) } + let!(:status) { PostStatusService.new.call(account, text: 'Hello #foo') } + + before do + subject.call(status, status.account_id, text: 'Hello #bar') + end + + it 'changes tags' do + expect(status.tags.pluck(:name)).to eq %w(bar) + end + end + + it 'notifies ActivityPub about the update' do + status = Fabricate(:status, text: 'Foo') + allow(ActivityPub::DistributionWorker).to receive(:perform_async) + subject.call(status, status.account_id, text: 'Bar') + expect(ActivityPub::DistributionWorker).to have_received(:perform_async) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b6d127a08..0414ba9ed 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -57,3 +57,10 @@ end def json_str_to_hash(str) JSON.parse(str, symbolize_names: true) end + +def expect_push_bulk_to_match(klass, matcher) + expect(Sidekiq::Client).to receive(:push_bulk).with(hash_including({ + "class" => klass, + "args" => matcher + })) +end diff --git a/spec/workers/activitypub/distribute_poll_update_worker_spec.rb b/spec/workers/activitypub/distribute_poll_update_worker_spec.rb index 7eb6119fd..d68a695b7 100644 --- a/spec/workers/activitypub/distribute_poll_update_worker_spec.rb +++ b/spec/workers/activitypub/distribute_poll_update_worker_spec.rb @@ -10,13 +10,12 @@ describe ActivityPub::DistributePollUpdateWorker do describe '#perform' do before do - allow(ActivityPub::DeliveryWorker).to receive(:push_bulk) follower.follow!(account) end it 'delivers to followers' do + expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), account.id, 'http://example.com']]) subject.perform(status.id) - expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) end end end diff --git a/spec/workers/activitypub/distribution_worker_spec.rb b/spec/workers/activitypub/distribution_worker_spec.rb index c017b4da1..3a5900d9b 100644 --- a/spec/workers/activitypub/distribution_worker_spec.rb +++ b/spec/workers/activitypub/distribution_worker_spec.rb @@ -8,7 +8,6 @@ describe ActivityPub::DistributionWorker do describe '#perform' do before do - allow(ActivityPub::DeliveryWorker).to receive(:push_bulk) follower.follow!(status.account) end @@ -18,8 +17,8 @@ describe ActivityPub::DistributionWorker do end it 'delivers to followers' do + expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]]) subject.perform(status.id) - expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) end end @@ -29,8 +28,8 @@ describe ActivityPub::DistributionWorker do end it 'delivers to followers' do + expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]]) subject.perform(status.id) - expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) end end @@ -43,8 +42,8 @@ describe ActivityPub::DistributionWorker do end it 'delivers to mentioned accounts' do + expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'https://foo.bar/inbox', anything]]) subject.perform(status.id) - expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['https://foo.bar/inbox']) end end end diff --git a/spec/workers/activitypub/move_distribution_worker_spec.rb b/spec/workers/activitypub/move_distribution_worker_spec.rb index b52788e54..af8c44cc0 100644 --- a/spec/workers/activitypub/move_distribution_worker_spec.rb +++ b/spec/workers/activitypub/move_distribution_worker_spec.rb @@ -9,14 +9,16 @@ describe ActivityPub::MoveDistributionWorker do describe '#perform' do before do - allow(ActivityPub::DeliveryWorker).to receive(:push_bulk) follower.follow!(migration.account) blocker.block!(migration.account) end it 'delivers to followers and known blockers' do + expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [ + [kind_of(String), migration.account.id, 'http://example.com'], + [kind_of(String), migration.account.id, 'http://example2.com'] + ]) subject.perform(migration.id) - expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com', 'http://example2.com']) end end end diff --git a/spec/workers/activitypub/status_update_distribution_worker_spec.rb b/spec/workers/activitypub/status_update_distribution_worker_spec.rb new file mode 100644 index 000000000..c014c6790 --- /dev/null +++ b/spec/workers/activitypub/status_update_distribution_worker_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +describe ActivityPub::StatusUpdateDistributionWorker do + subject { described_class.new } + + let(:status) { Fabricate(:status, text: 'foo') } + let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') } + + describe '#perform' do + before do + follower.follow!(status.account) + + status.snapshot! + status.text = 'bar' + status.edited_at = Time.now.utc + status.snapshot! + status.save! + end + + context 'with public status' do + before do + status.update(visibility: :public) + end + + it 'delivers to followers' do + expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]]) + + subject.perform(status.id) + end + end + + context 'with private status' do + before do + status.update(visibility: :private) + end + + it 'delivers to followers' do + expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]]) + + subject.perform(status.id) + end + end + end +end diff --git a/spec/workers/activitypub/update_distribution_worker_spec.rb b/spec/workers/activitypub/update_distribution_worker_spec.rb index 688a424d5..0e057fd0b 100644 --- a/spec/workers/activitypub/update_distribution_worker_spec.rb +++ b/spec/workers/activitypub/update_distribution_worker_spec.rb @@ -8,13 +8,12 @@ describe ActivityPub::UpdateDistributionWorker do describe '#perform' do before do - allow(ActivityPub::DeliveryWorker).to receive(:push_bulk) follower.follow!(account) end it 'delivers to followers' do + expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), account.id, 'http://example.com', anything]]) subject.perform(account.id) - expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) end end end diff --git a/spec/workers/move_worker_spec.rb b/spec/workers/move_worker_spec.rb index 4db5810f1..be02d3192 100644 --- a/spec/workers/move_worker_spec.rb +++ b/spec/workers/move_worker_spec.rb @@ -21,7 +21,6 @@ describe MoveWorker do blocking_account.block!(source_account) muting_account.mute!(source_account) - allow(UnfollowFollowWorker).to receive(:push_bulk) allow(BlockService).to receive(:new).and_return(block_service) allow(block_service).to receive(:call) end @@ -78,8 +77,8 @@ describe MoveWorker do context 'both accounts are distant' do describe 'perform' do it 'calls UnfollowFollowWorker' do + expect_push_bulk_to_match(UnfollowFollowWorker, [[local_follower.id, source_account.id, target_account.id, false]]) subject.perform(source_account.id, target_account.id) - expect(UnfollowFollowWorker).to have_received(:push_bulk).with([local_follower.id]) end include_examples 'user note handling' @@ -92,8 +91,8 @@ describe MoveWorker do describe 'perform' do it 'calls UnfollowFollowWorker' do + expect_push_bulk_to_match(UnfollowFollowWorker, [[local_follower.id, source_account.id, target_account.id, true]]) subject.perform(source_account.id, target_account.id) - expect(UnfollowFollowWorker).to have_received(:push_bulk).with([local_follower.id]) end include_examples 'user note handling' |