diff --git a/Gemfile b/Gemfile index a6ce6f27fa..cb02c03a74 100644 --- a/Gemfile +++ b/Gemfile @@ -106,9 +106,6 @@ 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 db57150d94..e2ad62f15a 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.4.0) + puma (6.3.1) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) @@ -602,8 +602,6 @@ 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) @@ -638,11 +636,11 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.29.0) parser (>= 3.2.1.0) - rubocop-capybara (2.19.0) + rubocop-capybara (2.18.0) rubocop (~> 1.41) rubocop-factory_bot (2.23.1) rubocop (~> 1.33) - rubocop-performance (1.19.1) + rubocop-performance (1.19.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.20.2) @@ -889,7 +887,6 @@ 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 657bb8e18d..c2c82bdff7 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 4fb9522c16..ee51220df6 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -198,9 +198,7 @@ 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 c0757accea..0948e5d255 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -113,7 +113,6 @@ 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 ed73751cb5..567b10ade2 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -79,8 +79,6 @@ class SearchQueryTransformer < Parslet::Transform 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 55a8c835fa..f82f4b91d0 100644 --- a/app/validators/unreserved_username_validator.rb +++ b/app/validators/unreserved_username_validator.rb @@ -11,31 +11,16 @@ class UnreservedUsernameValidator < ActiveModel::Validator private - def reserved_username? - pam_username_reserved? || settings_username_reserved? - end - - def pam_username_reserved? - pam_controlled? && pam_reserves_username? - end - def pam_controlled? - Devise.pam_authentication && Devise.pam_controlled_service + return false unless Devise.pam_authentication && Devise.pam_controlled_service + + Rpam2.account(Devise.pam_controlled_service, @username).present? end - 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 f1ef5ed3b1..a12fb26caf 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -33,6 +33,76 @@ ], "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 f7639d9867..1a180b0890 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_hidden_proxy=http://privoxy:8118 + ## http_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 329f171672..d17b253681 100644 --- a/lib/mastodon/cli/domains.rb +++ b/lib/mastodon/cli/domains.rb @@ -125,7 +125,7 @@ module Mastodon::CLI failed = Concurrent::AtomicFixnum.new(0) start_at = Time.now.to_f seed = start ? [start] : Instance.pluck(:domain) - blocked_domains = /\.?(#{Regexp.union(domain_block_suspended_domains).source})$/ + blocked_domains = /\.?(#{DomainBlock.where(severity: 1).pluck(:domain).map { |domain| Regexp.escape(domain) }.join('|')})$/ progress = create_progress_bar pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0) @@ -189,10 +189,6 @@ module Mastodon::CLI 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 cc9e3198b6..d46fdb108d 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -7,319 +7,449 @@ RSpec.describe AccountsController do let(:account) { Fabricate(:account) } - shared_examples 'unapproved account check' do - before { account.user.update(approved: false) } - - 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 - - it 'returns http gone' do - get :show, params: { username: account.username, format: format } - - expect(response).to have_http_status(410) - end - end - - shared_examples 'temporarily suspended account check' do |code: 403| - before { account.suspend! } - - it 'returns appropriate http response code' do - get :show, params: { username: account.username, format: format } - - expect(response).to have_http_status(code) - end - end - describe 'GET #show' do - context 'with basic account status checks' do - context 'with HTML' do - let(:format) { 'html' } + let(:format) { 'html' } - it_behaves_like 'unapproved account check' - it_behaves_like 'permanently suspended account check' - it_behaves_like 'temporarily suspended account check' - end + 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)) } - context 'with JSON' do - let(:format) { 'json' } + before do + 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 'unapproved account check' - it_behaves_like 'permanently suspended account check' - it_behaves_like 'temporarily suspended account check', code: 200 - end + shared_examples 'preliminary checks' do + context 'when account is not approved' do + before do + account.user.update(approved: false) + end - context 'with RSS' do - let(:format) { 'rss' } - - it_behaves_like 'unapproved account check' - it_behaves_like 'permanently suspended account check' - it_behaves_like 'temporarily suspended account check' + it 'returns http not found' do + get :show, params: { username: account.username, format: format } + expect(response).to have_http_status(404) + end end end - 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)) } + context 'with HTML' do + let(:format) { 'html' } - before do - 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 'with HTML' do - let(:format) { 'html' } - - shared_examples 'common HTML response' do - it 'returns a standard HTML response', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account) - - expect(response).to render_template(:show) - end + context 'when account is permanently suspended' do + before do + account.suspend! + account.deletion_request.destroy end - context 'with a normal account in an HTML request' do - before do - get :show, params: { username: account.username, format: format } - end - - it_behaves_like 'common HTML response' - 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 HTML response' - 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 HTML response' - 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 HTML response' + it 'returns http gone' do + get :show, params: { username: account.username, format: format } + expect(response).to have_http_status(410) end end - context 'with JSON' do - let(:authorized_fetch_mode) { false } - let(:format) { 'json' } + 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 + + shared_examples 'common response characteristics' do + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account) + end + + it 'renders show template' do + expect(response).to render_template(:show) + end + end + + context 'with a normal account in an HTML request' do + before do + get :show, params: { username: account.username, format: format } + end + + it_behaves_like 'common response characteristics' + 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' + 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' + end + + context 'with tag' do + let(:tag) { Fabricate(:tag) } + + let!(:status_tag) { Fabricate(:status, account: account) } before do - allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode) + 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 a normal account in a JSON request' do - before do - get :show, params: { username: account.username, format: format } - end + it_behaves_like 'common response characteristics' + end + end - it 'returns a JSON version of the account', :aggregate_failures do - expect(response).to have_http_status(200) + context 'with JSON' do + let(:authorized_fetch_mode) { false } + let(:format) { 'json' } - expect(response.media_type).to eq 'application/activity+json' + before do + allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode) + end - expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end + it_behaves_like 'preliminary checks' - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - - context 'with authorized fetch mode' do - let(:authorized_fetch_mode) { true } - - it 'returns http unauthorized' do - expect(response).to have_http_status(401) - end - end + context 'when account is suspended permanently' do + before do + account.suspend! + account.deletion_request.destroy end - context 'when signed in' do - let(:user) { Fabricate(:user) } + it 'returns http gone' do + get :show, params: { username: account.username, format: format } + expect(response).to have_http_status(410) + end + end - before do - sign_in(user) - get :show, params: { username: account.username, format: format } - end - - it 'returns a private JSON version of the account', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(response.media_type).to eq 'application/activity+json' - - expect(response.headers['Cache-Control']).to include 'private' - - expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end + context 'when account is suspended temporarily' do + before do + account.suspend! end - context 'with signature' do - let(:remote_account) { Fabricate(:account, domain: 'example.com') } + it 'returns http success' do + get :show, params: { username: account.username, format: format } + expect(response).to have_http_status(200) + end + end - before do - allow(controller).to receive(:signed_request_actor).and_return(remote_account) - get :show, params: { username: account.username, format: format } - end + context 'with a normal account in a JSON request' do + before do + get :show, params: { username: account.username, format: format } + end - it 'returns a JSON version of the account', :aggregate_failures do - expect(response).to have_http_status(200) + it 'returns http success' do + expect(response).to have_http_status(200) + end - expect(response.media_type).to eq 'application/activity+json' + it 'returns application/activity+json' do + expect(response.media_type).to eq 'application/activity+json' + end - expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - 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 authorized fetch mode' do + let(:authorized_fetch_mode) { true } - it 'returns a private signature JSON version of the account', :aggregate_failures do - expect(response).to have_http_status(200) - - expect(response.media_type).to eq 'application/activity+json' - - expect(response.headers['Cache-Control']).to include 'private' - - expect(response.headers['Vary']).to include 'Signature' - - expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) - end + it 'returns http unauthorized' do + expect(response).to have_http_status(401) end end end - context 'with RSS' do - let(:format) { 'rss' } + context 'when signed in' do + let(:user) { Fabricate(:user) } + + before do + sign_in(user) + 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' + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'private' + end + + it 'renders account' do + json = body_as_json + expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) + end + end + + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com') } + + 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' + 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 } - shared_examples 'common RSS response' do it 'returns http success' do expect(response).to have_http_status(200) end - it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' - end - - context 'with a normal account in an RSS request' do - before do - get :show, params: { username: account.username, format: format } + it 'returns application/activity+json' do + expect(response.media_type).to eq 'application/activity+json' end - it_behaves_like 'common RSS response' - - 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 - - context 'with replies' do - before do - allow(controller).to receive(:replies_requested?).and_return(true) - get :show, params: { username: account.username, format: format } + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'private' end - it_behaves_like 'common RSS response' - - 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 - - context 'with media' do - before do - allow(controller).to receive(:media_requested?).and_return(true) - get :show, params: { username: account.username, format: format } + it 'returns Vary header with Signature' do + expect(response.headers['Vary']).to include 'Signature' end - it_behaves_like 'common RSS response' - - 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 - - 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 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) + it 'renders account' do + json = body_as_json + expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary) end end end end - end - def include_status_tag(status) - include ActivityPub::TagManager.instance.url_for(status) + context 'with RSS' do + let(:format) { 'rss' } + + 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 + + shared_examples 'common response characteristics' do + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie' + end + + context 'with a normal account in an RSS request' do + before do + get :show, params: { username: account.username, format: format } + end + + it_behaves_like 'common response characteristics' + + it 'renders public status' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) + end + + it 'renders self-reply' do + expect(response.body).to 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 + + 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 'does not render reply to someone else' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + end + 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 'renders public status' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) + end + + it 'renders self-reply' do + expect(response.body).to 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 + + 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)) + 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' + + 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 + + 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 'does not render reply to someone else' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + 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 + + it_behaves_like 'common response characteristics' + + 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 'does not render status with media' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media)) + end + + 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 'does not render reply to someone else' do + expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) + end + + it 'renders status with tag' do + expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag)) + end + end + end end end diff --git a/spec/features/admin/accounts_spec.rb b/spec/features/admin/accounts_spec.rb index ad9c51485a..6d7bab1844 100644 --- a/spec/features/admin/accounts_spec.rb +++ b/spec/features/admin/accounts_spec.rb @@ -22,7 +22,7 @@ describe 'Admin::Accounts' do context 'without selecting any accounts' do it 'displays a notice about account selection' do - click_button button_for_suspend + click_on button_for_suspend expect(page).to have_content(selection_error_text) end @@ -32,7 +32,7 @@ describe 'Admin::Accounts' do it 'suspends the account' do batch_checkbox_for(approved_user_account).check - click_button button_for_suspend + click_on button_for_suspend expect(approved_user_account.reload).to be_suspended end @@ -42,7 +42,7 @@ describe 'Admin::Accounts' do it 'approves the account user' do batch_checkbox_for(unapproved_user_account).check - click_button button_for_approve + click_on button_for_approve expect(unapproved_user_account.reload.user).to be_approved end @@ -52,7 +52,7 @@ describe 'Admin::Accounts' do it 'rejects and removes the account' do batch_checkbox_for(unapproved_user_account).check - click_button button_for_reject + click_on 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 3fea8f06fe..8a8b6efcd1 100644 --- a/spec/features/admin/custom_emojis_spec.rb +++ b/spec/features/admin/custom_emojis_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::CustomEmojis' do context 'without selecting any records' do it 'displays a notice about selection' do - click_button button_for_enable + click_on 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 0d7b90c21c..4672c1e1a9 100644 --- a/spec/features/admin/domain_blocks_spec.rb +++ b/spec/features/admin/domain_blocks_spec.rb @@ -13,7 +13,7 @@ describe 'blocking domains through the moderation interface' do fill_in 'domain_block_domain', with: 'example.com' select I18n.t('admin.domain_blocks.new.severity.silence'), from: 'domain_block_severity' - click_button I18n.t('admin.domain_blocks.new.create') + click_on I18n.t('admin.domain_blocks.new.create') expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true end @@ -25,13 +25,13 @@ describe 'blocking domains through the moderation interface' do fill_in 'domain_block_domain', with: 'example.com' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_button I18n.t('admin.domain_blocks.new.create') + click_on 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_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true end @@ -45,13 +45,13 @@ describe 'blocking domains through the moderation interface' do fill_in 'domain_block_domain', with: 'example.com' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_button I18n.t('admin.domain_blocks.new.create') + click_on 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_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(domain_block.reload.severity).to eq 'suspend' end @@ -65,13 +65,13 @@ describe 'blocking domains through the moderation interface' do fill_in 'domain_block_domain', with: 'subdomain.example.com' select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_button I18n.t('admin.domain_blocks.new.create') + click_on 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_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') expect(DomainBlock.where(domain: 'subdomain.example.com', severity: 'suspend')).to exist @@ -88,13 +88,13 @@ describe 'blocking domains through the moderation interface' do visit edit_admin_domain_block_path(domain_block) select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' - click_button I18n.t('generic.save_changes') + click_on 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_button I18n.t('admin.domain_blocks.confirm_suspension.confirm') + click_on 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 80efe72e95..14959cbe74 100644 --- a/spec/features/admin/email_domain_blocks_spec.rb +++ b/spec/features/admin/email_domain_blocks_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::EmailDomainBlocks' do context 'without selecting any records' do it 'displays a notice about selection' do - click_button button_for_delete + click_on 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 465c889190..c9b16f6f78 100644 --- a/spec/features/admin/ip_blocks_spec.rb +++ b/spec/features/admin/ip_blocks_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::IpBlocks' do context 'without selecting any records' do it 'displays a notice about selection' do - click_button button_for_delete + click_on 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 a2373d35a6..4a635d1a79 100644 --- a/spec/features/admin/software_updates_spec.rb +++ b/spec/features/admin/software_updates_spec.rb @@ -11,13 +11,13 @@ describe 'finding software updates through the admin interface' do it 'shows a link to the software updates page, which links to release notes' do visit settings_profile_path - click_link I18n.t('admin.critical_update_pending') + click_on 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_link I18n.t('admin.software_updates.release_notes') + click_on 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 a21c901a92..531d0de953 100644 --- a/spec/features/admin/statuses_spec.rb +++ b/spec/features/admin/statuses_spec.rb @@ -17,7 +17,7 @@ describe 'Admin::Statuses' do context 'without selecting any records' do it 'displays a notice about selection' do - click_button button_for_report + click_on 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 cf9796abf3..dca89117b1 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 @@ describe 'Admin::Trends::Links::PreviewCardProviders' do context 'without selecting any records' do it 'displays a notice about selection' do - click_button button_for_allow + click_on 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 8b1b991a5a..99638bc069 100644 --- a/spec/features/admin/trends/links_spec.rb +++ b/spec/features/admin/trends/links_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::Trends::Links' do context 'without selecting any records' do it 'displays a notice about selection' do - click_button button_for_allow + click_on 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 a578ab0559..779a15d38f 100644 --- a/spec/features/admin/trends/statuses_spec.rb +++ b/spec/features/admin/trends/statuses_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::Trends::Statuses' do context 'without selecting any records' do it 'displays a notice about selection' do - click_button button_for_allow + click_on 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 7502bc8c6f..52e49c3a5d 100644 --- a/spec/features/admin/trends/tags_spec.rb +++ b/spec/features/admin/trends/tags_spec.rb @@ -16,7 +16,7 @@ describe 'Admin::Trends::Tags' do context 'without selecting any records' do it 'displays a notice about selection' do - click_button button_for_allow + click_on 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 6ccf066208..db89ff3e61 100644 --- a/spec/features/captcha_spec.rb +++ b/spec/features/captcha_spec.rb @@ -27,7 +27,7 @@ describe 'email confirmation flow when captcha is enabled' do expect(user.reload.confirmed?).to be false # It redirects to app and confirms user - click_button I18n.t('challenge.confirm') + click_on 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 7e5196aba9..c64e19d2b7 100644 --- a/spec/features/log_in_spec.rb +++ b/spec/features/log_in_spec.rb @@ -19,7 +19,7 @@ describe 'Log in' do 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_button I18n.t('auth.login') + click_on I18n.t('auth.login') expect(subject).to have_css('div.app-holder') end @@ -27,7 +27,7 @@ describe 'Log in' do 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_button I18n.t('auth.login') + click_on I18n.t('auth.login') expect(subject).to have_css('.flash-message', text: failure_message('invalid')) end @@ -38,7 +38,7 @@ describe 'Log in' do it 'A unconfirmed user is able to log in' do fill_in 'user_email', with: email fill_in 'user_password', with: password - click_button I18n.t('auth.login') + click_on 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 0e612b56a5..967956cc8e 100644 --- a/spec/features/oauth_spec.rb +++ b/spec/features/oauth_spec.rb @@ -20,7 +20,7 @@ describe 'Using OAuth from an external app' do expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) # Upon authorizing, it redirects to the apps' callback URL - click_button I18n.t('doorkeeper.authorizations.buttons.authorize') + click_on 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 @@ describe 'Using OAuth from an external app' do expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.deny')) # Upon denying, it redirects to the apps' callback URL - click_button I18n.t('doorkeeper.authorizations.buttons.deny') + click_on 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 @@ describe 'Using OAuth from an external app' do # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_button I18n.t('auth.login') + click_on 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_button I18n.t('auth.login') + click_on 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_button I18n.t('doorkeeper.authorizations.buttons.authorize') + click_on 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 @@ describe 'Using OAuth from an external app' do # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_button I18n.t('auth.login') + click_on 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_button I18n.t('auth.login') + click_on 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_button I18n.t('doorkeeper.authorizations.buttons.deny') + click_on 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 @@ describe 'Using OAuth from an external app' do # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_button I18n.t('auth.login') + click_on 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_button I18n.t('auth.login') + click_on 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_button I18n.t('auth.login') + click_on 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_button I18n.t('auth.login') + click_on 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_button I18n.t('doorkeeper.authorizations.buttons.authorize') + click_on 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 @@ describe 'Using OAuth from an external app' do # Failing to log-in presents the form again fill_in 'user_email', with: email fill_in 'user_password', with: 'wrong password' - click_button I18n.t('auth.login') + click_on 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_button I18n.t('auth.login') + click_on 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_button I18n.t('auth.login') + click_on 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_button I18n.t('auth.login') + click_on 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_button I18n.t('doorkeeper.authorizations.buttons.deny') + click_on 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 5ecea5ea16..a263d673de 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -6,24 +6,6 @@ require 'mastodon/cli/accounts' 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 @@ -569,15 +551,20 @@ describe Mastodon::CLI::Accounts do 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 @@ -585,7 +572,7 @@ describe Mastodon::CLI::Accounts do it 'displays a successful message' do expect { cli.follow(target_account.username) }.to output( - a_string_including("OK, followed target from #{Account.local.count} accounts") + a_string_including('OK, followed target from 3 accounts') ).to_stdout end end @@ -605,21 +592,26 @@ describe Mastodon::CLI::Accounts do context 'when the given username is found' do let!(:target_account) { Fabricate(:account) } - 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!(:follower_chris) { Fabricate(:account, username: 'chris') } + let!(:follower_rambo) { Fabricate(:account, username: 'rambo') } + let!(:follower_ana) { Fabricate(:account, username: 'ana') } 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| account.follow!(target_account) } + 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]) 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 @@ -679,8 +671,6 @@ describe Mastodon::CLI::Accounts do 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]) @@ -1122,19 +1112,26 @@ describe Mastodon::CLI::Accounts do 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', 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) } + 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') } 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 - stub_parallelize_with_progress! + 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_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) @@ -1143,6 +1140,7 @@ describe Mastodon::CLI::Accounts do 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 @@ -1150,27 +1148,35 @@ describe Mastodon::CLI::Accounts do 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 - expect { cli.cull }.to(change { tales.reload.updated_at }) + allow(tales).to receive(:touch) + + cli.cull + + expect(tales).to have_received(:touch).once end it 'displays the summary correctly' do expect { cli.cull }.to output( - a_string_including('Visited 5 accounts, removed 2') + a_string_including('Visited 5 accounts, removed 3') ).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 - stub_parallelize_with_progress! + allow(cli).to receive(:parallelize_with_progress).and_yield(gon) + .and_yield(tales) + .and_return([2, 2]) 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 @@ -1178,12 +1184,13 @@ describe Mastodon::CLI::Accounts do 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(domain) }.to output( + expect { cli.cull }.to output( a_string_including('Visited 2 accounts, removed 2') ).to_stdout end @@ -1192,9 +1199,7 @@ describe Mastodon::CLI::Accounts do context 'when a domain is unavailable' do shared_examples 'an unavailable domain' do before do - 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) + allow(cli).to receive(:parallelize_with_progress).and_yield(tales).and_return([1, 0]) end it 'skips accounts from the unavailable domain' do @@ -1205,7 +1210,7 @@ describe Mastodon::CLI::Accounts do it 'displays the summary correctly' do expect { cli.cull }.to output( - a_string_including("Visited 5 accounts, removed 0\nThe following domains were not available during the check:\n example.net") + a_string_including("Visited 1 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 1e064ed58e..b4b018b3be 100644 --- a/spec/lib/mastodon/cli/preview_cards_spec.rb +++ b/spec/lib/mastodon/cli/preview_cards_spec.rb @@ -4,52 +4,9 @@ require 'rails_helper' 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 5aa5548cc8..8ae04ca41f 100644 --- a/spec/models/poll_spec.rb +++ b/spec/models/poll_spec.rb @@ -29,23 +29,4 @@ describe Poll do 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 95bed8f59b..85a43f5d00 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -42,12 +42,6 @@ RSpec.configure do |config| # 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 82667ca080..2b345ddef1 100644 --- a/spec/support/stories/profile_stories.rb +++ b/spec/support/stories/profile_stories.rb @@ -18,7 +18,7 @@ module ProfileStories visit new_user_session_path fill_in 'user_email', with: email fill_in 'user_password', with: password - click_button I18n.t('auth.login') + click_on 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 244101f4d4..6faed6c808 100644 --- a/spec/system/new_statuses_spec.rb +++ b/spec/system/new_statuses_spec.rb @@ -24,10 +24,10 @@ describe 'NewStatuses' do within('.compose-form') do fill_in "What's on your mind?", with: status_text - click_button 'Publish!' + click_on 'Publish!' end - expect(subject).to have_css('.status__content__text', text: status_text) + expect(subject).to have_selector('.status__content__text', text: status_text) end it 'can be posted again' do @@ -37,9 +37,9 @@ describe 'NewStatuses' do within('.compose-form') do fill_in "What's on your mind?", with: status_text - click_button 'Publish!' + click_on 'Publish!' end - expect(subject).to have_css('.status__content__text', text: status_text) + expect(subject).to have_selector('.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 0eb5f83683..6f353eeafd 100644 --- a/spec/validators/unreserved_username_validator_spec.rb +++ b/spec/validators/unreserved_username_validator_spec.rb @@ -2,118 +2,41 @@ require 'rails_helper' -describe UnreservedUsernameValidator do - let(:record_class) do - Class.new do - include ActiveModel::Validations - attr_accessor :username - - validates_with UnreservedUsernameValidator - end - end - let(:record) { record_class.new } - +RSpec.describe UnreservedUsernameValidator, type: :validator do describe '#validate' do - context 'when username is nil' do - it 'does not add errors' do - record.username = nil + before do + allow(validator).to receive(:reserved_username?) { reserved_username } + validator.validate(account) + end - expect(record).to be_valid - expect(record.errors).to be_empty + let(:validator) { described_class.new } + let(:account) { instance_double(Account, username: username, errors: errors) } + let(:errors) { instance_double(ActiveModel::Errors, add: nil) } + + context 'when @username is blank?' do + let(:username) { nil } + + it 'not calls errors.add' do + expect(errors).to_not have_received(:add).with(:username, any_args) end end - context 'when PAM is enabled' do - before do - allow(Devise).to receive(:pam_authentication).and_return(true) - end + context 'when @username is not blank?' do + let(:username) { 'f' } - 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 + context 'with reserved_username?' do + let(:reserved_username) { true } - 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 - - it 'does not add errors to the record' do - record.username = 'username' - - expect(record).to be_valid - expect(record.errors).to be_empty - end + it 'calls errors.add' do + expect(errors).to have_received(:add).with(:username, :reserved) end end - context 'without a pam service' do - before do - allow(Devise).to receive(:pam_controlled_service).and_return(false) - end + context 'when username is not reserved' do + let(:reserved_username) { false } - 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 - - def stub_reserved_usernames(value) - allow(Setting).to receive(:[]).with('reserved_usernames').and_return(value) + it 'not calls errors.add' do + expect(errors).to_not have_received(:add).with(:username, any_args) end end end