2023-05-24 09:55:40 +00:00
# frozen_string_literal: true
require 'rails_helper'
require 'mastodon/cli/accounts'
2024-09-04 05:12:25 +00:00
RSpec . describe Mastodon :: CLI :: Accounts do
2023-12-07 13:49:14 +00:00
subject { cli . invoke ( action , arguments , options ) }
2023-05-29 09:43:19 +00:00
let ( :cli ) { described_class . new }
2023-12-07 13:49:14 +00:00
let ( :arguments ) { [ ] }
let ( :options ) { { } }
2023-05-29 09:43:19 +00:00
2023-12-01 12:00:34 +00:00
it_behaves_like 'CLI Command'
2023-10-17 13:30:12 +00:00
# `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
2023-05-29 09:43:19 +00:00
describe '#create' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :create }
2023-05-29 09:43:19 +00:00
shared_examples 'a new user with given email address and username' do
2023-12-11 10:23:45 +00:00
it 'creates user and accounts from options and displays success message' do
allow ( SecureRandom ) . to receive ( :hex ) . and_return ( 'test_password' )
2023-05-29 09:43:19 +00:00
2023-12-11 10:23:45 +00:00
expect { subject }
. to output_results ( 'OK' , 'New password: test_password' )
expect ( user_from_options ) . to be_present
expect ( account_from_options ) . to be_present
2023-05-29 09:43:19 +00:00
end
2023-12-11 10:23:45 +00:00
def user_from_options
User . find_by ( email : options [ :email ] )
2023-05-29 09:43:19 +00:00
end
2023-12-11 10:23:45 +00:00
def account_from_options
Account . find_local ( 'tootctl_username' )
2023-05-29 09:43:19 +00:00
end
end
context 'when required USERNAME and --email are provided' do
let ( :arguments ) { [ 'tootctl_username' ] }
context 'with USERNAME and --email only' do
let ( :options ) { { email : 'tootctl@example.com' } }
it_behaves_like 'a new user with given email address and username'
context 'with invalid --email value' do
let ( :options ) { { email : 'invalid' } }
it 'exits with an error message' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , %r{ Failure/Error: email } )
2023-05-29 09:43:19 +00:00
end
end
end
context 'with --confirmed option' do
let ( :options ) { { email : 'tootctl@example.com' , confirmed : true } }
it_behaves_like 'a new user with given email address and username'
it 'creates a new user with confirmed status' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'New password' )
2023-05-29 09:43:19 +00:00
user = User . find_by ( email : options [ :email ] )
expect ( user . confirmed? ) . to be ( true )
end
end
context 'with --approve option' do
let ( :options ) { { email : 'tootctl@example.com' , approve : true } }
before do
Form :: AdminSettings . new ( registrations_mode : 'approved' ) . save
end
it_behaves_like 'a new user with given email address and username'
it 'creates a new user with approved status' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'New password' )
2023-05-29 09:43:19 +00:00
user = User . find_by ( email : options [ :email ] )
expect ( user . approved? ) . to be ( true )
end
end
context 'with --role option' do
context 'when role exists' do
let ( :default_role ) { Fabricate ( :user_role ) }
let ( :options ) { { email : 'tootctl@example.com' , role : default_role . name } }
it_behaves_like 'a new user with given email address and username'
it 'creates a new user and assigns the specified role' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'New password' )
2023-05-29 09:43:19 +00:00
role = User . find_by ( email : options [ :email ] ) & . role
expect ( role . name ) . to eq ( default_role . name )
end
end
context 'when role does not exist' do
let ( :options ) { { email : 'tootctl@example.com' , role : '404' } }
it 'exits with an error message indicating the role name was not found' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'Cannot find user role with that name' )
2023-05-29 09:43:19 +00:00
end
end
end
context 'with --reattach option' do
context " when account's user is present " do
let ( :options ) { { email : 'tootctl_new@example.com' , reattach : true } }
let ( :user ) { Fabricate . build ( :user , email : 'tootctl@example.com' ) }
before do
Fabricate ( :account , username : 'tootctl_username' , user : user )
end
it 'returns an error message indicating the username is already taken' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( " The chosen username is currently in use \n Use --force to reattach it anyway and delete the other user " )
2023-05-29 09:43:19 +00:00
end
context 'with --force option' do
let ( :options ) { { email : 'tootctl_new@example.com' , reattach : true , force : true } }
it 'reattaches the account to the new user and deletes the previous user' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'New password' )
2023-05-29 09:43:19 +00:00
user = Account . find_local ( 'tootctl_username' ) & . user
expect ( user . email ) . to eq ( options [ :email ] )
end
end
end
context " when account's user is not present " do
let ( :options ) { { email : 'tootctl@example.com' , reattach : true } }
before do
Fabricate ( :account , username : 'tootctl_username' , user : nil )
end
it_behaves_like 'a new user with given email address and username'
end
end
end
context 'when required --email option is not provided' do
let ( :arguments ) { [ 'tootctl_username' ] }
it 'raises a required argument missing error (Thor::RequiredArgumentMissingError)' do
2023-12-07 13:49:14 +00:00
expect { subject }
2023-05-29 09:43:19 +00:00
. to raise_error ( Thor :: RequiredArgumentMissingError )
end
end
end
2023-05-29 13:31:59 +00:00
describe '#modify' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :modify }
2023-05-29 13:31:59 +00:00
context 'when the given username is not found' do
let ( :arguments ) { [ 'non_existent_username' ] }
it 'exits with an error message indicating the user was not found' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No user with such username' )
2023-05-29 13:31:59 +00:00
end
end
context 'when the given username is found' do
let ( :user ) { Fabricate ( :user ) }
let ( :arguments ) { [ user . account . username ] }
context 'when no option is provided' do
2023-12-11 10:23:45 +00:00
it 'returns a successful message and preserves user' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-29 13:31:59 +00:00
expect ( user ) . to eq ( user . reload )
end
end
context 'with --role option' do
context 'when the given role is not found' do
let ( :options ) { { role : '404' } }
it 'exits with an error message indicating the role was not found' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'Cannot find user role with that name' )
2023-05-29 13:31:59 +00:00
end
end
context 'when the given role is found' do
let ( :default_role ) { Fabricate ( :user_role ) }
let ( :options ) { { role : default_role . name } }
it " updates the user's role to the specified role " do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-29 13:31:59 +00:00
role = user . reload . role
expect ( role . name ) . to eq ( default_role . name )
end
end
end
context 'with --remove-role option' do
let ( :options ) { { remove_role : true } }
let ( :role ) { Fabricate ( :user_role ) }
let ( :user ) { Fabricate ( :user , role : role ) }
it " removes the user's role successfully " do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-29 13:31:59 +00:00
role = user . reload . role
expect ( role . name ) . to be_empty
end
end
context 'with --email option' do
let ( :user ) { Fabricate ( :user , email : 'old_email@email.com' ) }
let ( :options ) { { email : 'new_email@email.com' } }
it " sets the user's unconfirmed email to the provided email address " do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-29 13:31:59 +00:00
expect ( user . reload . unconfirmed_email ) . to eq ( options [ :email ] )
end
it " does not update the user's original email address " do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-29 13:31:59 +00:00
expect ( user . reload . email ) . to eq ( 'old_email@email.com' )
end
context 'with --confirm option' do
let ( :user ) { Fabricate ( :user , email : 'old_email@email.com' , confirmed_at : nil ) }
let ( :options ) { { email : 'new_email@email.com' , confirm : true } }
it " updates the user's email address to the provided email " do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-29 13:31:59 +00:00
expect ( user . reload . email ) . to eq ( options [ :email ] )
end
it " sets the user's email address as confirmed " do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-29 13:31:59 +00:00
expect ( user . reload . confirmed? ) . to be ( true )
end
end
end
context 'with --confirm option' do
let ( :user ) { Fabricate ( :user , confirmed_at : nil ) }
let ( :options ) { { confirm : true } }
it " confirms the user's email address " do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-29 13:31:59 +00:00
expect ( user . reload . confirmed? ) . to be ( true )
end
end
context 'with --approve option' do
let ( :user ) { Fabricate ( :user , approved : false ) }
let ( :options ) { { approve : true } }
before do
Form :: AdminSettings . new ( registrations_mode : 'approved' ) . save
end
it 'approves the user' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
. and change { user . reload . approved } . from ( false ) . to ( true )
2023-05-29 13:31:59 +00:00
end
end
context 'with --disable option' do
let ( :user ) { Fabricate ( :user , disabled : false ) }
let ( :options ) { { disable : true } }
it 'disables the user' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
. and change { user . reload . disabled } . from ( false ) . to ( true )
2023-05-29 13:31:59 +00:00
end
end
context 'with --enable option' do
let ( :user ) { Fabricate ( :user , disabled : true ) }
let ( :options ) { { enable : true } }
it 'enables the user' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
. and change { user . reload . disabled } . from ( true ) . to ( false )
2023-05-29 13:31:59 +00:00
end
end
context 'with --reset-password option' do
let ( :options ) { { reset_password : true } }
it 'returns a new password for the user' do
allow ( SecureRandom ) . to receive ( :hex ) . and_return ( 'new_password' )
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( 'new_password' )
2023-05-29 13:31:59 +00:00
end
end
context 'with --disable-2fa option' do
let ( :user ) { Fabricate ( :user , otp_required_for_login : true ) }
let ( :options ) { { disable_2fa : true } }
it 'disables the two-factor authentication for the user' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
. and change { user . reload . otp_required_for_login } . from ( true ) . to ( false )
2023-05-29 13:31:59 +00:00
end
end
context 'when provided data is invalid' do
let ( :user ) { Fabricate ( :user ) }
let ( :options ) { { email : 'invalid' } }
it 'exits with an error message' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , %r{ Failure/Error: email } )
2023-05-29 13:31:59 +00:00
end
end
end
end
2023-05-30 09:59:17 +00:00
describe '#delete' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :delete }
2023-05-30 09:59:17 +00:00
let ( :account ) { Fabricate ( :account ) }
let ( :delete_account_service ) { instance_double ( DeleteAccountService ) }
before do
allow ( DeleteAccountService ) . to receive ( :new ) . and_return ( delete_account_service )
allow ( delete_account_service ) . to receive ( :call )
end
context 'when both username and --email are provided' do
2023-12-07 13:49:14 +00:00
let ( :arguments ) { [ account . username ] }
let ( :options ) { { email : account . user . email } }
2023-05-30 09:59:17 +00:00
it 'exits with an error message indicating that only one should be used' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'Use username or --email, not both' )
2023-05-30 09:59:17 +00:00
end
end
context 'when neither username nor --email are provided' do
it 'exits with an error message indicating that no username was provided' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No username provided' )
2023-05-30 09:59:17 +00:00
end
end
context 'when username is provided' do
2023-12-07 13:49:14 +00:00
let ( :arguments ) { [ account . username ] }
2023-05-30 09:59:17 +00:00
it 'deletes the specified user successfully' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'Deleting' )
2023-05-30 09:59:17 +00:00
expect ( delete_account_service ) . to have_received ( :call ) . with ( account , reserve_email : false ) . once
end
context 'with --dry-run option' do
let ( :options ) { { dry_run : true } }
2023-12-11 10:23:45 +00:00
it 'outputs a successful message in dry run mode and does not delete the user' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( 'OK (DRY RUN)' )
2023-12-11 10:23:45 +00:00
expect ( delete_account_service ) . to_not have_received ( :call ) . with ( account , reserve_email : false )
2023-05-30 09:59:17 +00:00
end
end
context 'when the given username is not found' do
let ( :arguments ) { [ 'non_existent_username' ] }
it 'exits with an error message indicating that no user was found' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No user with such username' )
2023-05-30 09:59:17 +00:00
end
end
end
context 'when --email is provided' do
2023-12-07 13:49:14 +00:00
let ( :options ) { { email : account . user . email } }
2023-05-30 09:59:17 +00:00
it 'deletes the specified user successfully' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'Deleting' )
2023-05-30 09:59:17 +00:00
expect ( delete_account_service ) . to have_received ( :call ) . with ( account , reserve_email : false ) . once
end
context 'with --dry-run option' do
let ( :options ) { { email : account . user . email , dry_run : true } }
2023-12-11 10:23:45 +00:00
it 'outputs a successful message in dry run mode and does not delete the user' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( 'OK (DRY RUN)' )
2023-12-11 10:23:45 +00:00
expect ( delete_account_service )
. to_not have_received ( :call )
. with ( account , reserve_email : false )
2023-05-30 09:59:17 +00:00
end
end
context 'when the given email address is not found' do
let ( :options ) { { email : '404@example.com' } }
it 'exits with an error message indicating that no user was found' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No user with such email' )
2023-05-30 09:59:17 +00:00
end
end
end
end
2023-05-30 13:00:21 +00:00
describe '#approve' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :approve }
2023-11-20 09:04:46 +00:00
let ( :total_users ) { 4 }
2023-05-30 13:00:21 +00:00
before do
Form :: AdminSettings . new ( registrations_mode : 'approved' ) . save
Fabricate . times ( total_users , :user )
end
context 'with --all option' do
2023-12-07 13:49:14 +00:00
let ( :options ) { { all : true } }
2023-05-30 13:00:21 +00:00
it 'approves all pending registrations' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-30 13:00:21 +00:00
expect ( User . pluck ( :approved ) . all? ( true ) ) . to be ( true )
end
end
context 'with --number option' do
context 'when the number is positive' do
2023-11-20 09:04:46 +00:00
let ( :options ) { { number : 2 } }
2023-05-30 13:00:21 +00:00
2023-12-11 10:23:45 +00:00
it 'approves the earliest n pending registrations but not the remaining ones' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-30 13:00:21 +00:00
expect ( n_earliest_pending_registrations . all? ( & :approved? ) ) . to be ( true )
2023-12-11 10:23:45 +00:00
expect ( pending_registrations . all? ( & :approved? ) ) . to be ( false )
2023-05-30 13:00:21 +00:00
end
2023-12-11 10:23:45 +00:00
def n_earliest_pending_registrations
User . order ( created_at : :asc ) . first ( options [ :number ] )
end
2023-05-30 13:00:21 +00:00
2023-12-11 10:23:45 +00:00
def pending_registrations
User . order ( created_at : :asc ) . last ( total_users - options [ :number ] )
2023-05-30 13:00:21 +00:00
end
end
context 'when the number is negative' do
2023-12-07 13:49:14 +00:00
let ( :options ) { { number : - 1 } }
2023-05-30 13:00:21 +00:00
it 'exits with an error message indicating that the number must be positive' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'Number must be positive' )
2023-05-30 13:00:21 +00:00
end
end
context 'when the given number is greater than the number of users' do
let ( :options ) { { number : total_users * 2 } }
2023-12-11 10:23:45 +00:00
it 'approves all users and does not raise any error' do
2023-12-07 13:49:14 +00:00
expect { subject }
2023-12-13 10:14:19 +00:00
. to output_results ( 'OK' )
2023-12-11 10:23:45 +00:00
expect ( User . pluck ( :approved ) . all? ( true ) ) . to be ( true )
2023-05-30 13:00:21 +00:00
end
end
end
context 'with username argument' do
context 'when the given username is found' do
let ( :user ) { User . last }
let ( :arguments ) { [ user . account . username ] }
it 'approves the specified user successfully' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-05-30 13:00:21 +00:00
expect ( user . reload . approved? ) . to be ( true )
end
end
context 'when the given username is not found' do
let ( :arguments ) { [ 'non_existent_username' ] }
it 'exits with an error message indicating that no such account was found' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No such account' )
2023-05-30 13:00:21 +00:00
end
end
end
end
2023-05-30 14:13:29 +00:00
describe '#follow' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :follow }
2023-05-30 14:13:29 +00:00
context 'when the given username is not found' do
let ( :arguments ) { [ 'non_existent_username' ] }
it 'exits with an error message indicating that no account with the given username was found' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No such account' )
2023-05-30 14:13:29 +00:00
end
end
context 'when the given username is found' do
let! ( :target_account ) { Fabricate ( :account ) }
let! ( :follower_bob ) { Fabricate ( :account , username : 'bob' ) }
let! ( :follower_rony ) { Fabricate ( :account , username : 'rony' ) }
let! ( :follower_charles ) { Fabricate ( :account , username : 'charles' ) }
let ( :follow_service ) { instance_double ( FollowService , call : nil ) }
2023-12-07 13:49:14 +00:00
let ( :arguments ) { [ target_account . username ] }
2023-05-30 14:13:29 +00:00
before do
allow ( FollowService ) . to receive ( :new ) . and_return ( follow_service )
2023-10-17 13:30:12 +00:00
stub_parallelize_with_progress!
2023-05-30 14:13:29 +00:00
end
2023-12-11 10:23:45 +00:00
it 'displays a successful message and makes all local accounts follow the target account' do
expect { subject }
. to output_results ( " OK, followed target from #{ Account . local . count } accounts " )
2023-05-30 14:13:29 +00:00
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
end
end
end
2023-05-31 07:33:57 +00:00
describe '#unfollow' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :unfollow }
2023-05-31 07:33:57 +00:00
context 'when the given username is not found' do
let ( :arguments ) { [ 'non_existent_username' ] }
it 'exits with an error message indicating that no account with the given username was found' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No such account' )
2023-05-31 07:33:57 +00:00
end
end
context 'when the given username is found' do
let! ( :target_account ) { Fabricate ( :account ) }
2023-10-17 13:30:12 +00:00
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 ) }
2023-05-31 07:33:57 +00:00
let ( :unfollow_service ) { instance_double ( UnfollowService , call : nil ) }
2023-12-07 13:49:14 +00:00
let ( :arguments ) { [ target_account . username ] }
2023-05-31 07:33:57 +00:00
before do
accounts = [ follower_chris , follower_rambo , follower_ana ]
2023-10-17 13:30:12 +00:00
accounts . each { | account | account . follow! ( target_account ) }
2023-05-31 07:33:57 +00:00
allow ( UnfollowService ) . to receive ( :new ) . and_return ( unfollow_service )
2023-10-17 13:30:12 +00:00
stub_parallelize_with_progress!
2023-05-31 07:33:57 +00:00
end
2023-12-11 10:23:45 +00:00
it 'displays a successful message and makes all local accounts unfollow the target account' do
expect { subject }
. to output_results ( 'OK, unfollowed target from 3 accounts' )
2023-05-31 07:33:57 +00:00
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
end
end
end
2023-05-31 13:32:37 +00:00
2024-09-03 15:36:59 +00:00
describe '#fix_duplicates' do
let ( :action ) { :fix_duplicates }
let ( :service_double ) { instance_double ( ActivityPub :: FetchRemoteAccountService , call : nil ) }
let ( :uri ) { 'https://host.example/same/value' }
context 'when there are duplicate URI accounts' do
before do
Fabricate . times ( 2 , :account , domain : 'host.example' , uri : uri )
allow ( ActivityPub :: FetchRemoteAccountService ) . to receive ( :new ) . and_return ( service_double )
end
it 'finds the duplicates and calls fetch remote account service' do
expect { subject }
. to output_results ( 'Duplicates found' )
expect ( service_double ) . to have_received ( :call ) . with ( uri )
end
end
end
2023-05-31 13:32:37 +00:00
describe '#backup' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :backup }
2023-05-31 13:32:37 +00:00
context 'when the given username is not found' do
let ( :arguments ) { [ 'non_existent_username' ] }
it 'exits with an error message indicating that there is no such account' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No user with such username' )
2023-05-31 13:32:37 +00:00
end
end
context 'when the given username is found' do
let ( :account ) { Fabricate ( :account ) }
let ( :user ) { account . user }
let ( :arguments ) { [ account . username ] }
2023-12-11 10:23:45 +00:00
before { allow ( BackupWorker ) . to receive ( :perform_async ) }
2023-05-31 13:32:37 +00:00
2023-12-11 10:23:45 +00:00
it 'creates a new backup and backup job for the specified user and outputs success message' do
expect { subject }
. to change { user . backups . count } . by ( 1 )
. and output_results ( 'OK' )
2023-05-31 13:32:37 +00:00
expect ( BackupWorker ) . to have_received ( :perform_async ) . with ( latest_backup . id ) . once
end
2023-12-11 10:23:45 +00:00
def latest_backup
user . backups . last
2023-05-31 13:32:37 +00:00
end
end
end
2023-06-01 08:01:09 +00:00
describe '#refresh' do
2024-01-17 13:38:37 +00:00
let ( :action ) { :refresh }
2023-06-01 08:01:09 +00:00
context 'with --all option' do
2024-01-17 13:38:37 +00:00
let ( :options ) { { all : true } }
let! ( :local_account ) { Fabricate ( :account , domain : nil ) }
let ( :remote_com_avatar_url ) { 'https://example.host/avatar/com' }
let ( :remote_com_header_url ) { 'https://example.host/header/com' }
let ( :remote_account_example_com ) { Fabricate ( :account , domain : 'example.com' , avatar_remote_url : remote_com_avatar_url , header_remote_url : remote_com_header_url ) }
let ( :remote_net_avatar_url ) { 'https://example.host/avatar/net' }
let ( :remote_net_header_url ) { 'https://example.host/header/net' }
let ( :account_example_net ) { Fabricate ( :account , domain : 'example.net' , avatar_remote_url : remote_net_avatar_url , header_remote_url : remote_net_header_url ) }
let ( :scope ) { Account . remote }
2023-06-01 08:01:09 +00:00
before do
2024-01-17 13:38:37 +00:00
stub_parallelize_with_progress!
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
stub_request ( :get , remote_com_avatar_url )
. to_return request_fixture ( 'avatar.txt' )
stub_request ( :get , remote_com_header_url )
. to_return request_fixture ( 'avatar.txt' )
stub_request ( :get , remote_net_avatar_url )
. to_return request_fixture ( 'avatar.txt' )
stub_request ( :get , remote_net_header_url )
. to_return request_fixture ( 'avatar.txt' )
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
remote_account_example_com
. update_column ( :avatar_file_name , nil )
account_example_net
. update_column ( :avatar_file_name , nil )
2023-06-01 08:01:09 +00:00
end
2024-01-17 13:38:37 +00:00
it 'refreshes the avatar and header for all remote accounts' do
expect { subject }
2023-12-13 10:14:19 +00:00
. to output_results ( 'Refreshed 2 accounts' )
2024-01-17 13:38:37 +00:00
. and not_change ( local_account , :updated_at )
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
# One request from factory creation, one more from task
expect ( a_request ( :get , remote_com_avatar_url ) )
. to have_been_made . at_least_times ( 2 )
expect ( a_request ( :get , remote_com_header_url ) )
. to have_been_made . at_least_times ( 2 )
expect ( a_request ( :get , remote_net_avatar_url ) )
. to have_been_made . at_least_times ( 2 )
expect ( a_request ( :get , remote_net_header_url ) )
. to have_been_made . at_least_times ( 2 )
2023-06-01 08:01:09 +00:00
end
context 'with --dry-run option' do
2024-01-17 13:38:37 +00:00
let ( :options ) { { all : true , dry_run : true } }
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
it 'does not refresh the avatar or header for any account' do
expect { subject }
2023-12-13 10:14:19 +00:00
. to output_results ( 'Refreshed 2 accounts' )
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
# One request from factory creation, none from task due to dry run
expect ( a_request ( :get , remote_com_avatar_url ) )
. to have_been_made . once
expect ( a_request ( :get , remote_com_header_url ) )
. to have_been_made . once
expect ( a_request ( :get , remote_net_avatar_url ) )
. to have_been_made . once
expect ( a_request ( :get , remote_net_header_url ) )
. to have_been_made . once
2023-06-01 08:01:09 +00:00
end
end
end
context 'with a list of accts' do
let! ( :account_example_com_a ) { Fabricate ( :account , domain : 'example.com' ) }
let! ( :account_example_com_b ) { Fabricate ( :account , domain : 'example.com' ) }
let! ( :account_example_net ) { Fabricate ( :account , domain : 'example.net' ) }
let ( :arguments ) { [ account_example_com_a . acct , account_example_com_b . acct ] }
before do
2023-11-20 10:46:02 +00:00
# NOTE: `Account.find_remote` is stubbed so that `Account#reset_avatar!`
# can be stubbed on the individual accounts.
2023-06-01 08:01:09 +00:00
allow ( Account ) . to receive ( :find_remote ) . with ( account_example_com_a . username , account_example_com_a . domain ) . and_return ( account_example_com_a )
allow ( Account ) . to receive ( :find_remote ) . with ( account_example_com_b . username , account_example_com_b . domain ) . and_return ( account_example_com_b )
allow ( Account ) . to receive ( :find_remote ) . with ( account_example_net . username , account_example_net . domain ) . and_return ( account_example_net )
end
it 'resets the avatar for the specified accounts' do
allow ( account_example_com_a ) . to receive ( :reset_avatar! )
allow ( account_example_com_b ) . to receive ( :reset_avatar! )
2024-01-17 13:38:37 +00:00
expect { subject }
2023-12-13 10:14:19 +00:00
. to output_results ( 'OK' )
2023-06-01 08:01:09 +00:00
expect ( account_example_com_a ) . to have_received ( :reset_avatar! ) . once
expect ( account_example_com_b ) . to have_received ( :reset_avatar! ) . once
end
it 'does not reset the avatar for unspecified accounts' do
allow ( account_example_net ) . to receive ( :reset_avatar! )
2024-01-17 13:38:37 +00:00
expect { subject }
2023-12-13 10:14:19 +00:00
. to output_results ( 'OK' )
2023-06-01 08:01:09 +00:00
expect ( account_example_net ) . to_not have_received ( :reset_avatar! )
end
it 'resets the header for the specified accounts' do
allow ( account_example_com_a ) . to receive ( :reset_header! )
allow ( account_example_com_b ) . to receive ( :reset_header! )
2024-01-17 13:38:37 +00:00
expect { subject }
2023-12-13 10:14:19 +00:00
. to output_results ( 'OK' )
2023-06-01 08:01:09 +00:00
expect ( account_example_com_a ) . to have_received ( :reset_header! ) . once
expect ( account_example_com_b ) . to have_received ( :reset_header! ) . once
end
it 'does not reset the header for unspecified accounts' do
allow ( account_example_net ) . to receive ( :reset_header! )
2024-01-17 13:38:37 +00:00
expect { subject }
2023-12-13 10:14:19 +00:00
. to output_results ( 'OK' )
2023-06-01 08:01:09 +00:00
expect ( account_example_net ) . to_not have_received ( :reset_header! )
end
context 'when an UnexpectedResponseError is raised' do
it 'displays a failure message' do
allow ( account_example_com_a ) . to receive ( :reset_avatar! ) . and_raise ( Mastodon :: UnexpectedResponseError )
2024-01-17 13:38:37 +00:00
expect { subject }
2023-12-07 13:49:14 +00:00
. to output_results ( " Account failed: #{ account_example_com_a . username } @ #{ account_example_com_a . domain } " )
2023-06-01 08:01:09 +00:00
end
end
context 'when a specified account is not found' do
it 'exits with an error message' do
allow ( Account ) . to receive ( :find_remote ) . with ( account_example_com_b . username , account_example_com_b . domain ) . and_return ( nil )
2024-01-17 13:38:37 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No such account' )
2023-06-01 08:01:09 +00:00
end
end
context 'with --dry-run option' do
2024-01-17 13:38:37 +00:00
let ( :options ) { { dry_run : true } }
2023-06-01 08:01:09 +00:00
it 'does not refresh the avatar for any account' do
allow ( account_example_com_a ) . to receive ( :reset_avatar! )
allow ( account_example_com_b ) . to receive ( :reset_avatar! )
2024-01-17 13:38:37 +00:00
expect { subject }
2023-12-13 10:14:19 +00:00
. to output_results ( 'OK (DRY RUN)' )
2023-06-01 08:01:09 +00:00
expect ( account_example_com_a ) . to_not have_received ( :reset_avatar! )
expect ( account_example_com_b ) . to_not have_received ( :reset_avatar! )
end
it 'does not refresh the header for any account' do
allow ( account_example_com_a ) . to receive ( :reset_header! )
allow ( account_example_com_b ) . to receive ( :reset_header! )
2024-01-17 13:38:37 +00:00
expect { subject }
2023-12-13 10:14:19 +00:00
. to output_results ( 'OK (DRY RUN)' )
2023-06-01 08:01:09 +00:00
expect ( account_example_com_a ) . to_not have_received ( :reset_header! )
expect ( account_example_com_b ) . to_not have_received ( :reset_header! )
end
end
end
context 'with --domain option' do
2024-01-17 13:38:37 +00:00
let ( :domain ) { 'example.com' }
let ( :options ) { { domain : domain } }
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
let ( :com_a_avatar_url ) { 'https://example.host/avatar/a' }
let ( :com_a_header_url ) { 'https://example.host/header/a' }
let ( :account_example_com_a ) { Fabricate ( :account , domain : domain , avatar_remote_url : com_a_avatar_url , header_remote_url : com_a_header_url ) }
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
let ( :com_b_avatar_url ) { 'https://example.host/avatar/b' }
let ( :com_b_header_url ) { 'https://example.host/header/b' }
let ( :account_example_com_b ) { Fabricate ( :account , domain : domain , avatar_remote_url : com_b_avatar_url , header_remote_url : com_b_header_url ) }
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
let ( :net_avatar_url ) { 'https://example.host/avatar/net' }
let ( :net_header_url ) { 'https://example.host/header/net' }
let ( :account_example_net ) { Fabricate ( :account , domain : 'example.net' , avatar_remote_url : net_avatar_url , header_remote_url : net_header_url ) }
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
before do
stub_parallelize_with_progress!
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
stub_request ( :get , com_a_avatar_url )
. to_return request_fixture ( 'avatar.txt' )
stub_request ( :get , com_a_header_url )
. to_return request_fixture ( 'avatar.txt' )
stub_request ( :get , com_b_avatar_url )
. to_return request_fixture ( 'avatar.txt' )
stub_request ( :get , com_b_header_url )
. to_return request_fixture ( 'avatar.txt' )
stub_request ( :get , net_avatar_url )
. to_return request_fixture ( 'avatar.txt' )
stub_request ( :get , net_header_url )
. to_return request_fixture ( 'avatar.txt' )
account_example_com_a
. update_column ( :avatar_file_name , nil )
account_example_com_b
. update_column ( :avatar_file_name , nil )
account_example_net
. update_column ( :avatar_file_name , nil )
end
it 'refreshes the avatar and header for all accounts on specified domain' do
expect { subject }
2023-12-13 10:14:19 +00:00
. to output_results ( 'Refreshed 2 accounts' )
2023-06-01 08:01:09 +00:00
2024-01-17 13:38:37 +00:00
# One request from factory creation, one more from task
expect ( a_request ( :get , com_a_avatar_url ) )
. to have_been_made . at_least_times ( 2 )
expect ( a_request ( :get , com_a_header_url ) )
. to have_been_made . at_least_times ( 2 )
expect ( a_request ( :get , com_b_avatar_url ) )
. to have_been_made . at_least_times ( 2 )
expect ( a_request ( :get , com_b_header_url ) )
. to have_been_made . at_least_times ( 2 )
# One request from factory creation, none from task
expect ( a_request ( :get , net_avatar_url ) )
. to have_been_made . once
expect ( a_request ( :get , net_header_url ) )
. to have_been_made . once
2023-06-01 08:01:09 +00:00
end
end
context 'when neither a list of accts nor options are provided' do
it 'exits with an error message' do
2024-01-17 13:38:37 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No account(s) given' )
2023-06-01 08:01:09 +00:00
end
end
end
2023-06-01 12:29:29 +00:00
describe '#rotate' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :rotate }
2023-06-01 12:29:29 +00:00
context 'when neither username nor --all option are given' do
it 'exits with an error message' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No account(s) given' )
2023-06-01 12:29:29 +00:00
end
end
context 'when a username is given' do
let ( :account ) { Fabricate ( :account ) }
2023-12-07 13:49:14 +00:00
let ( :arguments ) { [ account . username ] }
2023-06-01 12:29:29 +00:00
it 'correctly rotates keys for the specified account' do
old_private_key = account . private_key
old_public_key = account . public_key
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-06-01 12:29:29 +00:00
account . reload
expect ( account . private_key ) . to_not eq ( old_private_key )
expect ( account . public_key ) . to_not eq ( old_public_key )
end
it 'broadcasts the new keys for the specified account' do
allow ( ActivityPub :: UpdateDistributionWorker ) . to receive ( :perform_in )
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-06-01 12:29:29 +00:00
expect ( ActivityPub :: UpdateDistributionWorker ) . to have_received ( :perform_in ) . with ( anything , account . id , anything ) . once
end
2023-12-11 10:23:45 +00:00
end
2023-06-01 12:29:29 +00:00
2023-12-11 10:23:45 +00:00
context 'when the given username is not found' do
let ( :arguments ) { [ 'non_existent_username' ] }
2023-12-07 13:49:14 +00:00
2023-12-11 10:23:45 +00:00
it 'exits with an error message when the specified username is not found' do
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No such account' )
2023-06-01 12:29:29 +00:00
end
end
context 'when --all option is provided' do
2023-11-20 10:46:02 +00:00
let! ( :accounts ) { Fabricate . times ( 2 , :account ) }
2023-12-07 13:49:14 +00:00
let ( :options ) { { all : true } }
2023-06-01 12:29:29 +00:00
it 'correctly rotates keys for all local accounts' do
old_private_keys = accounts . map ( & :private_key )
old_public_keys = accounts . map ( & :public_key )
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'rotated' )
2023-06-01 12:29:29 +00:00
accounts . each ( & :reload )
expect ( accounts . map ( & :private_key ) ) . to_not eq ( old_private_keys )
expect ( accounts . map ( & :public_key ) ) . to_not eq ( old_public_keys )
end
it 'broadcasts the new keys for each account' do
allow ( ActivityPub :: UpdateDistributionWorker ) . to receive ( :perform_in )
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'rotated' )
2023-06-01 12:29:29 +00:00
accounts . each do | account |
expect ( ActivityPub :: UpdateDistributionWorker ) . to have_received ( :perform_in ) . with ( anything , account . id , anything ) . once
end
end
end
end
2023-06-05 06:16:12 +00:00
describe '#merge' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :merge }
2023-06-05 06:16:12 +00:00
shared_examples 'an account not found' do | acct |
it 'exits with an error message indicating that there is no such account' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , " No such account ( #{ acct } ) " )
2023-06-05 06:16:12 +00:00
end
end
context 'when "from_account" is not found' do
let ( :to_account ) { Fabricate ( :account , domain : 'example.com' ) }
let ( :arguments ) { [ 'non_existent_username@domain.com' , " #{ to_account . username } @ #{ to_account . domain } " ] }
it_behaves_like 'an account not found' , 'non_existent_username@domain.com'
end
context 'when "from_account" is a local account' do
let ( :from_account ) { Fabricate ( :account , domain : nil , username : 'bob' ) }
let ( :to_account ) { Fabricate ( :account , domain : 'example.com' ) }
let ( :arguments ) { [ from_account . username , " #{ to_account . username } @ #{ to_account . domain } " ] }
it_behaves_like 'an account not found' , 'bob'
end
context 'when "to_account" is not found' do
let ( :from_account ) { Fabricate ( :account , domain : 'example.com' ) }
let ( :arguments ) { [ " #{ from_account . username } @ #{ from_account . domain } " , 'non_existent_username' ] }
it_behaves_like 'an account not found' , 'non_existent_username'
end
context 'when "to_account" is local' do
let ( :from_account ) { Fabricate ( :account , domain : 'example.com' ) }
let ( :to_account ) { Fabricate ( :account , domain : nil , username : 'bob' ) }
let ( :arguments ) do
[ " #{ from_account . username } @ #{ from_account . domain } " , " #{ to_account . username } @ #{ to_account . domain } " ]
end
it_behaves_like 'an account not found' , 'bob@'
end
context 'when "from_account" and "to_account" public keys do not match' do
let ( :from_account ) { instance_double ( Account , username : 'bob' , domain : 'example1.com' , local? : false , public_key : 'from_account' ) }
let ( :to_account ) { instance_double ( Account , username : 'bob' , domain : 'example2.com' , local? : false , public_key : 'to_account' ) }
let ( :arguments ) do
[ " #{ from_account . username } @ #{ from_account . domain } " , " #{ to_account . username } @ #{ to_account . domain } " ]
end
before do
allow ( Account ) . to receive ( :find_remote ) . with ( from_account . username , from_account . domain ) . and_return ( from_account )
allow ( Account ) . to receive ( :find_remote ) . with ( to_account . username , to_account . domain ) . and_return ( to_account )
end
it 'exits with an error message indicating that the accounts do not have the same pub key' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , " Accounts don't have the same public key, might not be duplicates! \n Override with --force \n " )
2023-06-05 06:16:12 +00:00
end
context 'with --force option' do
let ( :options ) { { force : true } }
before do
allow ( to_account ) . to receive ( :merge_with! )
allow ( from_account ) . to receive ( :destroy )
end
2023-12-11 10:23:45 +00:00
it 'merges `from_account` into `to_account` and deletes `from_account`' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-06-05 06:16:12 +00:00
expect ( to_account ) . to have_received ( :merge_with! ) . with ( from_account ) . once
expect ( from_account ) . to have_received ( :destroy ) . once
end
end
end
context 'when "from_account" and "to_account" public keys match' do
let ( :from_account ) { instance_double ( Account , username : 'bob' , domain : 'example1.com' , local? : false , public_key : 'pub_key' ) }
let ( :to_account ) { instance_double ( Account , username : 'bob' , domain : 'example2.com' , local? : false , public_key : 'pub_key' ) }
let ( :arguments ) do
[ " #{ from_account . username } @ #{ from_account . domain } " , " #{ to_account . username } @ #{ to_account . domain } " ]
end
before do
allow ( Account ) . to receive ( :find_remote ) . with ( from_account . username , from_account . domain ) . and_return ( from_account )
allow ( Account ) . to receive ( :find_remote ) . with ( to_account . username , to_account . domain ) . and_return ( to_account )
allow ( to_account ) . to receive ( :merge_with! )
allow ( from_account ) . to receive ( :destroy )
end
2023-12-11 10:23:45 +00:00
it 'merges "from_account" into "to_account" and deletes from_account' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'OK' )
2023-06-05 06:16:12 +00:00
expect ( to_account ) . to have_received ( :merge_with! ) . with ( from_account ) . once
expect ( from_account ) . to have_received ( :destroy )
end
end
end
2023-06-05 12:51:25 +00:00
describe '#cull' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :cull }
2023-06-05 12:51:25 +00:00
let ( :delete_account_service ) { instance_double ( DeleteAccountService , call : nil ) }
2023-10-17 13:30:12 +00:00
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 ) }
2023-06-05 12:51:25 +00:00
before do
allow ( DeleteAccountService ) . to receive ( :new ) . and_return ( delete_account_service )
end
context 'when no domain is specified' do
before do
2023-10-17 13:30:12 +00:00
stub_parallelize_with_progress!
2023-06-05 12:51:25 +00:00
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 )
end
2023-12-11 10:23:45 +00:00
def expect_delete_inactive_remote_accounts
2023-06-05 12:51:25 +00:00
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
2023-12-11 10:23:45 +00:00
def expect_not_delete_active_accounts
2023-06-05 12:51:25 +00:00
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
2023-12-11 10:23:45 +00:00
it 'touches inactive remote accounts that have not been deleted and summarizes activity' do
2023-12-07 13:49:14 +00:00
expect { subject }
2023-12-11 10:23:45 +00:00
. to change { tales . reload . updated_at }
. and output_results ( 'Visited 5 accounts, removed 2' )
expect_delete_inactive_remote_accounts
expect_not_delete_active_accounts
2023-06-05 12:51:25 +00:00
end
end
context 'when a domain is specified' do
2023-12-07 13:49:14 +00:00
let ( :arguments ) { [ 'example.net' ] }
2023-06-05 12:51:25 +00:00
before do
2023-10-17 13:30:12 +00:00
stub_parallelize_with_progress!
2023-06-05 12:51:25 +00:00
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
2023-12-11 10:23:45 +00:00
def expect_delete_inactive_remote_accounts
2023-06-05 12:51:25 +00:00
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
2023-12-11 10:23:45 +00:00
it 'displays the summary correctly and deletes inactive remote accounts' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( 'Visited 2 accounts, removed 2' )
2023-12-11 10:23:45 +00:00
expect_delete_inactive_remote_accounts
2023-06-05 12:51:25 +00:00
end
end
context 'when a domain is unavailable' do
shared_examples 'an unavailable domain' do
before do
2023-10-17 13:30:12 +00:00
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 )
2023-06-05 12:51:25 +00:00
end
2023-12-11 10:23:45 +00:00
def expect_skip_accounts_from_unavailable_domain
2023-06-05 12:51:25 +00:00
expect ( delete_account_service ) . to_not have_received ( :call ) . with ( tales , reserve_username : false )
end
2023-12-11 10:23:45 +00:00
it 'displays the summary correctly and skip accounts from unavailable domains' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( " Visited 5 accounts, removed 0 \n The following domains were not available during the check: \n example.net " )
2023-12-11 10:23:45 +00:00
expect_skip_accounts_from_unavailable_domain
2023-06-05 12:51:25 +00:00
end
end
context 'when a connection timeout occurs' do
before do
stub_request ( :head , 'https://example.net/users/tales' ) . to_timeout
end
it_behaves_like 'an unavailable domain'
end
context 'when a connection error occurs' do
before do
stub_request ( :head , 'https://example.net/users/tales' ) . to_raise ( HTTP :: ConnectionError )
end
it_behaves_like 'an unavailable domain'
end
context 'when an ssl error occurs' do
before do
stub_request ( :head , 'https://example.net/users/tales' ) . to_raise ( OpenSSL :: SSL :: SSLError )
end
it_behaves_like 'an unavailable domain'
end
context 'when a private network address error occurs' do
before do
stub_request ( :head , 'https://example.net/users/tales' ) . to_raise ( Mastodon :: PrivateNetworkAddressError )
end
it_behaves_like 'an unavailable domain'
end
end
end
2023-06-06 11:37:09 +00:00
describe '#reset_relationships' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :reset_relationships }
2023-06-06 11:37:09 +00:00
let ( :target_account ) { Fabricate ( :account ) }
let ( :arguments ) { [ target_account . username ] }
context 'when no option is given' do
it 'exits with an error message indicating that at least one option is required' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'Please specify either --follows or --followers, or both' )
2023-06-06 11:37:09 +00:00
end
end
context 'when the given username is not found' do
let ( :arguments ) { [ 'non_existent_username' ] }
2023-12-07 13:49:14 +00:00
let ( :options ) { { follows : true } }
2023-06-06 11:37:09 +00:00
it 'exits with an error message indicating that there is no such account' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'No such account' )
2023-06-06 11:37:09 +00:00
end
end
context 'when the given username is found' do
2023-11-20 09:04:46 +00:00
let ( :total_relationships ) { 3 }
2023-06-06 11:37:09 +00:00
let! ( :accounts ) { Fabricate . times ( total_relationships , :account ) }
context 'with --follows option' do
let ( :options ) { { follows : true } }
before do
accounts . each { | account | target_account . follow! ( account ) }
allow ( BootstrapTimelineWorker ) . to receive ( :perform_async )
end
2023-12-11 10:23:45 +00:00
it 'resets following relationships and displays a successful message and rebuilds timeline' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( " Processed #{ total_relationships } relationships " )
2023-12-11 10:23:45 +00:00
expect ( target_account . reload . following ) . to be_empty
expect ( BootstrapTimelineWorker ) . to have_received ( :perform_async ) . with ( target_account . id ) . once
2023-06-06 11:37:09 +00:00
end
end
context 'with --followers option' do
let ( :options ) { { followers : true } }
before do
accounts . each { | account | account . follow! ( target_account ) }
end
2023-12-11 10:23:45 +00:00
it 'resets followers relationships and displays a successful message' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( " Processed #{ total_relationships } relationships " )
2023-12-11 10:23:45 +00:00
expect ( target_account . reload . followers ) . to be_empty
2023-06-06 11:37:09 +00:00
end
end
context 'with --follows and --followers options' do
let ( :options ) { { followers : true , follows : true } }
before do
2023-11-20 09:04:46 +00:00
accounts . first ( 2 ) . each { | account | account . follow! ( target_account ) }
accounts . last ( 1 ) . each { | account | target_account . follow! ( account ) }
2023-06-06 11:37:09 +00:00
allow ( BootstrapTimelineWorker ) . to receive ( :perform_async )
end
2023-12-11 10:23:45 +00:00
it 'resets followers and following and displays a successful message and rebuilds timeline' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( " Processed #{ total_relationships } relationships " )
2023-12-11 10:23:45 +00:00
expect ( target_account . reload . followers ) . to be_empty
expect ( target_account . reload . following ) . to be_empty
expect ( BootstrapTimelineWorker ) . to have_received ( :perform_async ) . with ( target_account . id ) . once
2023-06-06 11:37:09 +00:00
end
end
end
end
2023-10-18 08:20:50 +00:00
describe '#prune' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :prune }
2023-10-18 08:20:50 +00:00
let! ( :local_account ) { Fabricate ( :account ) }
let! ( :bot_account ) { Fabricate ( :account , bot : true , domain : 'example.com' ) }
let! ( :group_account ) { Fabricate ( :account , actor_type : 'Group' , domain : 'example.com' ) }
let! ( :mentioned_account ) { Fabricate ( :account , domain : 'example.com' ) }
let! ( :prunable_accounts ) do
2023-11-20 09:04:46 +00:00
Fabricate . times ( 2 , :account , domain : 'example.com' , bot : false , suspended_at : nil , silenced_at : nil )
2023-10-18 08:20:50 +00:00
end
before do
Fabricate ( :mention , account : mentioned_account , status : Fabricate ( :status , account : Fabricate ( :account ) ) )
stub_parallelize_with_progress!
end
2023-12-11 10:23:45 +00:00
def expect_prune_remote_accounts_without_interaction
2023-10-18 08:20:50 +00:00
prunable_account_ids = prunable_accounts . pluck ( :id )
expect ( Account . where ( id : prunable_account_ids ) . count ) . to eq ( 0 )
end
2023-12-11 10:23:45 +00:00
it 'displays a successful message and handles accounts correctly' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( " OK, pruned #{ prunable_accounts . size } accounts " )
2023-12-11 10:23:45 +00:00
expect_prune_remote_accounts_without_interaction
expect_not_prune_local_accounts
expect_not_prune_bot_accounts
expect_not_prune_group_accounts
expect_not_prune_mentioned_accounts
2023-10-18 08:20:50 +00:00
end
2023-12-11 10:23:45 +00:00
def expect_not_prune_local_accounts
2023-10-18 08:20:50 +00:00
expect ( Account . exists? ( id : local_account . id ) ) . to be ( true )
end
2023-12-11 10:23:45 +00:00
def expect_not_prune_bot_accounts
2023-10-18 08:20:50 +00:00
expect ( Account . exists? ( id : bot_account . id ) ) . to be ( true )
end
2023-12-11 10:23:45 +00:00
def expect_not_prune_group_accounts
2023-10-18 08:20:50 +00:00
expect ( Account . exists? ( id : group_account . id ) ) . to be ( true )
end
2023-12-11 10:23:45 +00:00
def expect_not_prune_mentioned_accounts
2023-10-18 08:20:50 +00:00
expect ( Account . exists? ( id : mentioned_account . id ) ) . to be true
end
context 'with --dry-run option' do
2023-12-07 13:49:14 +00:00
let ( :options ) { { dry_run : true } }
2023-10-18 08:20:50 +00:00
2023-12-11 10:23:45 +00:00
def expect_no_account_prunes
2023-10-18 08:20:50 +00:00
prunable_account_ids = prunable_accounts . pluck ( :id )
expect ( Account . where ( id : prunable_account_ids ) . count ) . to eq ( prunable_accounts . size )
end
2023-12-11 10:23:45 +00:00
it 'displays a successful message with (DRY RUN) and doesnt prune anything' do
2023-12-07 13:49:14 +00:00
expect { subject }
. to output_results ( " OK, pruned #{ prunable_accounts . size } accounts (DRY RUN) " )
2023-12-11 10:23:45 +00:00
expect_no_account_prunes
2023-10-18 08:20:50 +00:00
end
end
end
2023-10-19 14:11:15 +00:00
describe '#migrate' do
2023-12-07 13:49:14 +00:00
let ( :action ) { :migrate }
2023-10-19 14:11:15 +00:00
let! ( :source_account ) { Fabricate ( :account ) }
let! ( :target_account ) { Fabricate ( :account , domain : 'example.com' ) }
let ( :arguments ) { [ source_account . username ] }
let ( :resolve_account_service ) { instance_double ( ResolveAccountService , call : nil ) }
let ( :move_service ) { instance_double ( MoveService , call : nil ) }
before do
allow ( ResolveAccountService ) . to receive ( :new ) . and_return ( resolve_account_service )
allow ( MoveService ) . to receive ( :new ) . and_return ( move_service )
end
shared_examples 'a successful migration' do
2024-01-26 16:35:19 +00:00
it 'displays a success message and calls the MoveService for the last migration' do
2023-12-13 10:14:19 +00:00
expect { subject }
2024-01-26 16:35:19 +00:00
. to output_results ( " OK, migrated #{ source_account . acct } to #{ target_account . acct } " )
2023-10-19 14:11:15 +00:00
2024-01-26 16:35:19 +00:00
expect ( move_service )
. to have_received ( :call ) . with ( last_migration ) . once
2023-10-19 14:11:15 +00:00
end
2024-01-26 16:35:19 +00:00
def last_migration
source_account . migrations . last
2023-10-19 14:11:15 +00:00
end
end
context 'when both --replay and --target options are given' do
let ( :options ) { { replay : true , target : " #{ target_account . username } @example.com " } }
it 'exits with an error message indicating that using both options is not possible' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'Use --replay or --target, not both' )
2023-10-19 14:11:15 +00:00
end
end
context 'when no option is given' do
it 'exits with an error message indicating that at least one option must be used' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'Use either --replay or --target' )
2023-10-19 14:11:15 +00:00
end
end
context 'when the given username is not found' do
let ( :arguments ) { [ 'non_existent_username' ] }
2023-12-07 13:49:14 +00:00
let ( :options ) { { replay : true } }
2023-10-19 14:11:15 +00:00
it 'exits with an error message indicating that there is no such account' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , " No such account: #{ arguments . first } " )
2023-10-19 14:11:15 +00:00
end
end
context 'with --replay option' do
let ( :options ) { { replay : true } }
context 'when the specified account has no previous migrations' do
it 'exits with an error message indicating that the given account has no previous migrations' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'The specified account has not performed any migration' )
2023-10-19 14:11:15 +00:00
end
end
context 'when the specified account has a previous migration' do
before do
allow ( resolve_account_service ) . to receive ( :call ) . with ( source_account . acct , any_args ) . and_return ( source_account )
allow ( resolve_account_service ) . to receive ( :call ) . with ( target_account . acct , any_args ) . and_return ( target_account )
target_account . aliases . create! ( acct : source_account . acct )
source_account . migrations . create! ( acct : target_account . acct )
source_account . update! ( moved_to_account : target_account )
end
it_behaves_like 'a successful migration'
context 'when the specified account is redirecting to a different target account' do
before do
source_account . update! ( moved_to_account : nil )
end
it 'exits with an error message' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway' )
2023-10-19 14:11:15 +00:00
end
end
context 'with --force option' do
let ( :options ) { { replay : true , force : true } }
it_behaves_like 'a successful migration'
end
end
end
context 'with --target option' do
let ( :options ) { { target : target_account . acct } }
before do
allow ( resolve_account_service ) . to receive ( :call ) . with ( source_account . acct , any_args ) . and_return ( source_account )
allow ( resolve_account_service ) . to receive ( :call ) . with ( target_account . acct , any_args ) . and_return ( target_account )
end
context 'when the specified target account is not found' do
before do
allow ( resolve_account_service ) . to receive ( :call ) . with ( target_account . acct ) . and_return ( nil )
end
it 'exits with an error message indicating that there is no such account' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , " The specified target account could not be found: #{ options [ :target ] } " )
2023-10-19 14:11:15 +00:00
end
end
context 'when the specified target account exists' do
before do
target_account . aliases . create! ( acct : source_account . acct )
end
it 'creates a migration for the specified account with the target account' do
2023-12-13 10:14:19 +00:00
expect { subject }
. to output_results ( 'migrated' )
2023-10-19 14:11:15 +00:00
last_migration = source_account . migrations . last
expect ( last_migration . acct ) . to eq ( target_account . acct )
end
it_behaves_like 'a successful migration'
end
context 'when the migration record is invalid' do
it 'exits with an error indicating that the validation failed' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , / Error: Validation failed / )
2023-10-19 14:11:15 +00:00
end
end
context 'when the specified account is redirecting to a different target account' do
before do
2023-11-20 10:46:02 +00:00
source_account . update ( moved_to_account : Fabricate ( :account ) )
2023-10-19 14:11:15 +00:00
end
it 'exits with an error message' do
2023-12-07 13:49:14 +00:00
expect { subject }
2024-01-26 08:53:44 +00:00
. to raise_error ( Thor :: Error , 'The specified account is redirecting to a different target account. Use --force if you want to change the migration target' )
2023-10-19 14:11:15 +00:00
end
end
context 'with --target and --force options' do
let ( :options ) { { target : target_account . acct , force : true } }
before do
2023-11-20 10:46:02 +00:00
source_account . update ( moved_to_account : Fabricate ( :account ) )
2023-10-19 14:11:15 +00:00
target_account . aliases . create! ( acct : source_account . acct )
end
it_behaves_like 'a successful migration'
end
end
end
2023-05-24 09:55:40 +00:00
end