From cc7ba5d2af2f84b28044d524bce2efa5bb261a55 Mon Sep 17 00:00:00 2001 From: KMY Date: Wed, 18 Oct 2023 10:41:42 +0900 Subject: [PATCH] Merge --- Gemfile | 3 + Gemfile.lock | 9 +- .../features/compose/components/search.jsx | 2 +- app/javascript/mastodon/features/ui/index.jsx | 2 + app/javascript/mastodon/initial_state.js | 1 + app/lib/search_query_transformer.rb | 2 + .../unreserved_username_validator.rb | 25 +- config/brakeman.ignore | 70 --- docker-compose.yml | 2 +- lib/mastodon/cli/domains.rb | 6 +- spec/controllers/accounts_controller_spec.rb | 550 +++++++----------- spec/features/admin/accounts_spec.rb | 8 +- spec/features/admin/custom_emojis_spec.rb | 2 +- spec/features/admin/domain_blocks_spec.rb | 18 +- .../admin/email_domain_blocks_spec.rb | 2 +- spec/features/admin/ip_blocks_spec.rb | 2 +- spec/features/admin/software_updates_spec.rb | 4 +- spec/features/admin/statuses_spec.rb | 2 +- .../links/preview_card_providers_spec.rb | 2 +- spec/features/admin/trends/links_spec.rb | 2 +- spec/features/admin/trends/statuses_spec.rb | 2 +- spec/features/admin/trends/tags_spec.rb | 2 +- spec/features/captcha_spec.rb | 2 +- spec/features/log_in_spec.rb | 6 +- spec/features/oauth_spec.rb | 36 +- spec/lib/mastodon/cli/accounts_spec.rb | 87 ++- spec/lib/mastodon/cli/preview_cards_spec.rb | 43 ++ spec/models/poll_spec.rb | 19 + spec/spec_helper.rb | 6 + spec/support/stories/profile_stories.rb | 2 +- spec/system/new_statuses_spec.rb | 8 +- .../unreserved_username_validator_spec.rb | 123 +++- 32 files changed, 510 insertions(+), 540 deletions(-) diff --git a/Gemfile b/Gemfile index cb02c03a7410c6..a6ce6f27faf16b 100644 --- a/Gemfile +++ b/Gemfile @@ -106,6 +106,9 @@ group :test do # Used to split testing into chunks in CI gem 'rspec_chunked', '~> 0.6' + # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab + gem 'rspec-github', '~> 2.4', require: false + # RSpec progress bar formatter gem 'fuubar', '~> 2.5' diff --git a/Gemfile.lock b/Gemfile.lock index e2ad62f15a6bf3..db57150d94b62c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -513,7 +513,7 @@ GEM premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) public_suffix (5.0.3) - puma (6.3.1) + puma (6.4.0) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) @@ -602,6 +602,8 @@ GEM rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) + rspec-github (2.4.0) + rspec-core (~> 3.0) rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) @@ -636,11 +638,11 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.29.0) parser (>= 3.2.1.0) - rubocop-capybara (2.18.0) + rubocop-capybara (2.19.0) rubocop (~> 1.41) rubocop-factory_bot (2.23.1) rubocop (~> 1.33) - rubocop-performance (1.19.0) + rubocop-performance (1.19.1) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.20.2) @@ -887,6 +889,7 @@ DEPENDENCIES redis (~> 4.5) redis-namespace (~> 1.10) rqrcode (~> 2.2) + rspec-github (~> 2.4) rspec-rails (~> 6.0) rspec-retry (>= 0.6.2) rspec-sidekiq (~> 4.0) diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx index c2c82bdff73e09..657bb8e18d81ae 100644 --- a/app/javascript/mastodon/features/compose/components/search.jsx +++ b/app/javascript/mastodon/features/compose/components/search.jsx @@ -66,7 +66,7 @@ class Search extends PureComponent { { label: <>before: , action: e => { e.preventDefault(); this._insertText('before:'); } }, { label: <>during: , action: e => { e.preventDefault(); this._insertText('during:'); } }, { label: <>after: , action: e => { e.preventDefault(); this._insertText('after:'); } }, - { label: <>in: , action: e => { e.preventDefault(); this._insertText('in:'); } }, + { label: <>in: , action: e => { e.preventDefault(); this._insertText('in:'); } }, { label: <>order: , action: e => { e.preventDefault(); this._insertText('order:'); } }, ]; diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index ee51220df67d2b..4fb9522c167ff5 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -198,7 +198,9 @@ class SwitchingColumnsArea extends PureComponent { {singleColumn ? : null} {singleColumn && pathName.startsWith('/deck/') ? : null} + {/* Redirect old bookmarks (without /deck) with home-like routes to the advanced interface */} {!singleColumn && pathName === '/getting-started' ? : null} + {!singleColumn && pathName === '/home' ? : null} diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 0948e5d2559c31..c0757accea21e6 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -113,6 +113,7 @@ const initialPath = document.querySelector("head meta[name=initialPath]")?.getAt /** @type {boolean} */ export const hasMultiColumnPath = initialPath === '/' || initialPath === '/getting-started' + || initialPath === '/home' || initialPath.startsWith('/deck'); /** diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb index 567b10ade2b107..ed73751cb561dd 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -79,6 +79,8 @@ def indexes case @flags['in'] when 'library' [StatusesIndex] + when 'public' + [PublicStatusesIndex] else @options[:current_account].user&.setting_use_public_index ? [PublicStatusesIndex, StatusesIndex] : [StatusesIndex] end diff --git a/app/validators/unreserved_username_validator.rb b/app/validators/unreserved_username_validator.rb index f82f4b91d03282..55a8c835fae1c4 100644 --- a/app/validators/unreserved_username_validator.rb +++ b/app/validators/unreserved_username_validator.rb @@ -11,16 +11,31 @@ def validate(account) private + def reserved_username? + pam_username_reserved? || settings_username_reserved? + end + + def pam_username_reserved? + pam_controlled? && pam_reserves_username? + end + def pam_controlled? - return false unless Devise.pam_authentication && Devise.pam_controlled_service + Devise.pam_authentication && Devise.pam_controlled_service + end - Rpam2.account(Devise.pam_controlled_service, @username).present? + def pam_reserves_username? + Rpam2.account(Devise.pam_controlled_service, @username) end - def reserved_username? - return true if pam_controlled? - return false unless Setting.reserved_usernames + def settings_username_reserved? + settings_has_reserved_usernames? && settings_reserves_username? + end + + def settings_has_reserved_usernames? + Setting.reserved_usernames.present? + end + def settings_reserves_username? Setting.reserved_usernames.include?(@username.downcase) end end diff --git a/config/brakeman.ignore b/config/brakeman.ignore index a12fb26caf692e..f1ef5ed3b18fe1 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -33,76 +33,6 @@ ], "note": "" }, - { - "warning_type": "Denial of Service", - "warning_code": 76, - "fingerprint": "7b6abba5699755348e7ee82a4694bfbf574b41c7cce2d0db0f7c11ae3f983c72", - "check_name": "RegexDoS", - "message": "Model attribute used in regular expression", - "file": "lib/mastodon/cli/domains.rb", - "line": 128, - "link": "https://brakemanscanner.org/docs/warning_types/denial_of_service/", - "code": "/\\.?(#{DomainBlock.where(:severity => 1).pluck(:domain).map do\n Regexp.escape(domain)\n end.join(\"|\")})$/", - "render_path": null, - "location": { - "type": "method", - "class": "Mastodon::CLI::Domains", - "method": "crawl" - }, - "user_input": "DomainBlock.where(:severity => 1).pluck(:domain)", - "confidence": "Weak", - "cwe_id": [ - 20, - 185 - ], - "note": "" - }, - { - "warning_type": "Mass Assignment", - "warning_code": 105, - "fingerprint": "874be88fedf4c680926845e9a588d3197765a6ccbfdd76466b44cc00151c612e", - "check_name": "PermitAttributes", - "message": "Potentially dangerous key allowed for mass assignment", - "file": "app/controllers/api/v1/admin/reports_controller.rb", - "line": 88, - "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/", - "code": "params.permit(:resolved, :account_id, :target_account_id)", - "render_path": null, - "location": { - "type": "method", - "class": "Api::V1::Admin::ReportsController", - "method": "filter_params" - }, - "user_input": ":account_id", - "confidence": "High", - "cwe_id": [ - 915 - ], - "note": "" - }, - { - "warning_type": "Mass Assignment", - "warning_code": 105, - "fingerprint": "ab5035dd1a9f8c3a8d92fb2c37e8fe86fede4f87c91b71aa32e89c9eede602fc", - "check_name": "PermitAttributes", - "message": "Potentially dangerous key allowed for mass assignment", - "file": "app/controllers/api/v1/notifications_controller.rb", - "line": 81, - "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/", - "code": "params.permit(:account_id, :types => ([]), :exclude_types => ([]))", - "render_path": null, - "location": { - "type": "method", - "class": "Api::V1::NotificationsController", - "method": "browserable_params" - }, - "user_input": ":account_id", - "confidence": "High", - "cwe_id": [ - 915 - ], - "note": "" - }, { "warning_type": "Cross-Site Scripting", "warning_code": 4, diff --git a/docker-compose.yml b/docker-compose.yml index 1a180b089070f1..f7639d9867335c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,7 +111,7 @@ services: test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"] ## Uncomment to enable federation with tor instances along with adding the following ENV variables - ## http_proxy=http://privoxy:8118 + ## http_hidden_proxy=http://privoxy:8118 ## ALLOW_ACCESS_TO_HIDDEN_SERVICE=true # tor: # image: sirboops/tor diff --git a/lib/mastodon/cli/domains.rb b/lib/mastodon/cli/domains.rb index d17b25368181b6..329f1716725b52 100644 --- a/lib/mastodon/cli/domains.rb +++ b/lib/mastodon/cli/domains.rb @@ -125,7 +125,7 @@ def crawl(start = nil) failed = Concurrent::AtomicFixnum.new(0) start_at = Time.now.to_f seed = start ? [start] : Instance.pluck(:domain) - blocked_domains = /\.?(#{DomainBlock.where(severity: 1).pluck(:domain).map { |domain| Regexp.escape(domain) }.join('|')})$/ + blocked_domains = /\.?(#{Regexp.union(domain_block_suspended_domains).source})$/ progress = create_progress_bar pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0) @@ -189,6 +189,10 @@ def crawl(start = nil) private + def domain_block_suspended_domains + DomainBlock.suspend.pluck(:domain) + end + def stats_to_summary(stats, processed, failed, start_at) stats.compact! diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index d46fdb108d6315..cc9e3198b6fbdb 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -7,449 +7,319 @@ let(:account) { Fabricate(:account) } - describe 'GET #show' do - let(:format) { 'html' } + shared_examples 'unapproved account check' do + before { account.user.update(approved: false) } - let!(:status) { Fabricate(:status, account: account) } - let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) } - let!(:status_self_reply) { Fabricate(:status, account: account, thread: status) } - let!(:status_media) { Fabricate(:status, account: account) } - let!(:status_pinned) { Fabricate(:status, account: account) } - let!(:status_private) { Fabricate(:status, account: account, visibility: :private) } - let!(:status_direct) { Fabricate(:status, account: account, visibility: :direct) } - let!(:status_reblog) { Fabricate(:status, account: account, reblog: Fabricate(:status)) } + it 'returns http not found' do + get :show, params: { username: account.username, format: format } - before do - status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image) - account.pinned_statuses << status_pinned - account.pinned_statuses << status_private + expect(response).to have_http_status(404) end + end - shared_examples 'preliminary checks' do - context 'when account is not approved' do - before do - account.user.update(approved: false) - end - - it 'returns http not found' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(404) - end - end + shared_examples 'permanently suspended account check' do + before do + account.suspend! + account.deletion_request.destroy end - context 'with HTML' do - let(:format) { 'html' } + it 'returns http gone' do + get :show, params: { username: account.username, format: format } - it_behaves_like 'preliminary checks' - - context 'when account is permanently suspended' do - before do - account.suspend! - account.deletion_request.destroy - end - - it 'returns http gone' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(410) - end - end - - context 'when account is temporarily suspended' do - before do - account.suspend! - end - - it 'returns http forbidden' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(403) - end - end + expect(response).to have_http_status(410) + end + end - shared_examples 'common response characteristics' do - it 'returns http success' do - expect(response).to have_http_status(200) - end + shared_examples 'temporarily suspended account check' do |code: 403| + before { account.suspend! } - it 'returns Link header' do - expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account) - end + it 'returns appropriate http response code' do + get :show, params: { username: account.username, format: format } - it 'renders show template' do - expect(response).to render_template(:show) - end - end + expect(response).to have_http_status(code) + end + end - context 'with a normal account in an HTML request' do - before do - get :show, params: { username: account.username, format: format } - end + describe 'GET #show' do + context 'with basic account status checks' do + context 'with HTML' do + let(:format) { 'html' } - it_behaves_like 'common response characteristics' + it_behaves_like 'unapproved account check' + it_behaves_like 'permanently suspended account check' + it_behaves_like 'temporarily suspended account check' end - context 'with replies' do - before do - allow(controller).to receive(:replies_requested?).and_return(true) - get :show, params: { username: account.username, format: format } - end + context 'with JSON' do + let(:format) { 'json' } - it_behaves_like 'common response characteristics' + it_behaves_like 'unapproved account check' + it_behaves_like 'permanently suspended account check' + it_behaves_like 'temporarily suspended account check', code: 200 end - context 'with media' do - before do - allow(controller).to receive(:media_requested?).and_return(true) - get :show, params: { username: account.username, format: format } - end + context 'with RSS' do + let(:format) { 'rss' } - it_behaves_like 'common response characteristics' - end - - context 'with tag' do - let(:tag) { Fabricate(:tag) } - - let!(:status_tag) { Fabricate(:status, account: account) } - - before do - allow(controller).to receive(:tag_requested?).and_return(true) - status_tag.tags << tag - get :show, params: { username: account.username, format: format, tag: tag.to_param } - end - - it_behaves_like 'common response characteristics' + it_behaves_like 'unapproved account check' + it_behaves_like 'permanently suspended account check' + it_behaves_like 'temporarily suspended account check' end end - context 'with JSON' do - let(:authorized_fetch_mode) { false } - let(:format) { 'json' } + context 'with existing statuses' do + let!(:status) { Fabricate(:status, account: account) } + let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) } + let!(:status_self_reply) { Fabricate(:status, account: account, thread: status) } + let!(:status_media) { Fabricate(:status, account: account) } + let!(:status_pinned) { Fabricate(:status, account: account) } + let!(:status_private) { Fabricate(:status, account: account, visibility: :private) } + let!(:status_direct) { Fabricate(:status, account: account, visibility: :direct) } + let!(:status_reblog) { Fabricate(:status, account: account, reblog: Fabricate(:status)) } before do - allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode) + status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image) + account.pinned_statuses << status_pinned + account.pinned_statuses << status_private end - it_behaves_like 'preliminary checks' - - context 'when account is suspended permanently' do - before do - account.suspend! - account.deletion_request.destroy - end - - it 'returns http gone' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(410) - end - end + context 'with HTML' do + let(:format) { 'html' } - context 'when account is suspended temporarily' do - before do - account.suspend! - end + shared_examples 'common HTML response' do + it 'returns a standard HTML response', :aggregate_failures do + expect(response).to have_http_status(200) - it 'returns http success' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(200) - end - end + expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account) - context 'with a normal account in a JSON request' do - before do - get :show, params: { username: account.username, format: format } + expect(response).to render_template(:show) + end end - it 'returns http success' do - expect(response).to have_http_status(200) - end + context 'with a normal account in an HTML request' do + before do + get :show, params: { username: account.username, format: format } + end - it 'returns application/activity+json' do - expect(response.media_type).to eq 'application/activity+json' + it_behaves_like 'common HTML response' end - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' + context 'with replies' do + before do + allow(controller).to receive(:replies_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end - it 'renders account' do - json = body_as_json - expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + it_behaves_like 'common HTML response' end - context 'with authorized fetch mode' do - let(:authorized_fetch_mode) { true } - - it 'returns http unauthorized' do - expect(response).to have_http_status(401) + context 'with media' do + before do + allow(controller).to receive(:media_requested?).and_return(true) + get :show, params: { username: account.username, format: format } end - end - end - - context 'when signed in' do - let(:user) { Fabricate(:user) } - before do - sign_in(user) - get :show, params: { username: account.username, format: format } + it_behaves_like 'common HTML response' end - it 'returns http success' do - expect(response).to have_http_status(200) - end + context 'with tag' do + let(:tag) { Fabricate(:tag) } - it 'returns application/activity+json' do - expect(response.media_type).to eq 'application/activity+json' - end + let!(:status_tag) { Fabricate(:status, account: account) } - it 'returns private Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'private' - end + before do + allow(controller).to receive(:tag_requested?).and_return(true) + status_tag.tags << tag + get :show, params: { username: account.username, format: format, tag: tag.to_param } + end - it 'renders account' do - json = body_as_json - expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + it_behaves_like 'common HTML response' end end - context 'with signature' do - let(:remote_account) { Fabricate(:account, domain: 'example.com') } + context 'with JSON' do + let(:authorized_fetch_mode) { false } + let(:format) { 'json' } before do - allow(controller).to receive(:signed_request_actor).and_return(remote_account) - get :show, params: { username: account.username, format: format } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns application/activity+json' do - expect(response.media_type).to eq 'application/activity+json' + allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode) end - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - - it 'renders account' do - json = body_as_json - expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end - - context 'with authorized fetch mode' do - let(:authorized_fetch_mode) { true } + context 'with a normal account in a JSON request' do + before do + get :show, params: { username: account.username, format: format } + end - it 'returns http success' do + it 'returns a JSON version of the account', :aggregate_failures do expect(response).to have_http_status(200) - end - it 'returns application/activity+json' do expect(response.media_type).to eq 'application/activity+json' - end - - it 'returns private Cache-Control header' do - expect(response.headers['Cache-Control']).to include 'private' - end - - it 'returns Vary header with Signature' do - expect(response.headers['Vary']).to include 'Signature' - end - it 'renders account' do - json = body_as_json - expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) end - end - end - end - context 'with RSS' do - let(:format) { 'rss' } + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - it_behaves_like 'preliminary checks' + context 'with authorized fetch mode' do + let(:authorized_fetch_mode) { true } - context 'when account is permanently suspended' do - before do - account.suspend! - account.deletion_request.destroy - end - - it 'returns http gone' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(410) - end - end - - context 'when account is temporarily suspended' do - before do - account.suspend! + it 'returns http unauthorized' do + expect(response).to have_http_status(401) + end + end end - it 'returns http forbidden' do - get :show, params: { username: account.username, format: format } - expect(response).to have_http_status(403) - end - end + context 'when signed in' do + let(:user) { Fabricate(:user) } - shared_examples 'common response characteristics' do - it 'returns http success' do - expect(response).to have_http_status(200) - end + before do + sign_in(user) + get :show, params: { username: account.username, format: format } + end - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - end + it 'returns a private JSON version of the account', :aggregate_failures do + expect(response).to have_http_status(200) - context 'with a normal account in an RSS request' do - before do - get :show, params: { username: account.username, format: format } - end + expect(response.media_type).to eq 'application/activity+json' - it_behaves_like 'common response characteristics' + expect(response.headers['Cache-Control']).to include 'private' - it 'renders public status' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) + expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end end - it 'renders self-reply' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com') } - it 'renders status with media' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) - end + before do + allow(controller).to receive(:signed_request_actor).and_return(remote_account) + get :show, params: { username: account.username, format: format } + end - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end + it 'returns a JSON version of the account', :aggregate_failures do + expect(response).to have_http_status(200) - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end + expect(response.media_type).to eq 'application/activity+json' - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end + expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end - it 'does not render reply to someone else' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) - end - end + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - context 'with replies' do - before do - allow(controller).to receive(:replies_requested?).and_return(true) - get :show, params: { username: account.username, format: format } - end + context 'with authorized fetch mode' do + let(:authorized_fetch_mode) { true } - it_behaves_like 'common response characteristics' + it 'returns a private signature JSON version of the account', :aggregate_failures do + expect(response).to have_http_status(200) - it 'renders public status' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) - end + expect(response.media_type).to eq 'application/activity+json' - it 'renders self-reply' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end + expect(response.headers['Cache-Control']).to include 'private' - it 'renders status with media' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) - end + expect(response.headers['Vary']).to include 'Signature' - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end - - it 'renders reply to someone else' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reply)) + expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end + end end end - context 'with media' do - before do - allow(controller).to receive(:media_requested?).and_return(true) - get :show, params: { username: account.username, format: format } - end - - it_behaves_like 'common response characteristics' + context 'with RSS' do + let(:format) { 'rss' } - it 'does not render public status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) - end - - it 'does not render self-reply' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end - - it 'renders status with media' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) - end + shared_examples 'common RSS response' do + it 'returns http success' do + expect(response).to have_http_status(200) + end - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' end - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end + context 'with a normal account in an RSS request' do + before do + get :show, params: { username: account.username, format: format } + end - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end + it_behaves_like 'common RSS response' - it 'does not render reply to someone else' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + it 'responds with correct statuses', :aggregate_failures do + expect(response.body).to include_status_tag(status_media) + expect(response.body).to include_status_tag(status_self_reply) + expect(response.body).to include_status_tag(status) + expect(response.body).to_not include_status_tag(status_direct) + expect(response.body).to_not include_status_tag(status_private) + expect(response.body).to_not include_status_tag(status_reblog.reblog) + expect(response.body).to_not include_status_tag(status_reply) + end end - end - - context 'with tag' do - let(:tag) { Fabricate(:tag) } - - let!(:status_tag) { Fabricate(:status, account: account) } - before do - allow(controller).to receive(:tag_requested?).and_return(true) - status_tag.tags << tag - get :show, params: { username: account.username, format: format, tag: tag.to_param } - end + context 'with replies' do + before do + allow(controller).to receive(:replies_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end - it_behaves_like 'common response characteristics' + it_behaves_like 'common RSS response' - it 'does not render public status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) + it 'responds with correct statuses with replies', :aggregate_failures do + expect(response.body).to include_status_tag(status_media) + expect(response.body).to include_status_tag(status_reply) + expect(response.body).to include_status_tag(status_self_reply) + expect(response.body).to include_status_tag(status) + expect(response.body).to_not include_status_tag(status_direct) + expect(response.body).to_not include_status_tag(status_private) + expect(response.body).to_not include_status_tag(status_reblog.reblog) + end end - it 'does not render self-reply' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end + context 'with media' do + before do + allow(controller).to receive(:media_requested?).and_return(true) + get :show, params: { username: account.username, format: format } + end - it 'does not render status with media' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media)) - end + it_behaves_like 'common RSS response' - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) + it 'responds with correct statuses with media', :aggregate_failures do + expect(response.body).to include_status_tag(status_media) + expect(response.body).to_not include_status_tag(status_direct) + expect(response.body).to_not include_status_tag(status_private) + expect(response.body).to_not include_status_tag(status_reblog.reblog) + expect(response.body).to_not include_status_tag(status_reply) + expect(response.body).to_not include_status_tag(status_self_reply) + expect(response.body).to_not include_status_tag(status) + end end - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end + context 'with tag' do + let(:tag) { Fabricate(:tag) } - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end + let!(:status_tag) { Fabricate(:status, account: account) } - it 'does not render reply to someone else' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) - end + before do + allow(controller).to receive(:tag_requested?).and_return(true) + status_tag.tags << tag + get :show, params: { username: account.username, format: format, tag: tag.to_param } + end - it 'renders status with tag' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag)) + it_behaves_like 'common RSS response' + + it 'responds with correct statuses with a tag', :aggregate_failures do + expect(response.body).to include_status_tag(status_tag) + expect(response.body).to_not include_status_tag(status_direct) + expect(response.body).to_not include_status_tag(status_media) + expect(response.body).to_not include_status_tag(status_private) + expect(response.body).to_not include_status_tag(status_reblog.reblog) + expect(response.body).to_not include_status_tag(status_reply) + expect(response.body).to_not include_status_tag(status_self_reply) + expect(response.body).to_not include_status_tag(status) + end end end end end + + def include_status_tag(status) + include ActivityPub::TagManager.instance.url_for(status) + end end diff --git a/spec/features/admin/accounts_spec.rb b/spec/features/admin/accounts_spec.rb index 6d7bab1844ccbf..ad9c51485a76db 100644 --- a/spec/features/admin/accounts_spec.rb +++ b/spec/features/admin/accounts_spec.rb @@ -22,7 +22,7 @@ context 'without selecting any accounts' do it 'displays a notice about account selection' do - click_on button_for_suspend + click_button button_for_suspend expect(page).to have_content(selection_error_text) end @@ -32,7 +32,7 @@ it 'suspends the account' do batch_checkbox_for(approved_user_account).check - click_on button_for_suspend + click_button button_for_suspend expect(approved_user_account.reload).to be_suspended end @@ -42,7 +42,7 @@ it 'approves the account user' do batch_checkbox_for(unapproved_user_account).check - click_on button_for_approve + click_button button_for_approve expect(unapproved_user_account.reload.user).to be_approved end @@ -52,7 +52,7 @@ it 'rejects and removes the account' do batch_checkbox_for(unapproved_user_account).check - click_on button_for_reject + click_button button_for_reject expect { unapproved_user_account.reload }.to raise_error(ActiveRecord::RecordNotFound) end diff --git a/spec/features/admin/custom_emojis_spec.rb b/spec/features/admin/custom_emojis_spec.rb index 8a8b6efcd119fb..3fea8f06fe6f50 100644 --- a/spec/features/admin/custom_emojis_spec.rb +++ b/spec/features/admin/custom_emojis_spec.rb @@ -16,7 +16,7 @@ context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_enable + click_button button_for_enable expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/domain_blocks_spec.rb b/spec/features/admin/domain_blocks_spec.rb index 4672c1e1a9ee45..0d7b90c21cd5d2 100644 --- a/spec/features/admin/domain_blocks_spec.rb +++ b/spec/features/admin/domain_blocks_spec.rb @@ -13,7 +13,7 @@ fill_in 'domain_block_domain', with: 'example.com' select I18n.t('admin.domain_blocks.new.severity.silence'), from: 'domain_block_severity' - click_on I18n.t('admin.domain_blocks.new.create') + click_button I18n.t('admin.domain_blocks.new.create') expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true end @@ -25,13 +25,13 @@ fill_in 'domain_block_domain', with: 'example.com' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_on I18n.t('admin.domain_blocks.new.create') + click_button I18n.t('admin.domain_blocks.new.create') # It presents a confirmation screen expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) # Confirming creates a block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true end @@ -45,13 +45,13 @@ fill_in 'domain_block_domain', with: 'example.com' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_on I18n.t('admin.domain_blocks.new.create') + click_button I18n.t('admin.domain_blocks.new.create') # It presents a confirmation screen expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) # Confirming updates the block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(domain_block.reload.severity).to eq 'suspend' end @@ -65,13 +65,13 @@ fill_in 'domain_block_domain', with: 'subdomain.example.com' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_on I18n.t('admin.domain_blocks.new.create') + click_button I18n.t('admin.domain_blocks.new.create') # It presents a confirmation screen expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'subdomain.example.com')) # Confirming creates the block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(DomainBlock.where(domain: 'subdomain.example.com', severity: 'suspend')).to exist @@ -88,13 +88,13 @@ visit edit_admin_domain_block_path(domain_block) select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_on I18n.t('generic.save_changes') + click_button I18n.t('generic.save_changes') # It presents a confirmation screen expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) # Confirming updates the block - click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(domain_block.reload.severity).to eq 'suspend' end diff --git a/spec/features/admin/email_domain_blocks_spec.rb b/spec/features/admin/email_domain_blocks_spec.rb index 14959cbe74b0dd..80efe72e95c568 100644 --- a/spec/features/admin/email_domain_blocks_spec.rb +++ b/spec/features/admin/email_domain_blocks_spec.rb @@ -16,7 +16,7 @@ context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_delete + click_button button_for_delete expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/ip_blocks_spec.rb b/spec/features/admin/ip_blocks_spec.rb index c9b16f6f78fb1d..465c8891907013 100644 --- a/spec/features/admin/ip_blocks_spec.rb +++ b/spec/features/admin/ip_blocks_spec.rb @@ -16,7 +16,7 @@ context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_delete + click_button button_for_delete expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/software_updates_spec.rb b/spec/features/admin/software_updates_spec.rb index 4a635d1a794f8f..a2373d35a6080c 100644 --- a/spec/features/admin/software_updates_spec.rb +++ b/spec/features/admin/software_updates_spec.rb @@ -11,13 +11,13 @@ it 'shows a link to the software updates page, which links to release notes' do visit settings_profile_path - click_on I18n.t('admin.critical_update_pending') + click_link I18n.t('admin.critical_update_pending') expect(page).to have_title(I18n.t('admin.software_updates.title')) expect(page).to have_content('99.99.99') - click_on I18n.t('admin.software_updates.release_notes') + click_link I18n.t('admin.software_updates.release_notes') expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true) end end diff --git a/spec/features/admin/statuses_spec.rb b/spec/features/admin/statuses_spec.rb index 531d0de9538b21..a21c901a921b23 100644 --- a/spec/features/admin/statuses_spec.rb +++ b/spec/features/admin/statuses_spec.rb @@ -17,7 +17,7 @@ context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_report + click_button button_for_report expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/trends/links/preview_card_providers_spec.rb b/spec/features/admin/trends/links/preview_card_providers_spec.rb index dca89117b1079a..cf9796abf362cd 100644 --- a/spec/features/admin/trends/links/preview_card_providers_spec.rb +++ b/spec/features/admin/trends/links/preview_card_providers_spec.rb @@ -16,7 +16,7 @@ context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_allow + click_button button_for_allow expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/trends/links_spec.rb b/spec/features/admin/trends/links_spec.rb index 99638bc069ffbe..8b1b991a5a85cc 100644 --- a/spec/features/admin/trends/links_spec.rb +++ b/spec/features/admin/trends/links_spec.rb @@ -16,7 +16,7 @@ context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_allow + click_button button_for_allow expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/trends/statuses_spec.rb b/spec/features/admin/trends/statuses_spec.rb index 779a15d38fbec3..a578ab05593c08 100644 --- a/spec/features/admin/trends/statuses_spec.rb +++ b/spec/features/admin/trends/statuses_spec.rb @@ -16,7 +16,7 @@ context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_allow + click_button button_for_allow expect(page).to have_content(selection_error_text) end diff --git a/spec/features/admin/trends/tags_spec.rb b/spec/features/admin/trends/tags_spec.rb index 52e49c3a5d92b6..7502bc8c6f50c1 100644 --- a/spec/features/admin/trends/tags_spec.rb +++ b/spec/features/admin/trends/tags_spec.rb @@ -16,7 +16,7 @@ context 'without selecting any records' do it 'displays a notice about selection' do - click_on button_for_allow + click_button button_for_allow expect(page).to have_content(selection_error_text) end diff --git a/spec/features/captcha_spec.rb b/spec/features/captcha_spec.rb index db89ff3e616c58..6ccf066208fae5 100644 --- a/spec/features/captcha_spec.rb +++ b/spec/features/captcha_spec.rb @@ -27,7 +27,7 @@ expect(user.reload.confirmed?).to be false # It redirects to app and confirms user - click_on I18n.t('challenge.confirm') + click_button I18n.t('challenge.confirm') expect(user.reload.confirmed?).to be true expect(page).to have_current_path(/\A#{client_app.confirmation_redirect_uri}/, url: true) end diff --git a/spec/features/log_in_spec.rb b/spec/features/log_in_spec.rb index c64e19d2b7c43f..7e5196aba99564 100644 --- a/spec/features/log_in_spec.rb +++ b/spec/features/log_in_spec.rb @@ -19,7 +19,7 @@ it 'A valid email and password user is able to log in' do fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(subject).to have_css('div.app-holder') end @@ -27,7 +27,7 @@ it 'A invalid email and password user is not able to log in' do fill_in 'user_email', with: 'invalid_email' fill_in 'user_password', with: 'invalid_password' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(subject).to have_css('.flash-message', text: failure_message('invalid')) end @@ -38,7 +38,7 @@ it 'A unconfirmed user is able to log in' do fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(subject).to have_css('div.admin-wrapper') end diff --git a/spec/features/oauth_spec.rb b/spec/features/oauth_spec.rb index 967956cc8ea91a..0e612b56a5a24f 100644 --- a/spec/features/oauth_spec.rb +++ b/spec/features/oauth_spec.rb @@ -20,7 +20,7 @@ expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon authorizing, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.authorize') + click_button I18n.t('doorkeeper.authorizations.buttons.authorize') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It grants the app access to the account @@ -35,7 +35,7 @@ expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.deny')) # Upon denying, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.deny') + click_button I18n.t('doorkeeper.authorizations.buttons.deny') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It does not grant the app access to the account @@ -63,17 +63,17 @@ # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('auth.login')) # Logging in redirects to an authorization page fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon authorizing, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.authorize') + click_button I18n.t('doorkeeper.authorizations.buttons.authorize') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It grants the app access to the account @@ -90,17 +90,17 @@ # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('auth.login')) # Logging in redirects to an authorization page fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon denying, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.deny') + click_button I18n.t('doorkeeper.authorizations.buttons.deny') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It does not grant the app access to the account @@ -120,27 +120,27 @@ # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('auth.login')) # Logging in redirects to a two-factor authentication page fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) # Filling in an incorrect two-factor authentication code presents the form again fill_in 'user_otp_attempt', with: 'wrong' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) # Filling in the correct TOTP code redirects to an app authorization page fill_in 'user_otp_attempt', with: user.current_otp - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon authorizing, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.authorize') + click_button I18n.t('doorkeeper.authorizations.buttons.authorize') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It grants the app access to the account @@ -157,27 +157,27 @@ # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('auth.login')) # Logging in redirects to a two-factor authentication page fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) # Filling in an incorrect two-factor authentication code presents the form again fill_in 'user_otp_attempt', with: 'wrong' - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp')) # Filling in the correct TOTP code redirects to an app authorization page fill_in 'user_otp_attempt', with: user.current_otp - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon denying, it redirects to the apps' callback URL - click_on I18n.t('doorkeeper.authorizations.buttons.deny') + click_button I18n.t('doorkeeper.authorizations.buttons.deny') expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true) # It does not grant the app access to the account diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index a263d673de760b..5ecea5ea162b4e 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -6,6 +6,24 @@ describe Mastodon::CLI::Accounts do let(:cli) { described_class.new } + # `parallelize_with_progress` cannot run in transactions, so instead, + # stub it with an alternative implementation that runs sequentially + # and can run in transactions. + def stub_parallelize_with_progress! + allow(cli).to receive(:parallelize_with_progress) do |scope, &block| + aggregate = 0 + total = 0 + + scope.reorder(nil).find_each do |record| + value = block.call(record) + aggregate += value if value.is_a?(Integer) + total += 1 + end + + [total, aggregate] + end + end + describe '.exit_on_failure?' do it 'returns true' do expect(described_class.exit_on_failure?).to be true @@ -551,20 +569,15 @@ let!(:follower_rony) { Fabricate(:account, username: 'rony') } let!(:follower_charles) { Fabricate(:account, username: 'charles') } let(:follow_service) { instance_double(FollowService, call: nil) } - let(:scope) { Account.local.without_suspended } before do - allow(cli).to receive(:parallelize_with_progress).and_yield(follower_bob) - .and_yield(follower_rony) - .and_yield(follower_charles) - .and_return([3, nil]) allow(FollowService).to receive(:new).and_return(follow_service) + stub_parallelize_with_progress! end it 'makes all local accounts follow the target account' do cli.follow(target_account.username) - expect(cli).to have_received(:parallelize_with_progress).with(scope).once expect(follow_service).to have_received(:call).with(follower_bob, target_account, any_args).once expect(follow_service).to have_received(:call).with(follower_rony, target_account, any_args).once expect(follow_service).to have_received(:call).with(follower_charles, target_account, any_args).once @@ -572,7 +585,7 @@ it 'displays a successful message' do expect { cli.follow(target_account.username) }.to output( - a_string_including('OK, followed target from 3 accounts') + a_string_including("OK, followed target from #{Account.local.count} accounts") ).to_stdout end end @@ -592,26 +605,21 @@ context 'when the given username is found' do let!(:target_account) { Fabricate(:account) } - let!(:follower_chris) { Fabricate(:account, username: 'chris') } - let!(:follower_rambo) { Fabricate(:account, username: 'rambo') } - let!(:follower_ana) { Fabricate(:account, username: 'ana') } + let!(:follower_chris) { Fabricate(:account, username: 'chris', domain: nil) } + let!(:follower_rambo) { Fabricate(:account, username: 'rambo', domain: nil) } + let!(:follower_ana) { Fabricate(:account, username: 'ana', domain: nil) } let(:unfollow_service) { instance_double(UnfollowService, call: nil) } - let(:scope) { target_account.followers.local } before do accounts = [follower_chris, follower_rambo, follower_ana] - accounts.each { |account| target_account.follow!(account) } - allow(cli).to receive(:parallelize_with_progress).and_yield(follower_chris) - .and_yield(follower_rambo) - .and_yield(follower_ana) - .and_return([3, nil]) + accounts.each { |account| account.follow!(target_account) } allow(UnfollowService).to receive(:new).and_return(unfollow_service) + stub_parallelize_with_progress! end it 'makes all local accounts unfollow the target account' do cli.unfollow(target_account.username) - expect(cli).to have_received(:parallelize_with_progress).with(scope).once expect(unfollow_service).to have_received(:call).with(follower_chris, target_account).once expect(unfollow_service).to have_received(:call).with(follower_rambo, target_account).once expect(unfollow_service).to have_received(:call).with(follower_ana, target_account).once @@ -671,6 +679,8 @@ let(:scope) { Account.remote } before do + # TODO: we should be using `stub_parallelize_with_progress!` but + # this makes the assertions harder to write allow(cli).to receive(:parallelize_with_progress).and_yield(remote_account_example_com) .and_yield(account_example_net) .and_return([2, nil]) @@ -1112,26 +1122,19 @@ describe '#cull' do let(:delete_account_service) { instance_double(DeleteAccountService, call: nil) } - let!(:tom) { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com') } - let!(:bob) { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org') } - let!(:gon) { Fabricate(:account, updated_at: 15.days.ago, last_webfingered_at: 15.days.ago, username: 'gon', uri: 'https://example.net/users/gon', domain: 'example.net') } - let!(:ana) { Fabricate(:account, username: 'ana', uri: 'https://example.com/users/ana', domain: 'example.com') } - let!(:tales) { Fabricate(:account, updated_at: 10.days.ago, last_webfingered_at: nil, username: 'tales', uri: 'https://example.net/users/tales', domain: 'example.net') } + let!(:tom) { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com', protocol: :activitypub) } + let!(:bob) { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org', protocol: :activitypub) } + let!(:gon) { Fabricate(:account, updated_at: 15.days.ago, last_webfingered_at: 15.days.ago, username: 'gon', uri: 'https://example.net/users/gon', domain: 'example.net', protocol: :activitypub) } + let!(:ana) { Fabricate(:account, username: 'ana', uri: 'https://example.com/users/ana', domain: 'example.com', protocol: :activitypub) } + let!(:tales) { Fabricate(:account, updated_at: 10.days.ago, last_webfingered_at: nil, username: 'tales', uri: 'https://example.net/users/tales', domain: 'example.net', protocol: :activitypub) } before do allow(DeleteAccountService).to receive(:new).and_return(delete_account_service) end context 'when no domain is specified' do - let(:scope) { Account.remote.where(protocol: :activitypub).partitioned } - before do - allow(cli).to receive(:parallelize_with_progress).and_yield(tom) - .and_yield(bob) - .and_yield(gon) - .and_yield(ana) - .and_yield(tales) - .and_return([5, 3]) + stub_parallelize_with_progress! stub_request(:head, 'https://example.org/users/bob').to_return(status: 404) stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) stub_request(:head, 'https://example.net/users/tales').to_return(status: 200) @@ -1140,7 +1143,6 @@ it 'deletes all inactive remote accounts that longer exist in the origin server' do cli.cull - expect(cli).to have_received(:parallelize_with_progress).with(scope).once expect(delete_account_service).to have_received(:call).with(bob, reserve_username: false).once expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once end @@ -1148,35 +1150,27 @@ it 'does not delete any active remote account that still exists in the origin server' do cli.cull - expect(cli).to have_received(:parallelize_with_progress).with(scope).once expect(delete_account_service).to_not have_received(:call).with(tom, reserve_username: false) expect(delete_account_service).to_not have_received(:call).with(ana, reserve_username: false) expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false) end it 'touches inactive remote accounts that have not been deleted' do - allow(tales).to receive(:touch) - - cli.cull - - expect(tales).to have_received(:touch).once + expect { cli.cull }.to(change { tales.reload.updated_at }) end it 'displays the summary correctly' do expect { cli.cull }.to output( - a_string_including('Visited 5 accounts, removed 3') + a_string_including('Visited 5 accounts, removed 2') ).to_stdout end end context 'when a domain is specified' do let(:domain) { 'example.net' } - let(:scope) { Account.remote.where(protocol: :activitypub, domain: domain).partitioned } before do - allow(cli).to receive(:parallelize_with_progress).and_yield(gon) - .and_yield(tales) - .and_return([2, 2]) + stub_parallelize_with_progress! stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) stub_request(:head, 'https://example.net/users/tales').to_return(status: 404) end @@ -1184,13 +1178,12 @@ it 'deletes inactive remote accounts that longer exist in the specified domain' do cli.cull(domain) - expect(cli).to have_received(:parallelize_with_progress).with(scope).once expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once end it 'displays the summary correctly' do - expect { cli.cull }.to output( + expect { cli.cull(domain) }.to output( a_string_including('Visited 2 accounts, removed 2') ).to_stdout end @@ -1199,7 +1192,9 @@ context 'when a domain is unavailable' do shared_examples 'an unavailable domain' do before do - allow(cli).to receive(:parallelize_with_progress).and_yield(tales).and_return([1, 0]) + stub_parallelize_with_progress! + stub_request(:head, 'https://example.org/users/bob').to_return(status: 200) + stub_request(:head, 'https://example.net/users/gon').to_return(status: 200) end it 'skips accounts from the unavailable domain' do @@ -1210,7 +1205,7 @@ it 'displays the summary correctly' do expect { cli.cull }.to output( - a_string_including("Visited 1 accounts, removed 0\nThe following domains were not available during the check:\n example.net") + a_string_including("Visited 5 accounts, removed 0\nThe following domains were not available during the check:\n example.net") ).to_stdout end end diff --git a/spec/lib/mastodon/cli/preview_cards_spec.rb b/spec/lib/mastodon/cli/preview_cards_spec.rb index b4b018b3be5cb5..1e064ed58ed0cf 100644 --- a/spec/lib/mastodon/cli/preview_cards_spec.rb +++ b/spec/lib/mastodon/cli/preview_cards_spec.rb @@ -4,9 +4,52 @@ require 'mastodon/cli/preview_cards' describe Mastodon::CLI::PreviewCards do + let(:cli) { described_class.new } + describe '.exit_on_failure?' do it 'returns true' do expect(described_class.exit_on_failure?).to be true end end + + describe '#remove' do + context 'with relevant preview cards' do + before do + Fabricate(:preview_card, updated_at: 10.years.ago, type: :link) + Fabricate(:preview_card, updated_at: 10.months.ago, type: :photo) + Fabricate(:preview_card, updated_at: 10.days.ago, type: :photo) + end + + context 'with no arguments' do + it 'deletes thumbnails for local preview cards' do + expect { cli.invoke(:remove) }.to output( + a_string_including('Removed 2 preview cards') + .and(a_string_including('approx. 119 KB')) + ).to_stdout + end + end + + context 'with the --link option' do + let(:options) { { link: true } } + + it 'deletes thumbnails for local preview cards' do + expect { cli.invoke(:remove, [], options) }.to output( + a_string_including('Removed 1 link-type preview cards') + .and(a_string_including('approx. 59.6 KB')) + ).to_stdout + end + end + + context 'with the --days option' do + let(:options) { { days: 365 } } + + it 'deletes thumbnails for local preview cards' do + expect { cli.invoke(:remove, [], options) }.to output( + a_string_including('Removed 1 preview cards') + .and(a_string_including('approx. 59.6 KB')) + ).to_stdout + end + end + end + end end diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb index 8ae04ca41f8188..5aa5548cc83a1f 100644 --- a/spec/models/poll_spec.rb +++ b/spec/models/poll_spec.rb @@ -29,4 +29,23 @@ end end end + + describe 'validations' do + context 'when valid' do + let(:poll) { Fabricate.build(:poll) } + + it 'is valid with valid attributes' do + expect(poll).to be_valid + end + end + + context 'when not valid' do + let(:poll) { Fabricate.build(:poll, expires_at: nil) } + + it 'is invalid without an expire date' do + poll.valid? + expect(poll).to model_have_error_on_field(:expires_at) + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 85a43f5d00969a..95bed8f59b80b3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -42,6 +42,12 @@ # for RSpec::Retry config.verbose_retry = true config.display_try_failure_messages = true + + # Use the GitHub Annotations formatter for CI + if ENV['GITHUB_ACTIONS'] == 'true' + require 'rspec/github' + config.add_formatter RSpec::Github::Formatter + end end def body_as_json diff --git a/spec/support/stories/profile_stories.rb b/spec/support/stories/profile_stories.rb index 2b345ddef10b31..82667ca080c99b 100644 --- a/spec/support/stories/profile_stories.rb +++ b/spec/support/stories/profile_stories.rb @@ -18,7 +18,7 @@ def as_a_logged_in_user visit new_user_session_path fill_in 'user_email', with: email fill_in 'user_password', with: password - click_on I18n.t('auth.login') + click_button I18n.t('auth.login') end def with_alice_as_local_user diff --git a/spec/system/new_statuses_spec.rb b/spec/system/new_statuses_spec.rb index 6faed6c808c30c..244101f4d4fd6b 100644 --- a/spec/system/new_statuses_spec.rb +++ b/spec/system/new_statuses_spec.rb @@ -24,10 +24,10 @@ within('.compose-form') do fill_in "What's on your mind?", with: status_text - click_on 'Publish!' + click_button 'Publish!' end - expect(subject).to have_selector('.status__content__text', text: status_text) + expect(subject).to have_css('.status__content__text', text: status_text) end it 'can be posted again' do @@ -37,9 +37,9 @@ within('.compose-form') do fill_in "What's on your mind?", with: status_text - click_on 'Publish!' + click_button 'Publish!' end - expect(subject).to have_selector('.status__content__text', text: status_text) + expect(subject).to have_css('.status__content__text', text: status_text) end end diff --git a/spec/validators/unreserved_username_validator_spec.rb b/spec/validators/unreserved_username_validator_spec.rb index 6f353eeafdc517..0eb5f83683df4f 100644 --- a/spec/validators/unreserved_username_validator_spec.rb +++ b/spec/validators/unreserved_username_validator_spec.rb @@ -2,41 +2,118 @@ require 'rails_helper' -RSpec.describe UnreservedUsernameValidator, type: :validator do - describe '#validate' do - before do - allow(validator).to receive(:reserved_username?) { reserved_username } - validator.validate(account) - end +describe UnreservedUsernameValidator do + let(:record_class) do + Class.new do + include ActiveModel::Validations + attr_accessor :username - let(:validator) { described_class.new } - let(:account) { instance_double(Account, username: username, errors: errors) } - let(:errors) { instance_double(ActiveModel::Errors, add: nil) } + validates_with UnreservedUsernameValidator + end + end + let(:record) { record_class.new } - context 'when @username is blank?' do - let(:username) { nil } + describe '#validate' do + context 'when username is nil' do + it 'does not add errors' do + record.username = nil - it 'not calls errors.add' do - expect(errors).to_not have_received(:add).with(:username, any_args) + expect(record).to be_valid + expect(record.errors).to be_empty end end - context 'when @username is not blank?' do - let(:username) { 'f' } + context 'when PAM is enabled' do + before do + allow(Devise).to receive(:pam_authentication).and_return(true) + end + + context 'with a pam service available' do + let(:service) { double } + let(:pam_class) do + Class.new do + def self.account(service, username); end + end + end + + before do + stub_const('Rpam2', pam_class) + allow(Devise).to receive(:pam_controlled_service).and_return(service) + end + + context 'when the account exists' do + before do + allow(Rpam2).to receive(:account).with(service, 'username').and_return(true) + end + + it 'adds errors to the record' do + record.username = 'username' + + expect(record).to_not be_valid + expect(record.errors.first.attribute).to eq(:username) + expect(record.errors.first.type).to eq(:reserved) + end + end + + context 'when the account does not exist' do + before do + allow(Rpam2).to receive(:account).with(service, 'username').and_return(false) + end - context 'with reserved_username?' do - let(:reserved_username) { true } + it 'does not add errors to the record' do + record.username = 'username' - it 'calls errors.add' do - expect(errors).to have_received(:add).with(:username, :reserved) + expect(record).to be_valid + expect(record.errors).to be_empty + end end end - context 'when username is not reserved' do - let(:reserved_username) { false } + context 'without a pam service' do + before do + allow(Devise).to receive(:pam_controlled_service).and_return(false) + end + + context 'when there are not any reserved usernames' do + before do + stub_reserved_usernames(nil) + end + + it 'does not add errors to the record' do + record.username = 'username' + + expect(record).to be_valid + expect(record.errors).to be_empty + end + end + + context 'when there are reserved usernames' do + before do + stub_reserved_usernames(%w(alice bob)) + end + + context 'when the username is reserved' do + it 'adds errors to the record' do + record.username = 'alice' + + expect(record).to_not be_valid + expect(record.errors.first.attribute).to eq(:username) + expect(record.errors.first.type).to eq(:reserved) + end + end + + context 'when the username is not reserved' do + it 'does not add errors to the record' do + record.username = 'chris' + + expect(record).to be_valid + expect(record.errors).to be_empty + end + end + end - it 'not calls errors.add' do - expect(errors).to_not have_received(:add).with(:username, any_args) + def stub_reserved_usernames(value) + allow(Setting).to receive(:[]).with('reserved_usernames').and_return(value) end end end