This commit is contained in:
Claire 2024-05-10 20:02:44 +00:00 committed by GitHub
commit 4e92c459ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 651 additions and 61 deletions

View File

@ -13,4 +13,8 @@ class ActivityPub::BaseController < Api::BaseController
def skip_temporary_suspension_response?
false
end
def skip_pending_deletion_response?
false
end
end

View File

@ -35,6 +35,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
true
end
def skip_pending_deletion_response?
true
end
def body
return @body if defined?(@body)

View File

@ -13,7 +13,7 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
private
def set_account
@account = Account.find(params[:account_id])
@account = Account.without_deleted.find(params[:account_id])
end
def set_featured_tags

View File

@ -14,7 +14,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
private
def set_account
@account = Account.find(params[:account_id])
@account = Account.without_deleted.find(params[:account_id])
end
def load_accounts

View File

@ -14,7 +14,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
private
def set_account
@account = Account.find(params[:account_id])
@account = Account.without_deleted.find(params[:account_id])
end
def load_accounts

View File

@ -11,6 +11,6 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
private
def set_account
@account = Account.find(params[:account_id])
@account = Account.without_deleted.find(params[:account_id])
end
end

View File

@ -13,6 +13,6 @@ class Api::V1::Accounts::ListsController < Api::BaseController
private
def set_account
@account = Account.find(params[:account_id])
@account = Account.without_deleted.find(params[:account_id])
end
end

View File

@ -21,7 +21,7 @@ class Api::V1::Accounts::NotesController < Api::BaseController
private
def set_account
@account = Account.find(params[:account_id])
@account = Account.without_deleted.find(params[:account_id])
end
def relationships_presenter

View File

@ -21,7 +21,7 @@ class Api::V1::Accounts::PinsController < Api::BaseController
private
def set_account
@account = Account.find(params[:account_id])
@account = Account.without_deleted.find(params[:account_id])
end
def relationships_presenter

View File

@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
before_action :require_user!
def index
@accounts = Account.where(id: account_ids).select(:id, :domain)
@accounts = Account.without_deleted.where(id: account_ids).select(:id, :domain)
@accounts.merge!(Account.without_suspended) unless truthy_param?(:with_suspended)
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
end

View File

@ -15,7 +15,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
private
def set_account
@account = Account.find(params[:account_id])
@account = Account.without_deleted.find(params[:account_id])
end
def load_statuses

View File

@ -82,7 +82,7 @@ class Api::V1::AccountsController < Api::BaseController
private
def set_account
@account = Account.find(params[:id])
@account = Account.without_deleted.find(params[:id])
end
def set_accounts

View File

@ -36,7 +36,7 @@ module AccountOwnedConcern
def check_account_suspension
if @account.permanently_unavailable?
permanent_unavailability_response
elsif @account.suspended? && !skip_temporary_suspension_response?
elsif (@account.suspended? && !skip_temporary_suspension_response?) || (@account.deleted? && !skip_pending_deletion_response?)
temporary_suspension_response
end
end
@ -45,6 +45,10 @@ module AccountOwnedConcern
false
end
def skip_pending_deletion_response?
false
end
def permanent_unavailability_response
expires_in(3.minutes, public: true)
gone

View File

@ -37,7 +37,7 @@ class Settings::DeletesController < Settings::BaseController
end
def destroy_account!
current_account.suspend!(origin: :local, block_email: false)
current_account.mark_deleted!
AccountDeletionWorker.perform_async(current_user.account_id)
sign_out
end

View File

@ -11,7 +11,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
target_statuses = target_statuses_by_account[target_account.id]
replied_to_accounts = target_statuses.nil? ? [] : Account.local.where(id: target_statuses.filter_map(&:in_reply_to_account_id))
next if target_account.suspended? || (!target_account.local? && replied_to_accounts.none?)
next if target_account.suspended? || target_account.permanently_deleted? || (!target_account.local? && replied_to_accounts.none?)
ReportService.new.call(
@account,

View File

@ -51,6 +51,7 @@
# reviewed_at :datetime
# requested_review_at :datetime
# indexable :boolean default(FALSE), not null
# deleted_at :datetime
#
class Account < ApplicationRecord
@ -122,7 +123,8 @@ class Account < ApplicationRecord
scope :silenced, -> { where.not(silenced_at: nil) }
scope :suspended, -> { where.not(suspended_at: nil) }
scope :sensitized, -> { where.not(sensitized_at: nil) }
scope :without_suspended, -> { where(suspended_at: nil) }
scope :without_deleted, -> { where(deleted_at: nil) }
scope :without_suspended, -> { without_deleted.where(suspended_at: nil) }
scope :without_silenced, -> { where(silenced_at: nil) }
scope :without_instance_actor, -> { where.not(id: INSTANCE_ACTOR_ID) }
scope :recent, -> { reorder(id: :desc) }
@ -247,16 +249,25 @@ class Account < ApplicationRecord
suspended_at.present? && !instance_actor?
end
def suspended_permanently?
suspended? && deletion_request.nil?
end
def suspended_temporarily?
suspended? && deletion_request.present?
end
alias unavailable? suspended?
alias permanently_unavailable? suspended_permanently?
def deleted?
deleted_at.present? && !instance_actor?
end
def permanently_deleted?
deleted? && deletion_request.nil?
end
def unavailable?
deleted? || suspended?
end
def permanently_unavailable?
unavailable? && deletion_request.nil?
end
def suspend!(date: Time.now.utc, origin: :local, block_email: true)
transaction do
@ -266,6 +277,13 @@ class Account < ApplicationRecord
end
end
def mark_deleted!(date: Time.now.utc)
transaction do
create_deletion_request!
update!(deleted_at: date)
end
end
def unsuspend!
transaction do
deletion_request&.destroy!

View File

@ -57,7 +57,7 @@ module Account::Search
LEFT JOIN users ON accounts.id = users.account_id
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
AND accounts.suspended_at IS NULL
AND accounts.suspended_at IS NULL AND accounts.deleted_at IS NULL
AND accounts.moved_to_account_id IS NULL
AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
ORDER BY rank DESC
@ -80,7 +80,7 @@ module Account::Search
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
WHERE accounts.id IN (SELECT * FROM first_degree)
AND to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
AND accounts.suspended_at IS NULL
AND accounts.suspended_at IS NULL AND accounts.deleted_at IS NULL
AND accounts.moved_to_account_id IS NULL
GROUP BY accounts.id, s.id
ORDER BY rank DESC
@ -98,7 +98,7 @@ module Account::Search
LEFT JOIN users ON accounts.id = users.account_id
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
AND accounts.suspended_at IS NULL
AND accounts.suspended_at IS NULL AND accounts.deleted_at IS NULL
AND accounts.moved_to_account_id IS NULL
AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
GROUP BY accounts.id, s.id

View File

@ -112,14 +112,14 @@ class User < ApplicationRecord
validates :confirm_password, absence: true, on: :create
validate :validate_role_elevation
scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) }
scope :account_available, -> { joins(:account).merge(Account.without_suspended.without_deleted) }
scope :recent, -> { order(id: :desc) }
scope :pending, -> { where(approved: false) }
scope :approved, -> { where(approved: true) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :enabled, -> { where(disabled: false) }
scope :disabled, -> { where(disabled: true) }
scope :active, -> { confirmed.signed_in_recently.account_not_suspended }
scope :active, -> { confirmed.signed_in_recently.account_available }
scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) }
scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) }
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }

View File

@ -225,9 +225,14 @@ class DeleteAccountService < BaseService
return unless keep_account_record?
if @options[:suspended_at]
@account.suspended_at = @options[:suspended_at]
@account.suspension_origin = :local
else
@account.deleted_at = Time.now.utc
end
@account.silenced_at = nil
@account.suspended_at = @options[:suspended_at] || Time.now.utc
@account.suspension_origin = :local
@account.locked = false
@account.memorial = false
@account.discoverable = false

View File

@ -8,6 +8,10 @@
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(account.id), method: :post, class: 'button' if can?(:redownload, account) && account.suspension_origin_remote?
- if deletion_request.present? && can?(:destroy, account)
= link_to t('admin.accounts.delete'), admin_account_path(account.id), method: :delete, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') }
- elsif account.deleted?
%hr.spacer/
%p.muted-hint= deletion_request.present? ? t('admin.accounts.deletion_reversible_hint_html', date: content_tag(:strong, l(deletion_request.due_at.to_date))) : t('admin.accounts.deletion_irreversible')
- else
.action-buttons
%div

View File

@ -22,12 +22,12 @@
%div
= link_to admin_action_logs_path(target_account_id: account.id) do
.dashboard__counters__text
- if account.local? && account.user.nil?
- if account.suspended?
= t('admin.accounts.suspended')
- elsif account.deleted? || (account.local? && account.user.nil?)
= t('admin.accounts.deleted')
- elsif account.memorial?
= t('admin.accounts.memorialized')
- elsif account.suspended?
= t('admin.accounts.suspended')
- elsif account.silenced?
= t('admin.accounts.silenced')
- elsif account.local? && account.user&.disabled?

View File

@ -37,6 +37,8 @@
%br/
- if target_account.suspended?
%span.red= t('admin.accounts.suspended')
- elsif target_account.deleted?
%span.red= t('admin.accounts.deleted')
- elsif target_account.silenced?
%span.red= t('admin.accounts.silenced')
- elsif target_account.user&.disabled?

View File

@ -35,7 +35,7 @@ class Scheduler::SelfDestructScheduler
# deletion request.
# This targets accounts that have not been deleted nor marked for deletion yet
Account.local.without_suspended.reorder(id: :asc).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
Account.local.without_deleted.reorder(id: :asc).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
delete_account!(account)
end
@ -43,9 +43,8 @@ class Scheduler::SelfDestructScheduler
# This targets accounts that have been marked for deletion but have not been
# deleted yet
Account.local.suspended.joins(:deletion_request).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
Account.local.joins(:deletion_request).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
delete_account!(account)
account.deletion_request&.destroy
end
end
@ -66,7 +65,8 @@ class Scheduler::SelfDestructScheduler
[json, account.id, inbox_url]
end
# Do not call `Account#suspend!` because we don't want to issue a deletion request
account.update!(suspended_at: Time.now.utc, suspension_origin: :local)
# Do not call `Account#mark_deleted!` because we don't want to issue a deletion request
account.update!(deleted_at: Time.now.utc)
account.deletion_request&.destroy
end
end

View File

@ -55,6 +55,8 @@ en:
custom: Custom
delete: Delete data
deleted: Deleted
deletion_irreversible: The data of this account has been irreversibly deleted.
deletion_reversible_hint_html: This account is pending deletion, and the data will be fully removed on %{date}.
demote: Demote
destroyed_msg: "%{username}'s data is now queued to be deleted imminently"
disable: Freeze

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddDeletedAtToAccounts < ActiveRecord::Migration[6.1]
def change
add_column :accounts, :deleted_at, :datetime
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class UpdateAccountSummariesToVersion3 < ActiveRecord::Migration[6.1]
def up
reapplication_global_follow_recommendations_v1 do
drop_view :account_summaries, materialized: true
create_view :account_summaries, version: 3, materialized: { no_data: true }
safety_assured { add_index :account_summaries, :account_id, unique: true }
end
end
def down
reapplication_global_follow_recommendations_v1 do
drop_view :account_summaries, materialized: true
create_view :account_summaries, version: 2, materialized: { no_data: true }
safety_assured { add_index :account_summaries, :account_id, unique: true }
end
end
def reapplication_global_follow_recommendations_v1
drop_view :global_follow_recommendations, materialized: true
yield
create_view :global_follow_recommendations, version: 1, materialized: { no_data: true }
safety_assured { add_index :global_follow_recommendations, :account_id, unique: true }
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class BackfillAccountDeletedAt < ActiveRecord::Migration[6.1]
def up
safety_assured do
execute 'UPDATE accounts SET deleted_at = suspended_at, suspended_at = NULL WHERE domain IS NULL AND suspended_at IS NOT NULL AND NOT EXISTS (SELECT 1 FROM users WHERE account_id = accounts.id)'
end
end
def down
safety_assured do
'UPDATE accounts SET suspended_at = deleted_at WHERE domain is NULL AND suspended_at IS NULL AND deleted_at IS NOT NULL'
end
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_03_22_161611) do
ActiveRecord::Schema[7.1].define(version: 2024_03_27_145507) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -200,6 +200,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_22_161611) do
t.datetime "reviewed_at", precision: nil
t.datetime "requested_review_at", precision: nil
t.boolean "indexable", default: false, null: false
t.datetime "deleted_at", precision: nil
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["domain", "id"], name: "index_accounts_on_domain_and_id"
@ -1449,7 +1450,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_22_161611) do
WHERE ((statuses.account_id = accounts.id) AND (statuses.deleted_at IS NULL) AND (statuses.reblog_of_id IS NULL))
ORDER BY statuses.id DESC
LIMIT 20) t0)
WHERE ((accounts.suspended_at IS NULL) AND (accounts.silenced_at IS NULL) AND (accounts.moved_to_account_id IS NULL) AND (accounts.discoverable = true) AND (accounts.locked = false))
WHERE ((accounts.suspended_at IS NULL) AND (accounts.deleted_at IS NULL) AND (accounts.silenced_at IS NULL) AND (accounts.moved_to_account_id IS NULL) AND (accounts.discoverable = true) AND (accounts.locked = false))
GROUP BY accounts.id;
SQL
add_index "account_summaries", ["account_id", "language", "sensitive"], name: "idx_on_account_id_language_sensitive_250461e1eb"

View File

@ -0,0 +1,24 @@
SELECT
accounts.id AS account_id,
mode() WITHIN GROUP (ORDER BY language ASC) AS language,
mode() WITHIN GROUP (ORDER BY sensitive ASC) AS sensitive
FROM accounts
CROSS JOIN LATERAL (
SELECT
statuses.account_id,
statuses.language,
statuses.sensitive
FROM statuses
WHERE statuses.account_id = accounts.id
AND statuses.deleted_at IS NULL
AND statuses.reblog_of_id IS NULL
ORDER BY statuses.id DESC
LIMIT 20
) t0
WHERE accounts.suspended_at IS NULL
AND accounts.deleted_at IS NULL
AND accounts.silenced_at IS NULL
AND accounts.moved_to_account_id IS NULL
AND accounts.discoverable = 't'
AND accounts.locked = 'f'
GROUP BY accounts.id

View File

@ -112,6 +112,21 @@ namespace :tests do
exit(1)
end
unless Account.find_by(username: 'suspended', domain: nil).suspended?
puts 'Unexpected value for Account#suspended? for user @suspended'
exit(1)
end
if Account.find_by(username: 'deleted', domain: nil).suspended?
puts 'Unexpected value for Account#suspended? for user @deleted'
exit(1)
end
unless Account.find_by(username: 'deleted', domain: nil).deleted?
puts 'Unexpected value for Account#deleted? for user @deleted'
exit(1)
end
unless Identity.where(provider: 'foo', uid: 0).count == 1
puts 'Identities not deduplicated as expected'
exit(1)
@ -212,20 +227,20 @@ namespace :tests do
INSERT INTO "accounts"
(id, username, domain, private_key, public_key, created_at, updated_at)
VALUES
(10, 'kmruser', NULL, #{user_private_key}, #{user_public_key}, now(), now()),
(11, 'qcuser', NULL, #{user_private_key}, #{user_public_key}, now(), now());
(12, 'kmruser', NULL, #{user_private_key}, #{user_public_key}, now(), now()),
(13, 'qcuser', NULL, #{user_private_key}, #{user_public_key}, now(), now());
INSERT INTO "users"
(id, account_id, email, created_at, updated_at, admin, locale, chosen_languages)
VALUES
(4, 10, 'kmruser@localhost', now(), now(), false, 'ku', '{en,kmr,ku,ckb}');
(5, 12, 'kmruser@localhost', now(), now(), false, 'ku', '{en,kmr,ku,ckb}');
INSERT INTO "users"
(id, account_id, email, created_at, updated_at, locale,
encrypted_otp_secret, encrypted_otp_secret_iv, encrypted_otp_secret_salt,
otp_required_for_login)
VALUES
(5, 11, 'qcuser@localhost', now(), now(), 'fr-QC',
(6, 13, 'qcuser@localhost', now(), now(), 'fr-QC',
E'Fttsy7QAa0edaDfdfSz094rRLAxc8cJweDQ4BsWH/zozcdVA8o9GLqcKhn2b\nGi/V\n',
'rys3THICkr60BoWC',
'_LMkAGvdg7a+sDIKjI3mR2Q==',
@ -234,7 +249,7 @@ namespace :tests do
INSERT INTO "settings"
(id, thing_type, thing_id, var, value, created_at, updated_at)
VALUES
(5, 'User', 4, 'default_language', E'--- kmr\n', now(), now()),
(5, 'User', 5, 'default_language', E'--- kmr\n', now(), now()),
(6, 'User', 1, 'interactions', E'--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nmust_be_follower: false\nmust_be_following: true\nmust_be_following_dm: false\n', now(), now());
INSERT INTO "identities"
@ -310,18 +325,25 @@ namespace :tests do
1, 'https://activitypub.com/users/evil/inbox', 'https://activitypub.com/users/evil/outbox',
'https://activitypub.com/users/evil/followers', true);
INSERT INTO "accounts"
(id, username, domain, private_key, public_key, created_at, updated_at, suspended)
VALUES
(10, 'suspended', NULL, #{admin_private_key}, #{admin_public_key}, now(), now(), true),
(11, 'deleted', NULL, #{user_private_key}, #{user_public_key}, now(), now(), true);
-- users
INSERT INTO "users"
(id, account_id, email, created_at, updated_at, admin)
VALUES
(1, 1, 'admin@localhost', now(), now(), true),
(2, 2, 'user@localhost', now(), now(), false);
(2, 2, 'user@localhost', now(), now(), false),
(3, 10, 'suspended@localhost', now(), now(), false);
INSERT INTO "users"
(id, account_id, email, created_at, updated_at, admin, locale)
VALUES
(3, 8, 'ptuser@localhost', now(), now(), false, 'pt');
(4, 8, 'ptuser@localhost', now(), now(), false, 'pt');
-- conversations
INSERT INTO "conversations" (id, created_at, updated_at) VALUES (1, now(), now());

View File

@ -60,6 +60,27 @@ RSpec.describe ActivityPub::CollectionsController do
expect(response).to have_http_status(403)
end
end
context 'when account is permanently deleted' do
before do
account.mark_deleted!
account.deletion_request.destroy
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when account is pending deletion' do
before do
account.mark_deleted!
end
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
end
context 'with signature' do

View File

@ -46,6 +46,27 @@ RSpec.describe ActivityPub::InboxesController do
expect(response).to have_http_status(202)
end
end
context 'when account is permanently deleted' do
before do
account.mark_deleted!
account.deletion_request.destroy
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when account is pending deletion' do
before do
account.mark_deleted!
end
it 'returns http accepted' do
expect(response).to have_http_status(202)
end
end
end
end

View File

@ -63,6 +63,27 @@ RSpec.describe ActivityPub::OutboxesController do
expect(response).to have_http_status(403)
end
end
context 'when account is permanently deleted' do
before do
account.mark_deleted!
account.deletion_request.destroy
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when account is pending deletion' do
before do
account.mark_deleted!
end
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
end
context 'with page requested' do

View File

@ -64,6 +64,31 @@ RSpec.describe ActivityPub::RepliesController do
end
end
context 'when account is permanently deleted' do
let(:parent_visibility) { :public }
before do
status.account.mark_deleted!
status.account.deletion_request.destroy
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when account is pending deletion' do
let(:parent_visibility) { :public }
before do
status.account.mark_deleted!
end
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
context 'when status is public' do
let(:parent_visibility) { :public }
let(:json) { body_as_json }

View File

@ -129,7 +129,7 @@ describe ApplicationController do
include_examples 'respond_with_error', 422
end
describe 'before_action :check_suspension' do
describe 'before_action :require_functional!' do
before do
routes.draw { get 'success' => 'anonymous#success' }
end
@ -139,17 +139,29 @@ describe ApplicationController do
expect(response).to have_http_status(200)
end
it 'does nothing if user who signed in is not suspended' do
it 'does nothing if user who signed in is functional' do
sign_in(Fabricate(:account, suspended: false).user)
get 'success'
expect(response).to have_http_status(200)
end
it 'redirects to account status page' do
it 'redirects to account status page if the account is suspended' do
sign_in(Fabricate(:account, suspended: true).user)
get 'success'
expect(response).to redirect_to(edit_user_registration_path)
end
it 'redirects to account status page if the account is pending deletion' do
sign_in(Fabricate(:account, deleted: true).user)
get 'success'
expect(response).to redirect_to(edit_user_registration_path)
end
it 'redirects to account status page if the account is unconfirmed' do
sign_in(Fabricate(:user, confirmed_at: nil))
get 'success'
expect(response).to redirect_to(edit_user_registration_path)
end
end
describe 'raise_not_found' do

View File

@ -134,6 +134,15 @@ RSpec.describe Auth::RegistrationsController do
expect(response).to have_http_status(403)
end
end
context 'when deleted' do
let(:user) { Fabricate(:user, account_attributes: { username: 'test', deleted_at: Time.now.utc }) }
it 'returns http forbidden' do
put :update
expect(response).to have_http_status(403)
end
end
end
describe 'GET #new' do

View File

@ -48,6 +48,19 @@ RSpec.describe Auth::SessionsController do
expect(response).to redirect_to(new_user_session_path)
end
end
context 'with a deleted user' do
before do
user.account.mark_deleted!
end
it 'redirects to home after sign out' do
sign_in(user, scope: :user)
delete :destroy
expect(response).to redirect_to(new_user_session_path)
end
end
end
describe 'POST #create' do

View File

@ -32,7 +32,7 @@ describe AccountControllerConcern do
end
end
context 'when account is suspended' do
context 'when account is permanently suspended' do
it 'returns http gone' do
account = Fabricate(:account, suspended: true)
get 'success', params: { account_username: account.username }
@ -40,14 +40,32 @@ describe AccountControllerConcern do
end
end
context 'when account is deleted by owner' do
context 'when account is temporarily suspended' do
it 'returns http forbidden' do
account = Fabricate(:account)
account.suspend!
get 'success', params: { account_username: account.username }
expect(response).to have_http_status(403)
end
end
context 'when account is permanently deleted' do
it 'returns http gone' do
account = Fabricate(:account, suspended: true, user: nil)
account = Fabricate(:account, deleted: true)
get 'success', params: { account_username: account.username }
expect(response).to have_http_status(410)
end
end
context 'when account is pending deletion' do
it 'returns http forbidden' do
account = Fabricate(:account)
account.mark_deleted!
get 'success', params: { account_username: account.username }
expect(response).to have_http_status(403)
end
end
context 'when account is not suspended' do
let(:account) { Fabricate(:account, username: 'username') }

View File

@ -36,6 +36,27 @@ describe FollowingAccountsController do
expect(response).to have_http_status(403)
end
end
context 'when account is permanently deleted' do
before do
alice.mark_deleted!
alice.deletion_request.destroy
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when account is pending deletion' do
before do
alice.mark_deleted!
end
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
end
context 'when format is json' do
@ -79,6 +100,27 @@ describe FollowingAccountsController do
expect(response).to have_http_status(403)
end
end
context 'when account is permanently deleted' do
before do
alice.mark_deleted!
alice.deletion_request.destroy
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when account is pending deletion' do
before do
alice.mark_deleted!
end
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
end
context 'without page' do
@ -127,6 +169,27 @@ describe FollowingAccountsController do
expect(response).to have_http_status(403)
end
end
context 'when account is permanently deleted' do
before do
alice.mark_deleted!
alice.deletion_request.destroy
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when account is pending deletion' do
before do
alice.mark_deleted!
end
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
end
end
end

View File

@ -27,6 +27,15 @@ describe Settings::DeletesController do
expect(response.headers['Cache-Control']).to include('private, no-store')
end
end
context 'when already deleted' do
let(:user) { Fabricate(:user, account_attributes: { deleted_at: Time.now.utc }) }
it 'returns http forbidden' do
get :show
expect(response).to have_http_status(403)
end
end
end
context 'when not signed in' do
@ -53,7 +62,7 @@ describe Settings::DeletesController do
it 'removes user record and redirects', :aggregate_failures, :sidekiq_inline do
expect(response).to redirect_to '/auth/sign_in'
expect(User.find_by(id: user.id)).to be_nil
expect(user.account.reload).to be_suspended
expect(user.account.reload).to be_deleted
expect(CanonicalEmailBlock.block?(user.email)).to be false
end
@ -64,6 +73,14 @@ describe Settings::DeletesController do
expect(response).to have_http_status(403)
end
end
context 'when already deleted' do
let(:user) { Fabricate(:user, account_attributes: { deleted_at: Time.now.utc }) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
end
context 'with incorrect password' do

View File

@ -34,6 +34,31 @@ describe StatusesController do
end
end
context 'when account is permanently deleted' do
before do
account.mark_deleted!
account.deletion_request.destroy
get :show, params: { account_username: account.username, id: status.id }
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when account is temporarily deleted' do
before do
account.mark_deleted!
get :show, params: { account_username: account.username, id: status.id }
end
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
context 'when status is a reblog' do
let(:original_account) { Fabricate(:account, domain: 'example.com') }
let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }

View File

@ -5,13 +5,14 @@ public_key = keypair.public_key.to_pem
private_key = keypair.to_pem
Fabricator(:account) do
transient :suspended, :silenced
transient :suspended, :silenced, :deleted
username { sequence(:username) { |i| "#{Faker::Internet.user_name(separators: %w(_))}#{i}" } }
last_webfingered_at { Time.now.utc }
public_key { public_key }
private_key { private_key }
suspended_at { |attrs| attrs[:suspended] ? Time.now.utc : nil }
silenced_at { |attrs| attrs[:silenced] ? Time.now.utc : nil }
deleted_at { |attrs| attrs[:deleted] ? Time.now.utc : nil }
user { |attrs| attrs[:domain].nil? ? Fabricate.build(:user, account: nil) : nil }
uri { |attrs| attrs[:domain].nil? ? '' : "https://#{attrs[:domain]}/users/#{attrs[:username]}" }
discoverable true

View File

@ -138,16 +138,19 @@ RSpec.describe User do
end
end
describe 'account_not_suspended' do
it 'returns with linked accounts that are not suspended' do
describe 'account_available' do
it 'returns with linked accounts that are not suspended or deleted' do
suspended_account = Fabricate(:account, suspended_at: 10.days.ago)
non_suspended_account = Fabricate(:account, suspended_at: nil)
suspended_user = Fabricate(:user, account: suspended_account)
non_suspended_user = Fabricate(:user, account: non_suspended_account)
deleted_account = Fabricate(:account, deleted_at: 10.days.ago)
deleted_user = Fabricate(:user, account: deleted_account)
expect(described_class.account_not_suspended)
expect(described_class.account_available)
.to include(non_suspended_user)
.and not_include(suspended_user)
.and not_include(deleted_user)
end
end

View File

@ -44,6 +44,33 @@ describe 'Accounts show response' do
end
end
describe 'permanently deleted account check' do
before do
account.mark_deleted!
account.deletion_request.destroy
end
it 'returns appropriate http response code' do
{ html: 410, json: 410, rss: 410 }.each do |format, code|
get short_account_path(username: account.username), as: format
expect(response).to have_http_status(code)
end
end
end
describe 'pending deletion account check' do
before { account.mark_deleted! }
it 'returns appropriate http response code' do
{ html: 403, json: 403, rss: 403 }.each do |format, code|
get short_account_path(username: account.username), as: format
expect(response).to have_http_status(code)
end
end
end
describe 'GET to short username paths' do
context 'with existing statuses' do
let!(:status) { Fabricate(:status, account: account) }

View File

@ -56,5 +56,30 @@ describe 'API V1 Accounts FollowerAccounts' do
expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
end
end
context 'when request account is permanently deleted' do
before do
account.mark_deleted!
account.deletion_request.destroy
end
it 'returns http not found' do
get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers
expect(response).to have_http_status(404)
end
end
context 'when request account is pending deletion' do
before do
account.mark_deleted!
end
it 'returns http not found' do
get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers
expect(response).to have_http_status(404)
end
end
end
end

View File

@ -56,5 +56,30 @@ describe 'API V1 Accounts FollowingAccounts' do
expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
end
end
context 'when request account is permanently deleted' do
before do
account.mark_deleted!
account.deletion_request.destroy
end
it 'returns http not found' do
get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers
expect(response).to have_http_status(404)
end
end
context 'when request account is pending deletion' do
before do
account.mark_deleted!
end
it 'returns http not found' do
get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers
expect(response).to have_http_status(404)
end
end
end
end

View File

@ -22,4 +22,29 @@ describe 'Accounts Lists API' do
expect(response).to have_http_status(200)
end
end
context 'when requested account is permanently deleted' do
before do
account.mark_deleted!
account.deletion_request.destroy
end
it 'returns http not found' do
get "/api/v1/accounts/#{account.id}/lists", headers: headers
expect(response).to have_http_status(404)
end
end
context 'when requested account is pending deletion' do
before do
account.mark_deleted!
end
it 'returns http not found' do
get "/api/v1/accounts/#{account.id}/lists", headers: headers
expect(response).to have_http_status(404)
end
end
end

View File

@ -137,6 +137,35 @@ describe 'API V1 Accounts Statuses' do
end
end
end
context 'when requested account is permanently deleted' do
let(:account) { Fabricate(:account) }
before do
account.mark_deleted!
account.deletion_request.destroy
end
it 'returns http not found' do
get "/api/v1/accounts/#{account.id}/statuses", params: { limit: 2 }, headers: headers
expect(response).to have_http_status(404)
end
end
context 'when requested account is pending deletion' do
let(:account) { Fabricate(:account) }
before do
account.mark_deleted!
end
it 'returns http not found' do
get "/api/v1/accounts/#{account.id}/statuses", params: { limit: 2 }, headers: headers
expect(response).to have_http_status(404)
end
end
end
private

View File

@ -45,6 +45,31 @@ describe '/api/v1/accounts' do
end
end
context 'when requesting a permanently deleted account' do
let(:other_account) { Fabricate(:account, deleted: true) }
before do
get "/api/v1/accounts/#{other_account.id}"
end
it 'returns http not found' do
expect(response).to have_http_status(404)
end
end
context 'when requesting an account pending deletion' do
let(:other_account) { Fabricate(:account) }
before do
other_account.mark_deleted!
get "/api/v1/accounts/#{other_account.id}"
end
it 'returns http not found' do
expect(response).to have_http_status(404)
end
end
context 'when logged in' do
subject do
get "/api/v1/accounts/#{account.id}", headers: headers

View File

@ -53,7 +53,18 @@ describe 'The /.well-known/webfinger endpoint' do
it_behaves_like 'a successful response'
end
context 'when an account is permanently suspended or deleted' do
context 'when an account is pending deletion' do
let(:resource) { alice.to_webfinger_s }
before do
alice.mark_deleted!
perform_request!
end
it_behaves_like 'a successful response'
end
context 'when an account is permanently suspended' do
let(:resource) { alice.to_webfinger_s }
before do
@ -67,6 +78,20 @@ describe 'The /.well-known/webfinger endpoint' do
end
end
context 'when an account is permanently deleted' do
let(:resource) { alice.to_webfinger_s }
before do
alice.mark_deleted!
alice.deletion_request.destroy
perform_request!
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
context 'when an account is not found' do
let(:resource) { 'acct:not@existing.com' }

View File

@ -78,6 +78,15 @@ describe AccountSearchService do
expect(results).to eq [partial]
end
it 'returns the fuzzy match first, and does not return deleted exacts' do
partial = Fabricate(:account, username: 'exactness')
Fabricate(:account, username: 'exact', deleted: true)
results = subject.call('exact', nil, limit: 10)
expect(results.size).to eq 1
expect(results).to eq [partial]
end
it 'does not return suspended remote accounts' do
Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', suspended: true)
results = subject.call('a@example.com', nil, limit: 2)
@ -85,5 +94,13 @@ describe AccountSearchService do
expect(results.size).to eq 0
expect(results).to eq []
end
it 'does not return deleted remote accounts' do
Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', deleted: true)
results = subject.call('a@example.com', nil, limit: 2)
expect(results.size).to eq 0
expect(results).to eq []
end
end
end

View File

@ -50,6 +50,11 @@ RSpec.describe NotifyService do
expect { subject }.to_not change(Notification, :count)
end
it 'does not notify when recipient is deleted' do
recipient.mark_deleted!
expect { subject }.to_not change(Notification, :count)
end
describe 'reblogs' do
let(:status) { Fabricate(:status, account: Fabricate(:account)) }
let(:activity) { Fabricate(:status, account: sender, reblog: status) }

View File

@ -39,19 +39,19 @@ describe Scheduler::SelfDestructScheduler do
end
context 'when sidekiq is operational' do
it 'suspends local non-suspended accounts' do
it 'deletes local non-deleted accounts' do
worker.perform
expect(account.reload.suspended_at).to_not be_nil
expect(account.reload.deleted_at).to_not be_nil
end
it 'suspends local suspended accounts marked for deletion' do
account.update(suspended_at: 10.days.ago)
it 'deletes local accounts marked for deletion' do
account.update(deleted_at: 10.days.ago)
deletion_request = Fabricate(:account_deletion_request, account: account)
worker.perform
expect(account.reload.suspended_at).to be > 1.day.ago
expect(account.reload.deleted_at).to be > 1.day.ago
expect { deletion_request.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end