# frozen_string_literal: true require 'rails_helper' require 'mastodon/cli/maintenance' describe Mastodon::CLI::Maintenance do subject { cli.invoke(action, arguments, options) } let(:cli) { described_class.new } let(:arguments) { [] } let(:options) { {} } it_behaves_like 'CLI Command' describe '#fix_duplicates' do let(:action) { :fix_duplicates } context 'when the database version is too old' do before do allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2000_01_01_000000) # Earlier than minimum end it 'Exits with error message' do expect { subject } .to raise_error(Thor::Error, /is too old/) end end context 'when the database version is too new and the user does not continue' do before do allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2100_01_01_000000) # Later than maximum allow(cli.shell).to receive(:yes?).with('Continue anyway? (Yes/No)').and_return(false).once end it 'Exits with error message' do expect { subject } .to output_results('more recent') .and raise_error(Thor::Error, /more recent/) end end context 'when Sidekiq is running' do before do allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2022_01_01_000000) # Higher than minimum, lower than maximum allow(Sidekiq::ProcessSet).to receive(:new).and_return [:process] end it 'Exits with error message' do expect { subject } .to raise_error(Thor::Error, /Sidekiq is running/) end end context 'when requirements are met' do before do allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2023_08_22_081029) # The latest migration before the cutoff agree_to_backup_warning end context 'with duplicate accounts' do before do prepare_duplicate_data choose_local_account_to_keep end let(:duplicate_account_username) { 'username' } let(:duplicate_account_domain) { 'host.example' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating accounts', 'Multiple local accounts were found for', 'Restoring index_accounts_on_username_and_domain_lower', 'Reindexing textual indexes on accounts…', 'Finished!' ) .and change(duplicate_remote_accounts, :count).from(2).to(1) .and change(duplicate_local_accounts, :count).from(2).to(1) end def duplicate_remote_accounts Account.where(username: duplicate_account_username, domain: duplicate_account_domain) end def duplicate_local_accounts Account.where(username: duplicate_account_username, domain: nil) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :accounts, name: :index_accounts_on_username_and_domain_lower _remote_account = Fabricate(:account, username: duplicate_account_username, domain: duplicate_account_domain) _remote_account_dupe = Fabricate.build(:account, username: duplicate_account_username, domain: duplicate_account_domain).save(validate: false) _local_account = Fabricate(:account, username: duplicate_account_username, domain: nil) _local_account_dupe = Fabricate.build(:account, username: duplicate_account_username, domain: nil).save(validate: false) end def choose_local_account_to_keep allow(cli.shell) .to receive(:ask) .with(/Account to keep unchanged/, anything) .and_return('0') .once end end context 'with duplicate users on email' do before do prepare_duplicate_data end let(:duplicate_email) { 'duplicate@example.host' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating user records', 'Restoring users indexes', 'Finished!' ) .and change(duplicate_users, :count).from(2).to(1) end def duplicate_users User.where(email: duplicate_email) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :users, :email Fabricate(:user, email: duplicate_email) Fabricate.build(:user, email: duplicate_email).save(validate: false) end end context 'with duplicate users on confirmation_token' do before do prepare_duplicate_data end let(:duplicate_confirmation_token) { '123ABC' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating user records', 'Unsetting confirmation token', 'Restoring users indexes', 'Finished!' ) .and change(duplicate_users, :count).from(2).to(1) end def duplicate_users User.where(confirmation_token: duplicate_confirmation_token) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :users, :confirmation_token Fabricate(:user, confirmation_token: duplicate_confirmation_token) Fabricate.build(:user, confirmation_token: duplicate_confirmation_token).save(validate: false) end end context 'with duplicate users on reset_password_token' do before do prepare_duplicate_data end let(:duplicate_reset_password_token) { '123ABC' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating user records', 'Unsetting password reset token', 'Restoring users indexes', 'Finished!' ) .and change(duplicate_users, :count).from(2).to(1) end def duplicate_users User.where(reset_password_token: duplicate_reset_password_token) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :users, :reset_password_token Fabricate(:user, reset_password_token: duplicate_reset_password_token) Fabricate.build(:user, reset_password_token: duplicate_reset_password_token).save(validate: false) end end context 'with duplicate account_domain_blocks' do before do prepare_duplicate_data end let(:duplicate_domain) { 'example.host' } let(:account) { Fabricate(:account) } it 'runs the deduplication process' do expect { subject } .to output_results( 'Removing duplicate account domain blocks', 'Restoring account domain blocks indexes', 'Finished!' ) .and change(duplicate_account_domain_blocks, :count).from(2).to(1) end def duplicate_account_domain_blocks AccountDomainBlock.where(account: account, domain: duplicate_domain) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :account_domain_blocks, [:account_id, :domain] Fabricate(:account_domain_block, account: account, domain: duplicate_domain) Fabricate.build(:account_domain_block, account: account, domain: duplicate_domain).save(validate: false) end end context 'with duplicate announcement_reactions' do before do prepare_duplicate_data end let(:account) { Fabricate(:account) } let(:announcement) { Fabricate(:announcement) } let(:name) { Fabricate(:custom_emoji).shortcode } it 'runs the deduplication process' do expect { subject } .to output_results( 'Removing duplicate announcement reactions', 'Restoring announcement_reactions indexes', 'Finished!' ) .and change(duplicate_announcement_reactions, :count).from(2).to(1) end def duplicate_announcement_reactions AnnouncementReaction.where(account: account, announcement: announcement, name: name) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :announcement_reactions, [:account_id, :announcement_id, :name] Fabricate(:announcement_reaction, account: account, announcement: announcement, name: name) Fabricate.build(:announcement_reaction, account: account, announcement: announcement, name: name).save(validate: false) end end context 'with duplicate conversations' do before do prepare_duplicate_data end let(:uri) { 'https://example.host/path' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating conversations', 'Restoring conversations indexes', 'Finished!' ) .and change(duplicate_conversations, :count).from(2).to(1) end def duplicate_conversations Conversation.where(uri: uri) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :conversations, :uri Fabricate(:conversation, uri: uri) Fabricate.build(:conversation, uri: uri).save(validate: false) end end context 'with duplicate custom_emojis' do before do prepare_duplicate_data end let(:duplicate_shortcode) { 'wowzers' } let(:duplicate_domain) { 'example.host' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating custom_emojis', 'Restoring custom_emojis indexes', 'Finished!' ) .and change(duplicate_custom_emojis, :count).from(2).to(1) end def duplicate_custom_emojis CustomEmoji.where(shortcode: duplicate_shortcode, domain: duplicate_domain) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :custom_emojis, [:shortcode, :domain] Fabricate(:custom_emoji, shortcode: duplicate_shortcode, domain: duplicate_domain) Fabricate.build(:custom_emoji, shortcode: duplicate_shortcode, domain: duplicate_domain).save(validate: false) end end context 'with duplicate custom_emoji_categories' do before do prepare_duplicate_data end let(:duplicate_name) { 'name_value' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating custom_emoji_categories', 'Restoring custom_emoji_categories indexes', 'Finished!' ) .and change(duplicate_custom_emoji_categories, :count).from(2).to(1) end def duplicate_custom_emoji_categories CustomEmojiCategory.where(name: duplicate_name) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :custom_emoji_categories, :name Fabricate(:custom_emoji_category, name: duplicate_name) Fabricate.build(:custom_emoji_category, name: duplicate_name).save(validate: false) end end context 'with duplicate domain_allows' do before do prepare_duplicate_data end let(:domain) { 'example.host' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating domain_allows', 'Restoring domain_allows indexes', 'Finished!' ) .and change(duplicate_domain_allows, :count).from(2).to(1) end def duplicate_domain_allows DomainAllow.where(domain: domain) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :domain_allows, :domain Fabricate(:domain_allow, domain: domain) Fabricate.build(:domain_allow, domain: domain).save(validate: false) end end context 'with duplicate domain_blocks' do before do prepare_duplicate_data end let(:domain) { 'example.host' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating domain_blocks', 'Restoring domain_blocks indexes', 'Finished!' ) .and change(duplicate_domain_blocks, :count).from(2).to(1) end def duplicate_domain_blocks DomainBlock.where(domain: domain) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :domain_blocks, :domain Fabricate(:domain_block, domain: domain) Fabricate.build(:domain_block, domain: domain).save(validate: false) end end context 'with duplicate email_domain_blocks' do before do prepare_duplicate_data end let(:domain) { 'example.host' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating email_domain_blocks', 'Restoring email_domain_blocks indexes', 'Finished!' ) .and change(duplicate_email_domain_blocks, :count).from(2).to(1) end def duplicate_email_domain_blocks EmailDomainBlock.where(domain: domain) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :email_domain_blocks, :domain Fabricate(:email_domain_block, domain: domain) Fabricate.build(:email_domain_block, domain: domain).save(validate: false) end end context 'with duplicate media_attachments' do before do prepare_duplicate_data end let(:shortcode) { 'codenam' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating media_attachments', 'Restoring media_attachments indexes', 'Finished!' ) .and change(duplicate_media_attachments, :count).from(2).to(1) end def duplicate_media_attachments MediaAttachment.where(shortcode: shortcode) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :media_attachments, :shortcode Fabricate(:media_attachment, shortcode: shortcode) Fabricate.build(:media_attachment, shortcode: shortcode).save(validate: false) end end context 'with duplicate preview_cards' do before do prepare_duplicate_data end let(:url) { 'https://example.host/path' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating preview_cards', 'Restoring preview_cards indexes', 'Finished!' ) .and change(duplicate_preview_cards, :count).from(2).to(1) end def duplicate_preview_cards PreviewCard.where(url: url) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :preview_cards, :url Fabricate(:preview_card, url: url) Fabricate.build(:preview_card, url: url).save(validate: false) end end context 'with duplicate statuses' do before do prepare_duplicate_data end let(:uri) { 'https://example.host/path' } let(:account) { Fabricate(:account) } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating statuses', 'Restoring statuses indexes', 'Finished!' ) .and change(duplicate_statuses, :count).from(2).to(1) end def duplicate_statuses Status.where(uri: uri) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :statuses, :uri Fabricate(:status, account: account, uri: uri) duplicate = Fabricate.build(:status, account: account, uri: uri) duplicate.save(validate: false) Fabricate(:status_pin, account: account, status: duplicate) Fabricate(:status, in_reply_to_id: duplicate.id) Fabricate(:status, reblog_of_id: duplicate.id) end end context 'with duplicate tags' do before do prepare_duplicate_data end let(:name) { 'tagname' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating tags', 'Restoring tags indexes', 'Finished!' ) .and change(duplicate_tags, :count).from(2).to(1) end def duplicate_tags Tag.where(name: name) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :tags, name: 'index_tags_on_name_lower_btree' Fabricate(:tag, name: name) Fabricate.build(:tag, name: name).save(validate: false) end end context 'with duplicate webauthn_credentials' do before do prepare_duplicate_data end let(:external_id) { '123_123_123' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating webauthn_credentials', 'Restoring webauthn_credentials indexes', 'Finished!' ) .and change(duplicate_webauthn_credentials, :count).from(2).to(1) end def duplicate_webauthn_credentials WebauthnCredential.where(external_id: external_id) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :webauthn_credentials, :external_id Fabricate(:webauthn_credential, external_id: external_id) Fabricate.build(:webauthn_credential, external_id: external_id).save(validate: false) end end context 'with duplicate webhooks' do before do prepare_duplicate_data end let(:url) { 'https://example.host/path' } it 'runs the deduplication process' do expect { subject } .to output_results( 'Deduplicating webhooks', 'Restoring webhooks indexes', 'Finished!' ) .and change(duplicate_webhooks, :count).from(2).to(1) end def duplicate_webhooks Webhook.where(url: url) end def prepare_duplicate_data ActiveRecord::Base.connection.remove_index :webhooks, :url Fabricate(:webhook, url: url) Fabricate.build(:webhook, url: url).save(validate: false) end end def agree_to_backup_warning allow(cli.shell) .to receive(:yes?) .with('Continue? (Yes/No)') .and_return(true) .once end end end end