Merge commit '5ef26d8fd50081c642b858a82bf0c5431b1c7e83' into glitch-soc/merge-upstream

local
Claire 7 months ago
commit 63179f0bf4
  1. 1
      .github/workflows/build-nightly.yml
  2. 1
      .github/workflows/crowdin-download.yml
  3. 3
      Gemfile
  4. 9
      Gemfile.lock
  5. 2
      app/javascript/mastodon/features/compose/components/search.jsx
  6. 2
      app/lib/search_query_transformer.rb
  7. 24
      config/brakeman.ignore
  8. 2
      docker-compose.yml
  9. 6
      lib/mastodon/cli/domains.rb
  10. 8
      spec/features/admin/accounts_spec.rb
  11. 2
      spec/features/admin/custom_emojis_spec.rb
  12. 18
      spec/features/admin/domain_blocks_spec.rb
  13. 2
      spec/features/admin/email_domain_blocks_spec.rb
  14. 2
      spec/features/admin/ip_blocks_spec.rb
  15. 4
      spec/features/admin/software_updates_spec.rb
  16. 2
      spec/features/admin/statuses_spec.rb
  17. 2
      spec/features/admin/trends/links/preview_card_providers_spec.rb
  18. 2
      spec/features/admin/trends/links_spec.rb
  19. 2
      spec/features/admin/trends/statuses_spec.rb
  20. 2
      spec/features/admin/trends/tags_spec.rb
  21. 2
      spec/features/captcha_spec.rb
  22. 6
      spec/features/log_in_spec.rb
  23. 36
      spec/features/oauth_spec.rb
  24. 87
      spec/lib/mastodon/cli/accounts_spec.rb
  25. 6
      spec/spec_helper.rb
  26. 2
      spec/support/stories/profile_stories.rb
  27. 8
      spec/system/new_statuses_spec.rb

@ -11,6 +11,7 @@ permissions:
jobs: jobs:
compute-suffix: compute-suffix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps: steps:
- id: version_vars - id: version_vars
env: env:

@ -11,6 +11,7 @@ permissions:
jobs: jobs:
download-translations: download-translations:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps: steps:
- name: Checkout - name: Checkout

@ -106,6 +106,9 @@ group :test do
# Used to split testing into chunks in CI # Used to split testing into chunks in CI
gem 'rspec_chunked', '~> 0.6' 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 # RSpec progress bar formatter
gem 'fuubar', '~> 2.5' gem 'fuubar', '~> 2.5'

@ -513,7 +513,7 @@ GEM
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0) private_address_check (0.5.0)
public_suffix (5.0.3) public_suffix (5.0.3)
puma (6.3.1) puma (6.4.0)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -602,6 +602,8 @@ GEM
rspec-expectations (3.12.3) rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-github (2.4.0)
rspec-core (~> 3.0)
rspec-mocks (3.12.5) rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
@ -634,11 +636,11 @@ GEM
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0) rubocop-ast (1.29.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
rubocop-capybara (2.18.0) rubocop-capybara (2.19.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.23.1) rubocop-factory_bot (2.23.1)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-performance (1.19.0) rubocop-performance (1.19.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.20.2) rubocop-rails (2.20.2)
@ -885,6 +887,7 @@ DEPENDENCIES
redis (~> 4.5) redis (~> 4.5)
redis-namespace (~> 1.10) redis-namespace (~> 1.10)
rqrcode (~> 2.2) rqrcode (~> 2.2)
rspec-github (~> 2.4)
rspec-rails (~> 6.0) rspec-rails (~> 6.0)
rspec-sidekiq (~> 4.0) rspec-sidekiq (~> 4.0)
rspec_chunked (~> 0.6) rspec_chunked (~> 0.6)

@ -64,7 +64,7 @@ class Search extends PureComponent {
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } }, { label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } }, { label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } }, { label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } } { label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } }
]; ];
setRef = c => { setRef = c => {

@ -58,6 +58,8 @@ class SearchQueryTransformer < Parslet::Transform
case @flags['in'] case @flags['in']
when 'library' when 'library'
[StatusesIndex] [StatusesIndex]
when 'public'
[PublicStatusesIndex]
else else
[PublicStatusesIndex, StatusesIndex] [PublicStatusesIndex, StatusesIndex]
end end

@ -33,30 +33,6 @@
], ],
"note": "" "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_type": "Cross-Site Scripting",
"warning_code": 4, "warning_code": 4,

@ -111,7 +111,7 @@ services:
test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"] test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"]
## Uncomment to enable federation with tor instances along with adding the following ENV variables ## 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 ## ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
# tor: # tor:
# image: sirboops/tor # image: sirboops/tor

@ -125,7 +125,7 @@ module Mastodon::CLI
failed = Concurrent::AtomicFixnum.new(0) failed = Concurrent::AtomicFixnum.new(0)
start_at = Time.now.to_f start_at = Time.now.to_f
seed = start ? [start] : Instance.pluck(:domain) 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 progress = create_progress_bar
pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0) 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 private
def domain_block_suspended_domains
DomainBlock.suspend.pluck(:domain)
end
def stats_to_summary(stats, processed, failed, start_at) def stats_to_summary(stats, processed, failed, start_at)
stats.compact! stats.compact!

@ -22,7 +22,7 @@ describe 'Admin::Accounts' do
context 'without selecting any accounts' do context 'without selecting any accounts' do
it 'displays a notice about account selection' 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) expect(page).to have_content(selection_error_text)
end end
@ -32,7 +32,7 @@ describe 'Admin::Accounts' do
it 'suspends the account' do it 'suspends the account' do
batch_checkbox_for(approved_user_account).check 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 expect(approved_user_account.reload).to be_suspended
end end
@ -42,7 +42,7 @@ describe 'Admin::Accounts' do
it 'approves the account user' do it 'approves the account user' do
batch_checkbox_for(unapproved_user_account).check 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 expect(unapproved_user_account.reload.user).to be_approved
end end
@ -52,7 +52,7 @@ describe 'Admin::Accounts' do
it 'rejects and removes the account' do it 'rejects and removes the account' do
batch_checkbox_for(unapproved_user_account).check 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) expect { unapproved_user_account.reload }.to raise_error(ActiveRecord::RecordNotFound)
end end

@ -16,7 +16,7 @@ describe 'Admin::CustomEmojis' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' 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) expect(page).to have_content(selection_error_text)
end end

@ -13,7 +13,7 @@ describe 'blocking domains through the moderation interface' do
fill_in 'domain_block_domain', with: 'example.com' fill_in 'domain_block_domain', with: 'example.com'
select I18n.t('admin.domain_blocks.new.severity.silence'), from: 'domain_block_severity' 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 expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true
end end
@ -25,13 +25,13 @@ describe 'blocking domains through the moderation interface' do
fill_in 'domain_block_domain', with: 'example.com' fill_in 'domain_block_domain', with: 'example.com'
select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' 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 # It presents a confirmation screen
expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com'))
# Confirming creates a block # 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 expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true
end end
@ -45,13 +45,13 @@ describe 'blocking domains through the moderation interface' do
fill_in 'domain_block_domain', with: 'example.com' fill_in 'domain_block_domain', with: 'example.com'
select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' 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 # It presents a confirmation screen
expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com'))
# Confirming updates the block # 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' expect(domain_block.reload.severity).to eq 'suspend'
end end
@ -65,13 +65,13 @@ describe 'blocking domains through the moderation interface' do
fill_in 'domain_block_domain', with: 'subdomain.example.com' fill_in 'domain_block_domain', with: 'subdomain.example.com'
select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' 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 # It presents a confirmation screen
expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'subdomain.example.com')) expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'subdomain.example.com'))
# Confirming creates the block # 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 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) visit edit_admin_domain_block_path(domain_block)
select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' 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 # It presents a confirmation screen
expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com'))
# Confirming updates the block # 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' expect(domain_block.reload.severity).to eq 'suspend'
end end

@ -16,7 +16,7 @@ describe 'Admin::EmailDomainBlocks' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' 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) expect(page).to have_content(selection_error_text)
end end

@ -16,7 +16,7 @@ describe 'Admin::IpBlocks' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' 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) expect(page).to have_content(selection_error_text)
end end

@ -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 it 'shows a link to the software updates page, which links to release notes' do
visit settings_profile_path 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_title(I18n.t('admin.software_updates.title'))
expect(page).to have_content('99.99.99') 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) expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true)
end end
end end

@ -17,7 +17,7 @@ describe 'Admin::Statuses' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' 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) expect(page).to have_content(selection_error_text)
end end

@ -16,7 +16,7 @@ describe 'Admin::Trends::Links::PreviewCardProviders' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' 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) expect(page).to have_content(selection_error_text)
end end

@ -16,7 +16,7 @@ describe 'Admin::Trends::Links' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' 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) expect(page).to have_content(selection_error_text)
end end

@ -16,7 +16,7 @@ describe 'Admin::Trends::Statuses' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' 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) expect(page).to have_content(selection_error_text)
end end

@ -16,7 +16,7 @@ describe 'Admin::Trends::Tags' do
context 'without selecting any records' do context 'without selecting any records' do
it 'displays a notice about selection' 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) expect(page).to have_content(selection_error_text)
end end

@ -27,7 +27,7 @@ describe 'email confirmation flow when captcha is enabled' do
expect(user.reload.confirmed?).to be false expect(user.reload.confirmed?).to be false
# It redirects to app and confirms user # 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(user.reload.confirmed?).to be true
expect(page).to have_current_path(/\A#{client_app.confirmation_redirect_uri}/, url: true) expect(page).to have_current_path(/\A#{client_app.confirmation_redirect_uri}/, url: true)
end end

@ -19,7 +19,7 @@ describe 'Log in' do
it 'A valid email and password user is able to 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_email', with: email
fill_in 'user_password', with: password 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') expect(subject).to have_css('div.app-holder')
end end
@ -27,7 +27,7 @@ describe 'Log in' do
it 'A invalid email and password user is not able to 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_email', with: 'invalid_email'
fill_in 'user_password', with: 'invalid_password' 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')) expect(subject).to have_css('.flash-message', text: failure_message('invalid'))
end end
@ -38,7 +38,7 @@ describe 'Log in' do
it 'A unconfirmed user is able to log in' do it 'A unconfirmed user is able to log in' do
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password 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') expect(subject).to have_css('div.admin-wrapper')
end end

@ -20,7 +20,7 @@ describe 'Using OAuth from an external app' do
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
# Upon authorizing, it redirects to the apps' callback URL # 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) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It grants the app access to the account # 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')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.deny'))
# Upon denying, it redirects to the apps' callback URL # 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) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It does not grant the app access to the account # 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 # Failing to log-in presents the form again
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: 'wrong password' 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')) expect(page).to have_content(I18n.t('auth.login'))
# Logging in redirects to an authorization page # Logging in redirects to an authorization page
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password 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')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
# Upon authorizing, it redirects to the apps' callback URL # 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) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It grants the app access to the account # 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 # Failing to log-in presents the form again
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: 'wrong password' 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')) expect(page).to have_content(I18n.t('auth.login'))
# Logging in redirects to an authorization page # Logging in redirects to an authorization page
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password 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')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
# Upon denying, it redirects to the apps' callback URL # 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) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It does not grant the app access to the account # 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 # Failing to log-in presents the form again
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: 'wrong password' 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')) expect(page).to have_content(I18n.t('auth.login'))
# Logging in redirects to a two-factor authentication page # Logging in redirects to a two-factor authentication page
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password 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')) expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
# Filling in an incorrect two-factor authentication code presents the form again # Filling in an incorrect two-factor authentication code presents the form again
fill_in 'user_otp_attempt', with: 'wrong' 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')) expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
# Filling in the correct TOTP code redirects to an app authorization page # Filling in the correct TOTP code redirects to an app authorization page
fill_in 'user_otp_attempt', with: user.current_otp 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')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
# Upon authorizing, it redirects to the apps' callback URL # 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) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It grants the app access to the account # 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 # Failing to log-in presents the form again
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: 'wrong password' 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')) expect(page).to have_content(I18n.t('auth.login'))
# Logging in redirects to a two-factor authentication page # Logging in redirects to a two-factor authentication page
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password 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')) expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
# Filling in an incorrect two-factor authentication code presents the form again # Filling in an incorrect two-factor authentication code presents the form again
fill_in 'user_otp_attempt', with: 'wrong' 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')) expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
# Filling in the correct TOTP code redirects to an app authorization page # Filling in the correct TOTP code redirects to an app authorization page
fill_in 'user_otp_attempt', with: user.current_otp 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')) expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
# Upon denying, it redirects to the apps' callback URL # 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) expect(page).to have_current_path(/\A#{client_app.redirect_uri}/, url: true)
# It does not grant the app access to the account # It does not grant the app access to the account

@ -6,6 +6,24 @@ require 'mastodon/cli/accounts'
describe Mastodon::CLI::Accounts do describe Mastodon::CLI::Accounts do
let(:cli) { described_class.new } 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 describe '.exit_on_failure?' do
it 'returns true' do it 'returns true' do
expect(described_class.exit_on_failure?).to be true 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_rony) { Fabricate(:account, username: 'rony') }
let!(:follower_charles) { Fabricate(:account, username: 'charles') } let!(:follower_charles) { Fabricate(:account, username: 'charles') }
let(:follow_service) { instance_double(FollowService, call: nil) } let(:follow_service) { instance_double(FollowService, call: nil) }
let(:scope) { Account.local.without_suspended }
before do 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) allow(FollowService).to receive(:new).and_return(follow_service)
stub_parallelize_with_progress!
end end
it 'makes all local accounts follow the target account' do it 'makes all local accounts follow the target account' do
cli.follow(target_account.username) 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_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_rony, target_account, any_args).once
expect(follow_service).to have_received(:call).with(follower_charles, 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 it 'displays a successful message' do
expect { cli.follow(target_account.username) }.to output( 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 ).to_stdout
end end
end end
@ -592,26 +605,21 @@ describe Mastodon::CLI::Accounts do
context 'when the given username is found' do context 'when the given username is found' do
let!(:target_account) { Fabricate(:account) } let!(:target_account) { Fabricate(:account) }
let!(:follower_chris) { Fabricate(:account, username: 'chris') } let!(:follower_chris) { Fabricate(:account, username: 'chris', domain: nil) }
let!(:follower_rambo) { Fabricate(:account, username: 'rambo') } let!(:follower_rambo) { Fabricate(:account, username: 'rambo', domain: nil) }
let!(:follower_ana) { Fabricate(:account, username: 'ana') } let!(:follower_ana) { Fabricate(:account, username: 'ana', domain: nil) }
let(:unfollow_service) { instance_double(UnfollowService, call: nil) } let(:unfollow_service) { instance_double(UnfollowService, call: nil) }
let(:scope) { target_account.followers.local }
before do before do
accounts = [follower_chris, follower_rambo, follower_ana] accounts = [follower_chris, follower_rambo, follower_ana]
accounts.each { |account| target_account.follow!(account) } accounts.each { |account| account.follow!(target_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) allow(UnfollowService).to receive(:new).and_return(unfollow_service)
stub_parallelize_with_progress!
end end
it 'makes all local accounts unfollow the target account' do it 'makes all local accounts unfollow the target account' do
cli.unfollow(target_account.username) 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_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_rambo, target_account).once
expect(unfollow_service).to have_received(:call).with(follower_ana, 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 } let(:scope) { Account.remote }
before do 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) allow(cli).to receive(:parallelize_with_progress).and_yield(remote_account_example_com)
.and_yield(account_example_net) .and_yield(account_example_net)
.and_return([2, nil]) .and_return([2, nil])
@ -1112,26 +1122,19 @@ describe Mastodon::CLI::Accounts do
describe '#cull' do describe '#cull' do
let(:delete_account_service) { instance_double(DeleteAccountService, call: nil) } 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!(: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') } 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') } 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') } 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') } 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 before do
allow(DeleteAccountService).to receive(:new).and_return(delete_account_service) allow(DeleteAccountService).to receive(:new).and_return(delete_account_service)
end end
context 'when no domain is specified' do context 'when no domain is specified' do
let(:scope) { Account.remote.where(protocol: :activitypub).partitioned }
before do before do
allow(cli).to receive(:parallelize_with_progress).and_yield(tom) stub_parallelize_with_progress!
.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.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/gon').to_return(status: 410)
stub_request(:head, 'https://example.net/users/tales').to_return(status: 200) 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 it 'deletes all inactive remote accounts that longer exist in the origin server' do
cli.cull 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(bob, reserve_username: false).once
expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once
end 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 it 'does not delete any active remote account that still exists in the origin server' do
cli.cull 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(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(ana, reserve_username: false)
expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false) expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false)
end end
it 'touches inactive remote accounts that have not been deleted' do it 'touches inactive remote accounts that have not been deleted' do
allow(tales).to receive(:touch) expect { cli.cull }.to(change { tales.reload.updated_at })
cli.cull
expect(tales).to have_received(:touch).once
end end
it 'displays the summary correctly' do it 'displays the summary correctly' do
expect { cli.cull }.to output( expect { cli.cull }.to output(
a_string_including('Visited 5 accounts, removed 3') a_string_including('Visited 5 accounts, removed 2')
).to_stdout ).to_stdout
end end
end end
context 'when a domain is specified' do context 'when a domain is specified' do
let(:domain) { 'example.net' } let(:domain) { 'example.net' }
let(:scope) { Account.remote.where(protocol: :activitypub, domain: domain).partitioned }
before do before do
allow(cli).to receive(:parallelize_with_progress).and_yield(gon) stub_parallelize_with_progress!
.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/gon').to_return(status: 410)
stub_request(:head, 'https://example.net/users/tales').to_return(status: 404) stub_request(:head, 'https://example.net/users/tales').to_return(status: 404)
end end
@ -1184,13 +1178,12 @@ describe Mastodon::CLI::Accounts do
it 'deletes inactive remote accounts that longer exist in the specified domain' do it 'deletes inactive remote accounts that longer exist in the specified domain' do
cli.cull(domain) 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(gon, reserve_username: false).once
expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once
end end
it 'displays the summary correctly' do 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') a_string_including('Visited 2 accounts, removed 2')
).to_stdout ).to_stdout
end end
@ -1199,7 +1192,9 @@ describe Mastodon::CLI::Accounts do
context 'when a domain is unavailable' do context 'when a domain is unavailable' do
shared_examples 'an unavailable domain' do shared_examples 'an unavailable domain' do
before 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 end
it 'skips accounts from the unavailable domain' do it 'skips accounts from the unavailable domain' do
@ -1210,7 +1205,7 @@ describe Mastodon::CLI::Accounts do
it 'displays the summary correctly' do it 'displays the summary correctly' do
expect { cli.cull }.to output( 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 ).to_stdout
end end
end end

@ -36,6 +36,12 @@ RSpec.configure do |config|
config.after :suite do config.after :suite do
FileUtils.rm_rf(Dir[Rails.root.join('spec', 'test_files')]) FileUtils.rm_rf(Dir[Rails.root.join('spec', 'test_files')])
end end
# Use the GitHub Annotations formatter for CI
if ENV['GITHUB_ACTIONS'] == 'true'
require 'rspec/github'
config.add_formatter RSpec::Github::Formatter
end
end end
def body_as_json def body_as_json

@ -18,7 +18,7 @@ module ProfileStories
visit new_user_session_path visit new_user_session_path
fill_in 'user_email', with: email fill_in 'user_email', with: email
fill_in 'user_password', with: password fill_in 'user_password', with: password
click_on I18n.t('auth.login') click_button I18n.t('auth.login')
end end
def with_alice_as_local_user def with_alice_as_local_user

@ -24,10 +24,10 @@ describe 'NewStatuses' do
within('.compose-form') do within('.compose-form') do
fill_in "What's on your mind?", with: status_text fill_in "What's on your mind?", with: status_text
click_on 'Publish!' click_button 'Publish!'
end 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
it 'can be posted again' do it 'can be posted again' do
@ -37,9 +37,9 @@ describe 'NewStatuses' do
within('.compose-form') do within('.compose-form') do
fill_in "What's on your mind?", with: status_text fill_in "What's on your mind?", with: status_text
click_on 'Publish!' click_button 'Publish!'
end 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
end end

Loading…
Cancel
Save