diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml index d2c3eea19..8adfcda84 100644 --- a/.github/workflows/build-nightly.yml +++ b/.github/workflows/build-nightly.yml @@ -11,6 +11,7 @@ permissions: jobs: compute-suffix: runs-on: ubuntu-latest + if: github.repository == 'mastodon/mastodon' steps: - id: version_vars env: diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml index dc6fd874d..dbd19a8d0 100644 --- a/.github/workflows/crowdin-download.yml +++ b/.github/workflows/crowdin-download.yml @@ -11,6 +11,7 @@ permissions: jobs: download-translations: runs-on: ubuntu-latest + if: github.repository == 'mastodon/mastodon' steps: - name: Checkout diff --git a/Gemfile b/Gemfile index 449b0a920..6cfdcfa5c 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 ce21bc0a2..29cdc8aa4 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) @@ -634,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.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) @@ -885,6 +887,7 @@ DEPENDENCIES redis (~> 4.5) redis-namespace (~> 1.10) rqrcode (~> 2.2) + rspec-github (~> 2.4) rspec-rails (~> 6.0) rspec-sidekiq (~> 4.0) rspec_chunked (~> 0.6) diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx index dfbfa98f5..2525df779 100644 --- a/app/javascript/mastodon/features/compose/components/search.jsx +++ b/app/javascript/mastodon/features/compose/components/search.jsx @@ -64,7 +64,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:'); } } ]; setRef = c => { diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb index a45ae3d09..927495eac 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -58,6 +58,8 @@ class SearchQueryTransformer < Parslet::Transform case @flags['in'] when 'library' [StatusesIndex] + when 'public' + [PublicStatusesIndex] else [PublicStatusesIndex, StatusesIndex] end diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 02ce23a07..9f85ccb6a 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -33,30 +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": "Cross-Site Scripting", "warning_code": 4, diff --git a/docker-compose.yml b/docker-compose.yml index d19f278f7..bcfa4c85f 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 d17b25368..329f17167 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 = /\.?(#{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 @@ 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/features/admin/accounts_spec.rb b/spec/features/admin/accounts_spec.rb index 6d7bab184..ad9c51485 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_on button_for_suspend + click_button 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_on button_for_suspend + click_button 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_on button_for_approve + click_button 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_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 8a8b6efcd..3fea8f06f 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_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 4672c1e1a..0d7b90c21 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_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 @@ 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_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 @@ 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_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 @@ 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_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 @@ 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_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 14959cbe7..80efe72e9 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_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 c9b16f6f7..465c88919 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_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 4a635d1a7..a2373d35a 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_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 531d0de95..a21c901a9 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_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 dca89117b..cf9796abf 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_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 99638bc06..8b1b991a5 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_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 779a15d38..a578ab055 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_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 52e49c3a5..7502bc8c6 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_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 db89ff3e6..6ccf06620 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_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 c64e19d2b..7e5196aba 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_on I18n.t('auth.login') + click_button 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_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 @@ 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_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 967956cc8..0e612b56a 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_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 @@ 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_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 @@ 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_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 @@ 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_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 @@ 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_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 @@ 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_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 a263d673d..5ecea5ea1 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -6,6 +6,24 @@ 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 @@ -551,20 +569,15 @@ 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 @@ -572,7 +585,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 3 accounts') + a_string_including("OK, followed target from #{Account.local.count} accounts") ).to_stdout end end @@ -592,26 +605,21 @@ 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') } - 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 @@ 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]) @@ -1112,26 +1122,19 @@ 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') } - 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 @@ 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 @@ -1148,35 +1150,27 @@ 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 - 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 @@ 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 }.to output( + expect { cli.cull(domain) }.to output( a_string_including('Visited 2 accounts, removed 2') ).to_stdout end @@ -1199,7 +1192,9 @@ describe Mastodon::CLI::Accounts do 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 @@ describe Mastodon::CLI::Accounts do 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/spec_helper.rb b/spec/spec_helper.rb index b4c20545f..4d3c234a0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -36,6 +36,12 @@ RSpec.configure do |config| config.after :suite do FileUtils.rm_rf(Dir[Rails.root.join('spec', 'test_files')]) end + + # 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 2b345ddef..82667ca08 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_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 6faed6c80..244101f4d 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_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 @@ describe 'NewStatuses' do 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