From e9385e93e9b4601c87d1f5d6b8ddfd815f7aedcb Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 1 Jun 2023 09:37:38 +0200 Subject: [PATCH] Add a confirmation screen when suspending a domain (#25144) --- .../admin/domain_blocks_controller.rb | 42 ++++-- .../components/admin/ImpactReport.jsx | 91 ++++++++++++ app/javascript/mastodon/locales/en.json | 4 + app/javascript/styles/mastodon/admin.scss | 9 ++ .../measure/instance_accounts_measure.rb | 16 ++- .../measure/instance_followers_measure.rb | 16 ++- .../measure/instance_follows_measure.rb | 16 ++- .../instance_media_attachments_measure.rb | 16 ++- .../measure/instance_reports_measure.rb | 16 ++- .../measure/instance_statuses_measure.rb | 16 ++- .../confirm_suspension.html.haml | 25 ++++ config/locales/en.yml | 9 ++ .../admin/domain_blocks_controller_spec.rb | 135 +++++++++++++++--- spec/features/admin/domain_blocks_spec.rb | 78 ++++++++++ .../measure/instance_accounts_measure_spec.rb | 40 ++++++ .../instance_followers_measure_spec.rb | 42 ++++++ .../measure/instance_follows_measure_spec.rb | 42 ++++++ ...instance_media_attachments_measure_spec.rb | 43 ++++++ .../measure/instance_reports_measure_spec.rb | 39 +++++ .../measure/instance_statuses_measure_spec.rb | 39 +++++ 20 files changed, 681 insertions(+), 53 deletions(-) create mode 100644 app/javascript/mastodon/components/admin/ImpactReport.jsx create mode 100644 app/views/admin/domain_blocks/confirm_suspension.html.haml create mode 100644 spec/features/admin/domain_blocks_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 081550b76..b9691c5a3 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -31,31 +31,41 @@ module Admin @domain_block = DomainBlock.new(resource_params) existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil + # Disallow accidentally downgrading a domain block if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe @domain_block.errors.delete(:domain) - render :new - else - if existing_domain_block.present? - @domain_block = existing_domain_block - @domain_block.update(resource_params) - end + return render :new + end - if @domain_block.save - DomainBlockWorker.perform_async(@domain_block.id) - log_action :create, @domain_block - redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') - else - render :new - end + # Allow transparently upgrading a domain block + if existing_domain_block.present? + @domain_block = existing_domain_block + @domain_block.assign_attributes(resource_params) + end + + # Require explicit confirmation when suspending + return render :confirm_suspension if requires_confirmation? + + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id) + log_action :create, @domain_block + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + else + render :new end end def update authorize :domain_block, :update? - if @domain_block.update(update_params) + @domain_block.assign_attributes(update_params) + + # Require explicit confirmation when suspending + return render :confirm_suspension if requires_confirmation? + + if @domain_block.save DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?) log_action :update, @domain_block redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') @@ -92,5 +102,9 @@ module Admin def action_from_button 'save' if params[:save] end + + def requires_confirmation? + @domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm] + end end end diff --git a/app/javascript/mastodon/components/admin/ImpactReport.jsx b/app/javascript/mastodon/components/admin/ImpactReport.jsx new file mode 100644 index 000000000..c27ee0ab0 --- /dev/null +++ b/app/javascript/mastodon/components/admin/ImpactReport.jsx @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedNumber, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import api from 'mastodon/api'; +import { Skeleton } from 'mastodon/components/skeleton'; + +export default class ImpactReport extends PureComponent { + + static propTypes = { + domain: PropTypes.string.isRequired, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { domain } = this.props; + + const params = { + domain: domain, + include_subdomains: true, + }; + + api().post('/api/v1/admin/measures', { + keys: ['instance_accounts', 'instance_follows', 'instance_followers'], + start_at: null, + end_at: null, + instance_accounts: params, + instance_follows: params, + instance_followers: params, + }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { loading, data } = this.state; + + return ( +
+

+ + + + + + + + + + 0 })}> + + + + + + 0 })}> + + + + + +
+ + + {loading ? : } +
+ + + {loading ? : } +
+ + + {loading ? : } +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index a9c9f534f..5ed793cdb 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -73,6 +73,10 @@ "admin.dashboard.retention.average": "Average", "admin.dashboard.retention.cohort": "Sign-up month", "admin.dashboard.retention.cohort_size": "New users", + "admin.impact_report.instance_accounts": "Accounts profiles this would delete", + "admin.impact_report.instance_followers": "Followers our users would lose", + "admin.impact_report.instance_follows": "Followers their users would lose", + "admin.impact_report.title": "Impact summary", "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "An unexpected error occurred.", diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index c4ecb4254..376cffe48 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -1293,6 +1293,15 @@ a.sparkline { &:last-child { border-bottom: 0; } + + &.negative { + color: $error-value-color; + font-weight: 700; + + .dimension__item__value { + color: $error-value-color; + } + } } } diff --git a/app/lib/admin/metrics/measure/instance_accounts_measure.rb b/app/lib/admin/metrics/measure/instance_accounts_measure.rb index 4c61a064a..14a61de88 100644 --- a/app/lib/admin/metrics/measure/instance_accounts_measure.rb +++ b/app/lib/admin/metrics/measure/instance_accounts_measure.rb @@ -16,7 +16,9 @@ class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure protected def perform_total_query - Account.where(domain: params[:domain]).count + domain = params[:domain] + domain = Instance.by_domain_and_subdomains(params[:domain]).select(:domain) if params[:include_subdomains] + Account.where(domain: domain).count end def perform_previous_total_query @@ -24,13 +26,21 @@ class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure end def perform_data_query + account_matching_sql = begin + if params[:include_subdomains] + "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" + else + 'accounts.domain = $3::text' + end + end + sql = <<-SQL.squish SELECT axis.*, ( WITH new_accounts AS ( SELECT accounts.id FROM accounts WHERE date_trunc('day', accounts.created_at)::date = axis.period - AND accounts.domain = $3::text + AND #{account_matching_sql} ) SELECT count(*) FROM new_accounts ) AS value @@ -53,6 +63,6 @@ class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure end def params - @params.permit(:domain) + @params.permit(:domain, :include_subdomains) end end diff --git a/app/lib/admin/metrics/measure/instance_followers_measure.rb b/app/lib/admin/metrics/measure/instance_followers_measure.rb index caa60013b..dc0f5492c 100644 --- a/app/lib/admin/metrics/measure/instance_followers_measure.rb +++ b/app/lib/admin/metrics/measure/instance_followers_measure.rb @@ -16,7 +16,9 @@ class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measur protected def perform_total_query - Follow.joins(:account).merge(Account.where(domain: params[:domain])).count + domain = params[:domain] + domain = Instance.by_domain_and_subdomains(params[:domain]).select(:domain) if params[:include_subdomains] + Follow.joins(:account).merge(Account.where(domain: domain)).count end def perform_previous_total_query @@ -24,6 +26,14 @@ class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measur end def perform_data_query + account_matching_sql = begin + if params[:include_subdomains] + "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" + else + 'accounts.domain = $3::text' + end + end + sql = <<-SQL.squish SELECT axis.*, ( WITH new_followers AS ( @@ -31,7 +41,7 @@ class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measur FROM follows INNER JOIN accounts ON follows.account_id = accounts.id WHERE date_trunc('day', follows.created_at)::date = axis.period - AND accounts.domain = $3::text + AND #{account_matching_sql} ) SELECT count(*) FROM new_followers ) AS value @@ -54,6 +64,6 @@ class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measur end def params - @params.permit(:domain) + @params.permit(:domain, :include_subdomains) end end diff --git a/app/lib/admin/metrics/measure/instance_follows_measure.rb b/app/lib/admin/metrics/measure/instance_follows_measure.rb index b026c7e6d..f2088ffb3 100644 --- a/app/lib/admin/metrics/measure/instance_follows_measure.rb +++ b/app/lib/admin/metrics/measure/instance_follows_measure.rb @@ -16,7 +16,9 @@ class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure: protected def perform_total_query - Follow.joins(:target_account).merge(Account.where(domain: params[:domain])).count + domain = params[:domain] + domain = Instance.by_domain_and_subdomains(params[:domain]).select(:domain) if params[:include_subdomains] + Follow.joins(:target_account).merge(Account.where(domain: domain)).count end def perform_previous_total_query @@ -24,6 +26,14 @@ class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure: end def perform_data_query + account_matching_sql = begin + if params[:include_subdomains] + "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" + else + 'accounts.domain = $3::text' + end + end + sql = <<-SQL.squish SELECT axis.*, ( WITH new_follows AS ( @@ -31,7 +41,7 @@ class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure: FROM follows INNER JOIN accounts ON follows.target_account_id = accounts.id WHERE date_trunc('day', follows.created_at)::date = axis.period - AND accounts.domain = $3::text + AND #{account_matching_sql} ) SELECT count(*) FROM new_follows ) AS value @@ -54,6 +64,6 @@ class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure: end def params - @params.permit(:domain) + @params.permit(:domain, :include_subdomains) end end diff --git a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb index 2e2154c92..779883e03 100644 --- a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb +++ b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb @@ -26,7 +26,9 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: protected def perform_total_query - MediaAttachment.joins(:account).merge(Account.where(domain: params[:domain])).sum('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)') + domain = params[:domain] + domain = Instance.by_domain_and_subdomains(params[:domain]).select(:domain) if params[:include_subdomains] + MediaAttachment.joins(:account).merge(Account.where(domain: domain)).sum('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)') end def perform_previous_total_query @@ -34,6 +36,14 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: end def perform_data_query + account_matching_sql = begin + if params[:include_subdomains] + "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" + else + 'accounts.domain = $3::text' + end + end + sql = <<-SQL.squish SELECT axis.*, ( WITH new_media_attachments AS ( @@ -41,7 +51,7 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: FROM media_attachments INNER JOIN accounts ON accounts.id = media_attachments.account_id WHERE date_trunc('day', media_attachments.created_at)::date = axis.period - AND accounts.domain = $3::text + AND #{account_matching_sql} ) SELECT SUM(size) FROM new_media_attachments ) AS value @@ -64,6 +74,6 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: end def params - @params.permit(:domain) + @params.permit(:domain, :include_subdomains) end end diff --git a/app/lib/admin/metrics/measure/instance_reports_measure.rb b/app/lib/admin/metrics/measure/instance_reports_measure.rb index 6b3f35067..c1f7189bf 100644 --- a/app/lib/admin/metrics/measure/instance_reports_measure.rb +++ b/app/lib/admin/metrics/measure/instance_reports_measure.rb @@ -16,7 +16,9 @@ class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure: protected def perform_total_query - Report.where(target_account: Account.where(domain: params[:domain])).count + domain = params[:domain] + domain = Instance.by_domain_and_subdomains(params[:domain]).select(:domain) if params[:include_subdomains] + Report.where(target_account: Account.where(domain: domain)).count end def perform_previous_total_query @@ -24,6 +26,14 @@ class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure: end def perform_data_query + account_matching_sql = begin + if params[:include_subdomains] + "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" + else + 'accounts.domain = $3::text' + end + end + sql = <<-SQL.squish SELECT axis.*, ( WITH new_reports AS ( @@ -31,7 +41,7 @@ class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure: FROM reports INNER JOIN accounts ON accounts.id = reports.target_account_id WHERE date_trunc('day', reports.created_at)::date = axis.period - AND accounts.domain = $3::text + AND #{account_matching_sql} ) SELECT count(*) FROM new_reports ) AS value @@ -54,6 +64,6 @@ class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure: end def params - @params.permit(:domain) + @params.permit(:domain, :include_subdomains) end end diff --git a/app/lib/admin/metrics/measure/instance_statuses_measure.rb b/app/lib/admin/metrics/measure/instance_statuses_measure.rb index 86b10da6c..1b38b40c5 100644 --- a/app/lib/admin/metrics/measure/instance_statuses_measure.rb +++ b/app/lib/admin/metrics/measure/instance_statuses_measure.rb @@ -16,7 +16,9 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure protected def perform_total_query - Status.joins(:account).merge(Account.where(domain: params[:domain])).count + domain = params[:domain] + domain = Instance.by_domain_and_subdomains(params[:domain]).select(:domain) if params[:include_subdomains] + Status.joins(:account).merge(Account.where(domain: domain)).count end def perform_previous_total_query @@ -24,6 +26,14 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure end def perform_data_query + account_matching_sql = begin + if params[:include_subdomains] + "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $5::text))" + else + 'accounts.domain = $5::text' + end + end + sql = <<-SQL.squish SELECT axis.*, ( WITH new_statuses AS ( @@ -31,7 +41,7 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure FROM statuses INNER JOIN accounts ON accounts.id = statuses.account_id WHERE statuses.id BETWEEN $3 AND $4 - AND accounts.domain = $5::text + AND #{account_matching_sql} AND date_trunc('day', statuses.created_at)::date = axis.period ) SELECT count(*) FROM new_statuses @@ -55,6 +65,6 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure end def params - @params.permit(:domain) + @params.permit(:domain, :include_subdomains) end end diff --git a/app/views/admin/domain_blocks/confirm_suspension.html.haml b/app/views/admin/domain_blocks/confirm_suspension.html.haml new file mode 100644 index 000000000..fa9272c77 --- /dev/null +++ b/app/views/admin/domain_blocks/confirm_suspension.html.haml @@ -0,0 +1,25 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('.title', domain: Addressable::IDNA.to_unicode(@domain_block.domain)) + += simple_form_for @domain_block, url: admin_domain_blocks_path(@domain_block) do |f| + + %p.hint= t('.preamble_html', domain: Addressable::IDNA.to_unicode(@domain_block.domain)) + %ul.hint + %li= t('.stop_communication') + %li= t('.remove_all_data') + %li= t('.undo_relationships') + %li.negative-hint= t('.permanent_action') + + - %i(domain severity reject_media reject_reports obfuscate private_comment public_comment).each do |key| + = f.hidden_field key + + %hr.spacer + + = react_admin_component :impact_report, domain: @domain_block.domain + + .actions + = link_to t('.cancel'), admin_instances_path, class: 'button button-tertiary' + = f.button :submit, t('.confirm'), class: 'button negative', name: :confirm diff --git a/config/locales/en.yml b/config/locales/en.yml index 76198763a..6a8da6e60 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -382,6 +382,15 @@ en: undo: Disallow federation with domain domain_blocks: add_new: Add new domain block + confirm_suspension: + cancel: Cancel + confirm: Suspend + permanent_action: Undoing the suspension will not restore any data or relationship. + preamble_html: You are about to suspend %{domain} and its subdomains. + remove_all_data: This will remove all content, media, and profile data for this domain's accounts from your server. + stop_communication: Your server will stop communicating with these servers. + title: Confirm domain block for %{domain} + undo_relationships: This will undo any follow relationship between accounts of these servers and yours. created_msg: Domain block is now being processed destroyed_msg: Domain block has been undone domain: Domain diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb index 6aed172ac..d499aa64c 100644 --- a/spec/controllers/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/domain_blocks_controller_spec.rb @@ -40,42 +40,135 @@ RSpec.describe Admin::DomainBlocksController do end describe 'POST #create' do - it 'blocks the domain when succeeded to save' do + before do allow(DomainBlockWorker).to receive(:perform_async).and_return(true) - - post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } } - - expect(DomainBlockWorker).to have_received(:perform_async) - expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg') - expect(response).to redirect_to(admin_instances_path(limited: '1')) end - it 'renders new when failed to save' do - Fabricate(:domain_block, domain: 'example.com', severity: 'suspend') - allow(DomainBlockWorker).to receive(:perform_async).and_return(true) + context 'with "silence" severity and no conflict' do + before do + post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } } + end - post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } } + it 'records a block' do + expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true + end - expect(DomainBlockWorker).to_not have_received(:perform_async) - expect(response).to render_template :new + it 'calls DomainBlockWorker' do + expect(DomainBlockWorker).to have_received(:perform_async) + end + + it 'redirects with a success message' do + expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg') + expect(response).to redirect_to(admin_instances_path(limited: '1')) + end end - it 'allows upgrading a block' do - Fabricate(:domain_block, domain: 'example.com', severity: 'silence') - allow(DomainBlockWorker).to receive(:perform_async).and_return(true) + context 'when the new domain block conflicts with an existing one' do + before do + Fabricate(:domain_block, domain: 'example.com', severity: 'suspend') + post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } } + end - post :create, params: { domain_block: { domain: 'example.com', severity: 'silence', reject_media: true, reject_reports: true } } + it 'does not record a block' do + expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be false + end - expect(DomainBlockWorker).to have_received(:perform_async) - expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg') - expect(response).to redirect_to(admin_instances_path(limited: '1')) + it 'does not call DomainBlockWorker' do + expect(DomainBlockWorker).to_not have_received(:perform_async) + end + + it 'renders new' do + expect(response).to render_template :new + end + end + + context 'with "suspend" severity and no conflict' do + context 'without a confirmation' do + before do + post :create, params: { domain_block: { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true } } + end + + it 'does not record a block' do + expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be false + end + + it 'does not call DomainBlockWorker' do + expect(DomainBlockWorker).to_not have_received(:perform_async) + end + + it 'renders confirm_suspension' do + expect(response).to render_template :confirm_suspension + end + end + + context 'with a confirmation' do + before do + post :create, params: { :domain_block => { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true }, 'confirm' => '' } + end + + it 'records a block' do + expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true + end + + it 'calls DomainBlockWorker' do + expect(DomainBlockWorker).to have_received(:perform_async) + end + + it 'redirects with a success message' do + expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg') + expect(response).to redirect_to(admin_instances_path(limited: '1')) + end + end + end + + context 'when upgrading an existing block' do + before do + Fabricate(:domain_block, domain: 'example.com', severity: 'silence') + end + + context 'without a confirmation' do + before do + post :create, params: { domain_block: { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true } } + end + + it 'does not record a block' do + expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be false + end + + it 'does not call DomainBlockWorker' do + expect(DomainBlockWorker).to_not have_received(:perform_async) + end + + it 'renders confirm_suspension' do + expect(response).to render_template :confirm_suspension + end + end + + context 'with a confirmation' do + before do + post :create, params: { :domain_block => { domain: 'example.com', severity: 'suspend', reject_media: true, reject_reports: true }, 'confirm' => '' } + end + + it 'updates the record' do + expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true + end + + it 'calls DomainBlockWorker' do + expect(DomainBlockWorker).to have_received(:perform_async) + end + + it 'redirects with a success message' do + expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg') + expect(response).to redirect_to(admin_instances_path(limited: '1')) + end + end end end describe 'PUT #update' do let!(:remote_account) { Fabricate(:account, domain: 'example.com') } let(:subject) do - post :update, params: { id: domain_block.id, domain_block: { domain: 'example.com', severity: new_severity } } + post :update, params: { :id => domain_block.id, :domain_block => { domain: 'example.com', severity: new_severity }, 'confirm' => '' } end let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) } diff --git a/spec/features/admin/domain_blocks_spec.rb b/spec/features/admin/domain_blocks_spec.rb new file mode 100644 index 000000000..3cf60a48a --- /dev/null +++ b/spec/features/admin/domain_blocks_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'blocking domains through the moderation interface' do + before do + sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user + end + + context 'when silencing a new domain' do + it 'adds a new domain block' do + visit new_admin_domain_block_path + + fill_in 'domain_block_domain', with: 'example.com' + select I18n.t('admin.domain_blocks.new.severity.silence'), from: 'domain_block_severity' + click_on I18n.t('admin.domain_blocks.new.create') + + expect(DomainBlock.exists?(domain: 'example.com', severity: 'silence')).to be true + end + end + + context 'when suspending a new domain' do + it 'presents a confirmation screen before suspending the domain' do + visit new_admin_domain_block_path + + fill_in 'domain_block_domain', with: 'example.com' + select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' + click_on I18n.t('admin.domain_blocks.new.create') + + # It presents a confirmation screen + expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) + + # Confirming creates a block + click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + + expect(DomainBlock.exists?(domain: 'example.com', severity: 'suspend')).to be true + end + end + + context 'when suspending a domain that is already silenced' do + it 'presents a confirmation screen before suspending the domain' do + domain_block = Fabricate(:domain_block, domain: 'example.com', severity: 'silence') + + visit new_admin_domain_block_path + + fill_in 'domain_block_domain', with: 'example.com' + select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' + click_on I18n.t('admin.domain_blocks.new.create') + + # It presents a confirmation screen + expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) + + # Confirming updates the block + click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + + expect(domain_block.reload.severity).to eq 'silence' + end + end + + context 'when editing a domain block' do + it 'presents a confirmation screen before suspending the domain' do + domain_block = Fabricate(:domain_block, domain: 'example.com', severity: 'silence') + + visit edit_admin_domain_block_path(domain_block) + + select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity' + click_on I18n.t('generic.save_changes') + + # It presents a confirmation screen + expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com')) + + # Confirming updates the block + click_on I18n.t('admin.domain_blocks.confirm_suspension.confirm') + + expect(domain_block.reload.severity).to eq 'silence' + end + end +end diff --git a/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb new file mode 100644 index 000000000..29a157491 --- /dev/null +++ b/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::InstanceAccountsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:domain) { 'example.com' } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + + let(:params) { ActionController::Parameters.new(domain: domain) } + + before do + Fabricate(:account, domain: domain, created_at: 1.year.ago) + Fabricate(:account, domain: domain, created_at: 1.month.ago) + Fabricate(:account, domain: domain) + + Fabricate(:account, domain: "foo.#{domain}", created_at: 1.year.ago) + Fabricate(:account, domain: "foo.#{domain}") + Fabricate(:account, domain: "bar.#{domain}") + end + + describe 'total' do + context 'without include_subdomains' do + it 'returns the expected number of accounts' do + expect(measure.total).to eq 3 + end + end + + context 'with include_subdomains' do + let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') } + + it 'returns the expected number of accounts' do + expect(measure.total).to eq 6 + end + end + end +end diff --git a/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb new file mode 100644 index 000000000..ebf789c1b --- /dev/null +++ b/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::InstanceFollowersMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:domain) { 'example.com' } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + + let(:params) { ActionController::Parameters.new(domain: domain) } + + before do + local_account = Fabricate(:account) + + Fabricate(:account, domain: domain).follow!(local_account) + Fabricate(:account, domain: domain).follow!(local_account) + Fabricate(:account, domain: domain) + + Fabricate(:account, domain: "foo.#{domain}").follow!(local_account) + Fabricate(:account, domain: "foo.#{domain}").follow!(local_account) + Fabricate(:account, domain: "bar.#{domain}") + end + + describe 'total' do + context 'without include_subdomains' do + it 'returns the expected number of accounts' do + expect(measure.total).to eq 2 + end + end + + context 'with include_subdomains' do + let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') } + + it 'returns the expected number of accounts' do + expect(measure.total).to eq 4 + end + end + end +end diff --git a/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb new file mode 100644 index 000000000..335e3c732 --- /dev/null +++ b/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::InstanceFollowsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:domain) { 'example.com' } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + + let(:params) { ActionController::Parameters.new(domain: domain) } + + before do + local_account = Fabricate(:account) + + local_account.follow!(Fabricate(:account, domain: domain)) + local_account.follow!(Fabricate(:account, domain: domain)) + Fabricate(:account, domain: domain) + + local_account.follow!(Fabricate(:account, domain: "foo.#{domain}")) + local_account.follow!(Fabricate(:account, domain: "foo.#{domain}")) + Fabricate(:account, domain: "bar.#{domain}") + end + + describe 'total' do + context 'without include_subdomains' do + it 'returns the expected number of accounts' do + expect(measure.total).to eq 2 + end + end + + context 'with include_subdomains' do + let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') } + + it 'returns the expected number of accounts' do + expect(measure.total).to eq 4 + end + end + end +end diff --git a/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb new file mode 100644 index 000000000..711a2aff0 --- /dev/null +++ b/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:domain) { 'example.com' } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + + let(:params) { ActionController::Parameters.new(domain: domain) } + + let(:remote_account) { Fabricate(:account, domain: domain) } + let(:remote_account_on_subdomain) { Fabricate(:account, domain: "foo.#{domain}") } + + before do + remote_account.media_attachments.create!(file: attachment_fixture('attachment.jpg')) + remote_account_on_subdomain.media_attachments.create!(file: attachment_fixture('attachment.jpg')) + end + + describe 'total' do + context 'without include_subdomains' do + it 'returns the expected number of accounts' do + expected_total = remote_account.media_attachments.sum(:file_file_size) + remote_account.media_attachments.sum(:thumbnail_file_size) + expect(measure.total).to eq expected_total + end + end + + context 'with include_subdomains' do + let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') } + + it 'returns the expected number of accounts' do + expected_total = [remote_account, remote_account_on_subdomain].sum do |account| + account.media_attachments.sum(:file_file_size) + account.media_attachments.sum(:thumbnail_file_size) + end + + expect(measure.total).to eq expected_total + end + end + end +end diff --git a/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb new file mode 100644 index 000000000..f0ffd39cf --- /dev/null +++ b/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::InstanceReportsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:domain) { 'example.com' } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + + let(:params) { ActionController::Parameters.new(domain: domain) } + + before do + Fabricate(:report, target_account: Fabricate(:account, domain: domain)) + Fabricate(:report, target_account: Fabricate(:account, domain: domain)) + + Fabricate(:report, target_account: Fabricate(:account, domain: "foo.#{domain}")) + Fabricate(:report, target_account: Fabricate(:account, domain: "foo.#{domain}")) + Fabricate(:report, target_account: Fabricate(:account, domain: "bar.#{domain}")) + end + + describe 'total' do + context 'without include_subdomains' do + it 'returns the expected number of accounts' do + expect(measure.total).to eq 2 + end + end + + context 'with include_subdomains' do + let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') } + + it 'returns the expected number of accounts' do + expect(measure.total).to eq 5 + end + end + end +end diff --git a/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb new file mode 100644 index 000000000..c1425ecdb --- /dev/null +++ b/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::InstanceStatusesMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:domain) { 'example.com' } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + + let(:params) { ActionController::Parameters.new(domain: domain) } + + before do + Fabricate(:status, account: Fabricate(:account, domain: domain)) + Fabricate(:status, account: Fabricate(:account, domain: domain)) + + Fabricate(:status, account: Fabricate(:account, domain: "foo.#{domain}")) + Fabricate(:status, account: Fabricate(:account, domain: "foo.#{domain}")) + Fabricate(:status, account: Fabricate(:account, domain: "bar.#{domain}")) + end + + describe 'total' do + context 'without include_subdomains' do + it 'returns the expected number of accounts' do + expect(measure.total).to eq 2 + end + end + + context 'with include_subdomains' do + let(:params) { ActionController::Parameters.new(domain: domain, include_subdomains: 'true') } + + it 'returns the expected number of accounts' do + expect(measure.total).to eq 5 + end + end + end +end