params[:username]
end
+ def skip_temporary_suspension_response?
+ request.format == :json
+ end
+
def rss_url
if tag_requested?
short_account_tag_url(@account, params[:tag], format: 'rss')
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode?
end
+
+ def skip_temporary_suspension_response?
+ false
+ end
end
params[:account_username].present?
end
+ def skip_temporary_suspension_response?
+ true
+ end
+
def body
return @body if defined?(@body)
end
def set_replies
- @replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses
+ @replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
end
def check_account_suspension
- expires_in(3.minutes, public: true) && gone if @account.suspended?
+ if @account.suspended_permanently?
+ permanent_suspension_response
+ elsif @account.suspended? && !skip_temporary_suspension_response?
+ temporary_suspension_response
+ end
+ end
+
+ def skip_temporary_suspension_response?
+ false
+ end
+
+ def permanent_suspension_response
+ expires_in(3.minutes, public: true)
+ gone
+ end
+
+ def temporary_suspension_response
+ expires_in(3.minutes, public: true)
+ forbidden
end
end
account_followers_url(@account, page: page) unless page.nil?
end
+ def next_page_url
+ page_url(follows.next_page) if follows.respond_to?(:next_page)
+ end
+
+ def prev_page_url
+ page_url(follows.prev_page) if follows.respond_to?(:prev_page)
+ end
+
def collection_presenter
if page_requested?
ActivityPub::CollectionPresenter.new(
size: @account.followers_count,
items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) },
part_of: account_followers_url(@account),
- next: page_url(follows.next_page),
- prev: page_url(follows.prev_page)
+ next: next_page_url,
+ prev: prev_page_url
)
else
ActivityPub::CollectionPresenter.new(
account_following_index_url(@account, page: page) unless page.nil?
end
+ def next_page_url
+ page_url(follows.next_page) if follows.respond_to?(:next_page)
+ end
+
+ def prev_page_url
+ page_url(follows.prev_page) if follows.respond_to?(:prev_page)
+ end
+
def collection_presenter
if page_requested?
ActivityPub::CollectionPresenter.new(
size: @account.following_count,
items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) },
part_of: account_following_index_url(@account),
- next: page_url(follows.next_page),
- prev: page_url(follows.prev_page)
+ next: next_page_url,
+ prev: prev_page_url
)
else
ActivityPub::CollectionPresenter.new(
end
def destroy_account!
- current_account.suspend!
+ current_account.suspend!(origin: :local)
AccountDeletionWorker.perform_async(current_user.account_id)
sign_out
end
end
def check_account_suspension
- expires_in(3.minutes, public: true) && gone if @account.suspended?
+ expires_in(3.minutes, public: true) && gone if @account.suspended_permanently?
end
def bad_request
margin-bottom: 10px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+ &:last-child {
+ margin-bottom: 0;
+ }
+
&__img {
width: 100%;
position: relative;
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
+ suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
}.freeze
def self.default_key_transform
class Webfinger
class Error < StandardError; end
+ class GoneError < Error; end
+ class RedirectError < StandardError; end
class Response
def initialize(body)
res.body_with_limit
elsif res.code == 404 && use_fallback
body_from_host_meta
+ elsif res.code == 410
+ raise Webfinger::GoneError, "#{@uri} is gone from the server"
else
raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
end
# header_storage_schema_version :integer
# devices_url :string
# sensitized_at :datetime
+# suspension_origin :integer
#
class Account < ApplicationRecord
}.freeze
enum protocol: [:ostatus, :activitypub]
+ enum suspension_origin: [:local, :remote], _prefix: true
validates :username, presence: true
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
suspended_at.present?
end
- def suspend!(date = Time.now.utc)
+ def suspended_permanently?
+ suspended? && deletion_request.nil?
+ end
+
+ def suspended_temporarily?
+ suspended? && deletion_request.present?
+ end
+
+ def suspend!(date: Time.now.utc, origin: :local)
transaction do
create_deletion_request!
- update!(suspended_at: date)
+ update!(suspended_at: date, suspension_origin: origin)
end
end
def unsuspend!
transaction do
deletion_request&.destroy!
- update!(suspended_at: nil)
+ update!(suspended_at: nil, suspension_origin: nil)
end
end
def handle_suspend!
authorize(target_account, :suspend?)
log_action(:suspend, target_account)
- target_account.suspend!
+ target_account.suspend!(origin: :local)
end
def text_for_warning
end
def destroy?
- record.suspended? && record.deletion_request.present? && admin?
+ record.suspended_temporarily? && admin?
end
def unsuspend?
- staff?
+ staff? && record.suspension_origin_local?
end
def sensitive?
context_extensions :manually_approves_followers, :featured, :also_known_as,
:moved_to, :property_value, :identity_proof,
- :discoverable, :olm
+ :discoverable, :olm, :suspended
attributes :id, :type, :following, :followers,
:inbox, :outbox, :featured, :featured_tags,
attribute :devices, unless: :instance_actor?
attribute :moved_to, if: :moved?
attribute :also_known_as, if: :also_known_as?
+ attribute :suspended, if: :suspended?
class EndpointsSerializer < ActivityPub::Serializer
include RoutingHelper
has_one :icon, serializer: ActivityPub::ImageSerializer, if: :avatar_exists?
has_one :image, serializer: ActivityPub::ImageSerializer, if: :header_exists?
- delegate :moved?, :instance_actor?, to: :object
+ delegate :suspended?, :instance_actor?, to: :object
def id
object.instance_actor? ? instance_actor_url : account_url(object)
object.username
end
+ def discoverable
+ object.suspended? ? false : (object.discoverable || false)
+ end
+
def name
- object.display_name
+ object.suspended? ? '' : object.display_name
end
def summary
- Formatter.instance.simplified_format(object)
+ object.suspended? ? '' : Formatter.instance.simplified_format(object)
end
def icon
object
end
+ def suspended
+ object.suspended?
+ end
+
def url
object.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(object)
end
def avatar_exists?
- object.avatar?
+ !object.suspended? && object.avatar?
end
def header_exists?
- object.header?
+ !object.suspended? && object.header?
end
def manually_approves_followers
- object.locked
+ object.suspended? ? false : object.locked
end
def virtual_tags
- object.emojis + object.tags
+ object.suspended? ? [] : (object.emojis + object.tags)
end
def virtual_attachments
- object.fields + object.identity_proofs.active
+ object.suspended? ? [] : (object.fields + object.identity_proofs.active)
end
def moved_to
ActivityPub::TagManager.instance.uri_for(object.moved_to_account)
end
+ def moved?
+ !object.suspended? && object.moved?
+ end
+
def also_known_as?
- !object.also_known_as.empty?
+ !object.suspended? && !object.also_known_as.empty?
end
class CustomEmojiSerializer < ActivityPub::EmojiSerializer
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
- @account = Account.remote.find_by(uri: @uri) if @options[:only_key]
- @account ||= Account.find_remote(@username, @domain)
- @old_public_key = @account&.public_key
- @old_protocol = @account&.protocol
+ @account = Account.remote.find_by(uri: @uri) if @options[:only_key]
+ @account ||= Account.find_remote(@username, @domain)
+ @old_public_key = @account&.public_key
+ @old_protocol = @account&.protocol
+ @suspension_changed = false
create_account if @account.nil?
update_account
after_protocol_change! if protocol_changed?
after_key_change! if key_changed? && !@options[:signed_with_known_key]
clear_tombstones! if key_changed?
+ after_suspension_change! if suspension_changed?
- unless @options[:only_key]
+ unless @options[:only_key] || @account.suspended?
check_featured_collection! if @account.featured_collection_url.present?
check_links! unless @account.fields.empty?
end
def create_account
@account = Account.new
- @account.protocol = :activitypub
- @account.username = @username
- @account.domain = @domain
- @account.private_key = nil
- @account.suspended_at = domain_block.created_at if auto_suspend?
- @account.silenced_at = domain_block.created_at if auto_silence?
+ @account.protocol = :activitypub
+ @account.username = @username
+ @account.domain = @domain
+ @account.private_key = nil
+ @account.suspended_at = domain_block.created_at if auto_suspend?
+ @account.suspension_origin = :local if auto_suspend?
+ @account.silenced_at = domain_block.created_at if auto_silence?
+ @account.save
end
def update_account
@account.last_webfingered_at = Time.now.utc unless @options[:only_key]
@account.protocol = :activitypub
- set_immediate_attributes!
- set_fetchable_attributes! unless @options[:only_keys]
+ set_suspension!
+ set_immediate_attributes! unless @account.suspended?
+ set_fetchable_attributes! unless @options[:only_keys] || @account.suspended?
@account.save_with_optional_media!
end
@account.moved_to_account = @json['movedTo'].present? ? moved_account : nil
end
+ def set_suspension!
+ return if @account.suspended? && @account.suspension_origin_local?
+
+ if @account.suspended? && !@json['suspended']
+ @account.unsuspend!
+ @suspension_changed = true
+ elsif !@account.suspended? && @json['suspended']
+ @account.suspend!(origin: :remote)
+ @suspension_changed = true
+ end
+ end
+
def after_protocol_change!
ActivityPub::PostUpgradeWorker.perform_async(@account.domain)
end
RefollowWorker.perform_async(@account.id)
end
+ def after_suspension_change!
+ if @account.suspended?
+ Admin::SuspensionWorker.perform_async(@account.id)
+ else
+ Admin::UnsuspensionWorker.perform_async(@account.id)
+ end
+ end
+
def check_featured_collection!
ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id)
end
!@old_public_key.nil? && @old_public_key != @account.public_key
end
+ def suspension_changed?
+ @suspension_changed
+ end
+
def clear_tombstones!
Tombstone.where(account_id: @account.id).delete_all
end
@json = Oj.load(body, mode: :strict)
@options = options
- return if !supported_context? || (different_actor? && verify_account!.nil?) || @account.suspended? || @account.local?
+ return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
case @json['type']
when 'Collection', 'CollectionPage'
@json['actor'].present? && value_or_id(@json['actor']) != @account.uri
end
+ def suspended_actor?
+ @account.suspended? && !activity_allowed_while_suspended?
+ end
+
+ def activity_allowed_while_suspended?
+ %w(Delete Reject Undo Update).include?(@json['type'])
+ end
+
def process_items(items)
items.reverse_each.map { |item| process_item(item) }.compact
end
scope = Account.by_domain_and_subdomains(domain_block.domain)
scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.silence?
- scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) unless domain_block.suspend?
+ scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil, suspension_origin: nil) unless domain_block.suspend?
end
def process_domain_block!
end
def suspend_accounts!
- blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at)
+ blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at, suspension_origin: :local)
+
blocked_domain_accounts.where(suspended_at: @domain_block.created_at).reorder(nil).find_each do |account|
DeleteAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
end
def reject_follows!
return if @account.local? || !@account.activitypub?
+ # When deleting a remote account, the account obviously doesn't
+ # actually become deleted on its origin server, i.e. unlike a
+ # locally deleted account it continues to have access to its home
+ # feed and other content. To prevent it from being able to continue
+ # to access toots it would receive because it follows local accounts,
+ # we have to force it to unfollow them.
+
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
- [build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
+ [Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
end
end
return unless @options[:reserve_username]
- @account.silenced_at = nil
- @account.suspended_at = @options[:suspended_at] || Time.now.utc
- @account.locked = false
- @account.memorial = false
- @account.discoverable = false
- @account.display_name = ''
- @account.note = ''
- @account.fields = []
- @account.statuses_count = 0
- @account.followers_count = 0
- @account.following_count = 0
- @account.moved_to_account = nil
- @account.trust_level = :untrusted
+ @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
+ @account.display_name = ''
+ @account.note = ''
+ @account.fields = []
+ @account.statuses_count = 0
+ @account.followers_count = 0
+ @account.following_count = 0
+ @account.moved_to_account = nil
+ @account.trust_level = :untrusted
@account.avatar.destroy
@account.header.destroy
@account.save!
@delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
end
- def build_reject_json(follow)
- Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
- end
-
def delivery_inboxes
@delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
end
include DomainControlHelper
include WebfingerHelper
- class WebfingerRedirectError < StandardError; end
-
# Find or create an account record for a remote user. When creating,
# look up the user's webfinger and fetch ActivityPub data
# @param [String, Account] uri URI in the username@domain format or account record
@account ||= Account.find_remote(@username, @domain)
- return @account if @account&.local? || !webfinger_update_due?
+ if gone_from_origin? && not_yet_deleted?
+ queue_deletion!
+ return
+ end
+
+ return @account if @account&.local? || gone_from_origin? || !webfinger_update_due?
# Now it is certain, it is definitely a remote account, and it
# either needs to be created, or updated from fresh data
process_account!
- rescue Webfinger::Error, WebfingerRedirectError, Oj::ParseError => e
+ rescue Webfinger::Error, Oj::ParseError => e
Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}"
nil
end
elsif !redirected
return process_webfinger!("#{confirmed_username}@#{confirmed_domain}", true)
else
- raise WebfingerRedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}"
+ raise Webfinger::RedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}"
end
@domain = nil if TagManager.instance.local_domain?(@domain)
+ rescue Webfinger::GoneError
+ @gone = true
end
def process_account!
@actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? json : nil
end
+ def gone_from_origin?
+ @gone
+ end
+
+ def not_yet_deleted?
+ @account.present? && !@account.local?
+ end
+
+ def queue_deletion!
+ AccountDeletionWorker.perform_async(@account.id, reserve_username: false)
+ end
+
def lock_options
{ redis: Redis.current, key: "resolve:#{@username}@#{@domain}" }
end
# frozen_string_literal: true
class SuspendAccountService < BaseService
+ include Payloadable
+
def call(account)
@account = account
suspend!
+ reject_remote_follows!
+ distribute_update_actor!
unmerge_from_home_timelines!
unmerge_from_list_timelines!
privatize_media_attachments!
@account.suspend! unless @account.suspended?
end
+ def reject_remote_follows!
+ return if @account.local? || !@account.activitypub?
+
+ # When suspending a remote account, the account obviously doesn't
+ # actually become suspended on its origin server, i.e. unlike a
+ # locally suspended account it continues to have access to its home
+ # feed and other content. To prevent it from being able to continue
+ # to access toots it would receive because it follows local accounts,
+ # we have to force it to unfollow them. Unfortunately, there is no
+ # counterpart to this operation, i.e. you can't then force a remote
+ # account to re-follow you, so this part is not reversible.
+
+ follows = Follow.where(account: @account).to_a
+
+ ActivityPub::DeliveryWorker.push_bulk(follows) do |follow|
+ [Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
+ end
+
+ follows.in_batches.destroy_all
+ end
+
+ def distribute_update_actor!
+ ActivityPub::UpdateDistributionWorker.perform_async(@account.id) if @account.local?
+ end
+
def unmerge_from_home_timelines!
@account.followers_for_local_distribution.find_each do |follower|
FeedManager.instance.unmerge_from_home(@account, follower)
scope = Account.by_domain_and_subdomains(domain_block.domain)
scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.noop?
- scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) if domain_block.suspend?
+ scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil, suspension_origin: nil) if domain_block.suspend?
end
end
@account = account
unsuspend!
+ refresh_remote_account!
+
+ return if @account.nil?
+
merge_into_home_timelines!
merge_into_list_timelines!
publish_media_attachments!
@account.unsuspend! if @account.suspended?
end
+ def refresh_remote_account!
+ return if @account.local?
+
+ # While we had the remote account suspended, it could be that
+ # it got suspended on its origin, too. So, we need to refresh
+ # it straight away so it gets marked as remotely suspended in
+ # that case.
+
+ @account.update!(last_webfingered_at: nil)
+ @account = ResolveAccountService.new.call(@account)
+
+ # Worth noting that it is possible that the remote has not only
+ # been suspended, but deleted permanently, in which case
+ # @account would now be nil.
+ end
+
def merge_into_home_timelines!
@account.followers_for_local_distribution.find_each do |follower|
FeedManager.instance.merge_into_home(@account, follower)
sidekiq_options queue: 'pull'
- def perform(account_id)
- DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: false)
+ def perform(account_id, reserve_username: true)
+ DeleteAccountService.new.call(Account.find(account_id), reserve_username: reserve_username, reserve_email: false)
rescue ActiveRecord::RecordNotFound
true
end
--- /dev/null
+class AddSuspensionOriginToAccounts < ActiveRecord::Migration[5.2]
+ def change
+ add_column :accounts, :suspension_origin, :integer
+ end
+end
--- /dev/null
+# frozen_string_literal: true
+
+class FillAccountSuspensionOrigin < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def up
+ Account.suspended.where(suspension_origin: nil).in_batches.update_all(suspension_origin: :local)
+ end
+
+ def down; end
+end
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2020_10_08_220312) do
+ActiveRecord::Schema.define(version: 2020_10_17_234926) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
t.integer "avatar_storage_schema_version"
t.integer "header_storage_schema_version"
t.string "devices_url"
+ t.integer "suspension_origin"
t.datetime "sensitized_at"
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
end
if [404, 410].include?(code)
- SuspendAccountService.new.call(account, reserve_username: false) unless options[:dry_run]
+ DeleteAccountService.new.call(account, reserve_username: false) unless options[:dry_run]
1
else
# Touch account even during dry run to avoid getting the account into the window again
allow(service).to receive(:call)
end
- it 'does not create for user who is not signed in' do
- subject
- expect(FollowService).not_to receive(:new)
+ context 'when account is permanently suspended' do
+ before do
+ alice.suspend!
+ alice.deletion_request.destroy
+ subject
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ alice.suspend!
+ subject
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when signed out' do
+ before do
+ subject
+ end
+
+ it 'does not follow' do
+ expect(FollowService).not_to receive(:new)
+ end
end
- it 'redirects to account path' do
- sign_in(user)
- subject
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ subject
+ end
- expect(service).to have_received(:call).with(user.account, alice, with_rate_limit: true)
- expect(response).to redirect_to(account_path(alice))
+ it 'redirects to account path' do
+ expect(service).to have_received(:call).with(user.account, alice, with_rate_limit: true)
+ expect(response).to redirect_to(account_path(alice))
+ end
end
end
end
allow(service).to receive(:call)
end
- it 'does not create for user who is not signed in' do
- subject
- expect(UnfollowService).not_to receive(:new)
+ context 'when account is permanently suspended' do
+ before do
+ alice.suspend!
+ alice.deletion_request.destroy
+ subject
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ alice.suspend!
+ subject
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when signed out' do
+ before do
+ subject
+ end
+
+ it 'does not unfollow' do
+ expect(UnfollowService).not_to receive(:new)
+ end
end
- it 'redirects to account path' do
- sign_in(user)
- subject
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ subject
+ end
- expect(service).to have_received(:call).with(user.account, alice)
- expect(response).to redirect_to(account_path(alice))
+ it 'redirects to account path' do
+ expect(service).to have_received(:call).with(user.account, alice)
+ expect(response).to redirect_to(account_path(alice))
+ end
end
end
end
expect(response).to have_http_status(404)
end
end
+ end
+
+ context 'as HTML' do
+ let(:format) { 'html' }
- context 'when account is suspended' do
+ it_behaves_like 'preliminary checks'
+
+ context 'when account is permanently suspended' do
before do
account.suspend!
+ account.deletion_request.destroy
end
it 'returns http gone' do
expect(response).to have_http_status(410)
end
end
- end
- context 'as HTML' do
- let(:format) { 'html' }
+ context 'when account is temporarily suspended' do
+ before do
+ account.suspend!
+ end
- it_behaves_like 'preliminary checks'
+ it 'returns http forbidden' do
+ get :show, params: { username: account.username, format: format }
+ expect(response).to have_http_status(403)
+ end
+ end
shared_examples 'common response characteristics' do
it 'returns http success' do
it_behaves_like 'preliminary checks'
+ context 'when account is suspended permanently' do
+ before do
+ account.suspend!
+ account.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ get :show, params: { username: account.username, format: format }
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is suspended temporarily' do
+ before do
+ account.suspend!
+ end
+
+ it 'returns http success' do
+ get :show, params: { username: account.username, format: format }
+ expect(response).to have_http_status(200)
+ end
+ end
+
context do
before do
get :show, params: { username: account.username, format: format }
it_behaves_like 'preliminary checks'
+ context 'when account is permanently suspended' do
+ before do
+ account.suspend!
+ account.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ get :show, params: { username: account.username, format: format }
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ account.suspend!
+ end
+
+ it 'returns http forbidden' do
+ get :show, params: { username: account.username, format: format }
+ expect(response).to have_http_status(403)
+ end
+ end
+
shared_examples 'common response characteristics' do
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'does not set sessions' do
+ response
expect(session).to be_empty
end
context 'without signature' do
let(:remote_account) { nil }
- before do
- get :show, params: { id: 'featured', account_username: account.username }
- end
+ subject(:response) { get :show, params: { id: 'featured', account_username: account.username } }
+ subject(:body) { body_as_json }
it 'returns http success' do
expect(response).to have_http_status(200)
it_behaves_like 'cachable response'
it 'returns orderedItems with pinned statuses' do
- json = body_as_json
- expect(json[:orderedItems]).to be_an Array
- expect(json[:orderedItems].size).to eq 2
+ expect(body[:orderedItems]).to be_an Array
+ expect(body[:orderedItems].size).to eq 2
+ end
+
+ context 'when account is permanently suspended' do
+ before do
+ account.suspend!
+ account.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ account.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
end
end
context 'with signature from example.com' do
let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/instance') }
- before do
- get :show, params: { account_username: account.username }
- end
+ subject(:response) { get :show, params: { account_username: account.username } }
+ subject(:body) { body_as_json }
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns orderedItems with followers from example.com' do
- json = body_as_json
- expect(json[:orderedItems]).to be_an Array
- expect(json[:orderedItems].sort).to eq [follower_1.uri, follower_2.uri]
+ expect(body[:orderedItems]).to be_an Array
+ expect(body[:orderedItems].sort).to eq [follower_1.uri, follower_2.uri]
end
it 'returns private Cache-Control header' do
expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
end
+
+ context 'when account is permanently suspended' do
+ before do
+ account.suspend!
+ account.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ account.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
end
end
end
it 'returns http accepted' do
expect(response).to have_http_status(202)
end
+
+ context 'for a specific account' do
+ let(:account) { Fabricate(:account) }
+
+ subject(:response) { post :create, params: { account_username: account.username }, body: '{}' }
+
+ context 'when account is permanently suspended' do
+ before do
+ account.suspend!
+ account.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ account.suspend!
+ end
+
+ it 'returns http accepted' do
+ expect(response).to have_http_status(202)
+ end
+ end
+ end
end
context 'with Collection-Synchronization header' do
end
it 'does not set sessions' do
+ response
expect(session).to be_empty
end
context 'without signature' do
let(:remote_account) { nil }
- before do
- get :show, params: { account_username: account.username, page: page }
- end
+ subject(:response) { get :show, params: { account_username: account.username, page: page } }
+ subject(:body) { body_as_json }
context 'with page not requested' do
let(:page) { nil }
end
it 'returns totalItems' do
- json = body_as_json
- expect(json[:totalItems]).to eq 4
+ expect(body[:totalItems]).to eq 4
end
it_behaves_like 'cachable response'
+
+ context 'when account is permanently suspended' do
+ before do
+ account.suspend!
+ account.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ account.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
end
context 'with page requested' do
end
it 'returns orderedItems with public or unlisted statuses' do
- json = body_as_json
- expect(json[:orderedItems]).to be_an Array
- expect(json[:orderedItems].size).to eq 2
- expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+ expect(body[:orderedItems]).to be_an Array
+ expect(body[:orderedItems].size).to eq 2
+ expect(body[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
end
it_behaves_like 'cachable response'
+
+ context 'when account is permanently suspended' do
+ before do
+ account.suspend!
+ account.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ account.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
end
end
end
it 'does not set sessions' do
+ response
expect(session).to be_empty
end
describe 'GET #index' do
context 'with no signature' do
- before do
- get :index, params: { account_username: status.account.username, status_id: status.id }
+ subject(:response) { get :index, params: { account_username: status.account.username, status_id: status.id } }
+ subject(:body) { body_as_json }
+
+ context 'when account is permanently suspended' do
+ let(:parent_visibility) { :public }
+
+ before do
+ status.account.suspend!
+ status.account.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ let(:parent_visibility) { :public }
+
+ before do
+ status.account.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
end
context 'when status is public' do
it_behaves_like 'cachable response'
it 'returns items with account\'s own replies' do
- json = body_as_json
-
- expect(json[:first]).to be_a Hash
- expect(json[:first][:items]).to be_an Array
- expect(json[:first][:items].size).to eq 1
- expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
+ expect(body[:first]).to be_a Hash
+ expect(body[:first][:items]).to be_an Array
+ expect(body[:first][:items].size).to eq 1
+ expect(body[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
end
end
describe 'POST #unsuspend' do
before do
- account.touch(:suspended_at)
+ account.suspend!
post :unsuspend, params: { id: account.id }
end
context 'when format is html' do
subject(:response) { get :index, params: { account_username: alice.username, format: :html } }
+ context 'when account is permanently suspended' do
+ before do
+ alice.suspend!
+ alice.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ alice.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
it 'assigns follows' do
expect(response).to have_http_status(200)
expect(body['totalItems']).to eq 2
expect(body['partOf']).to be_present
end
+
+ context 'when account is permanently suspended' do
+ before do
+ alice.suspend!
+ alice.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ alice.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
end
context 'without page' do
expect(body['totalItems']).to eq 2
expect(body['partOf']).to be_blank
end
+
+ context 'when account is permanently suspended' do
+ before do
+ alice.suspend!
+ alice.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ alice.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
end
end
end
context 'when format is html' do
subject(:response) { get :index, params: { account_username: alice.username, format: :html } }
+ context 'when account is permanently suspended' do
+ before do
+ alice.suspend!
+ alice.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ alice.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
it 'assigns follows' do
expect(response).to have_http_status(200)
expect(body['totalItems']).to eq 2
expect(body['partOf']).to be_present
end
+
+ context 'when account is permanently suspended' do
+ before do
+ alice.suspend!
+ alice.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ alice.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
end
context 'without page' do
expect(body['totalItems']).to eq 2
expect(body['partOf']).to be_blank
end
+
+ context 'when account is permanently suspended' do
+ before do
+ alice.suspend!
+ alice.deletion_request.destroy
+ end
+
+ it 'returns http gone' do
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'when account is temporarily suspended' do
+ before do
+ alice.suspend!
+ end
+
+ it 'returns http forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
end
end
end
end
end
- describe 'with a suspended account' do
+ context 'with a permanently suspended account' do
before do
- @account = Fabricate(:account, suspended: true)
+ @account = Fabricate(:account)
+ @account.suspend!
+ @account.deletion_request.destroy
end
- it 'returns 410 gone on GET to #new' do
+ it 'returns http gone on GET to #new' do
get :new, params: { account_username: @account.to_param }
- expect(response).to have_http_status(:gone)
+ expect(response).to have_http_status(410)
end
- it 'returns 410 gone on POST to #create' do
+ it 'returns http gone on POST to #create' do
post :create, params: { account_username: @account.to_param }
- expect(response).to have_http_status(:gone)
+ expect(response).to have_http_status(410)
+ end
+ end
+
+ context 'with a temporarily suspended account' do
+ before do
+ @account = Fabricate(:account)
+ @account.suspend!
+ end
+
+ it 'returns http forbidden on GET to #new' do
+ get :new, params: { account_username: @account.to_param }
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns http forbidden on POST to #create' do
+ post :create, params: { account_username: @account.to_param }
+
+ expect(response).to have_http_status(403)
end
end
end
let(:account) { Fabricate(:account) }
let(:status) { Fabricate(:status, account: account) }
- context 'when account is suspended' do
- let(:account) { Fabricate(:account, suspended: true) }
-
+ context 'when account is permanently suspended' do
before do
+ account.suspend!
+ account.deletion_request.destroy
+
get :show, params: { account_username: account.username, id: status.id }
end
end
end
+ context 'when account is temporarily suspended' do
+ before do
+ account.suspend!
+
+ 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') }
let(:account) { Fabricate(:account) }
let(:status) { Fabricate(:status, account: account) }
- context 'when account is suspended' do
- let(:account) { Fabricate(:account, suspended: true) }
-
+ context 'when account is permanently suspended' do
before do
+ account.suspend!
+ account.deletion_request.destroy
+
get :activity, params: { account_username: account.username, id: status.id }
end
end
end
+ context 'when account is temporarily suspended' do
+ before do
+ account.suspend!
+
+ get :activity, 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 public' do
pending
end
render_views
describe 'GET #show' do
- let(:alice) do
- Fabricate(:account, username: 'alice')
+ let(:alternate_domains) { [] }
+ let(:alice) { Fabricate(:account, username: 'alice') }
+ let(:resource) { nil }
+
+ around(:each) do |example|
+ tmp = Rails.configuration.x.alternate_domains
+ Rails.configuration.x.alternate_domains = alternate_domains
+ example.run
+ Rails.configuration.x.alternate_domains = tmp
end
- before do
- alice.private_key = <<-PEM
------BEGIN RSA PRIVATE KEY-----
-MIICXQIBAAKBgQDHgPoPJlrfMZrVcuF39UbVssa8r4ObLP3dYl9Y17Mgp5K4mSYD
-R/Y2ag58tSi6ar2zM3Ze3QYsNfTq0NqN1g89eAu0MbSjWqpOsgntRPJiFuj3hai2
-X2Im8TBrkiM/UyfTRgn8q8WvMoKbXk8Lu6nqv420eyqhhLxfUoCpxuem1QIDAQAB
-AoGBAIKsOh2eM7spVI8mdgQKheEG/iEsnPkQ2R8ehfE9JzjmSbXbqghQJDaz9NU+
-G3Uu4R31QT0VbCudE9SSA/UPFl82GeQG4QLjrSE+PSjSkuslgSXelJHfAJ+ycGax
-ajtPyiQD0e4c2loagHNHPjqK9OhHx9mFnZWmoagjlZ+mQGEpAkEA8GtqfS65IaRQ
-uVhMzpp25rF1RWOwaaa+vBPkd7pGdJEQGFWkaR/a9UkU+2C4ZxGBkJDP9FApKVQI
-RANEwN3/hwJBANRuw5+es6BgBv4PD387IJvuruW2oUtYP+Lb2Z5k77J13hZTr0db
-Oo9j1UbbR0/4g+vAcsDl4JD9c/9LrGYEpcMCQBon9Yvs+2M3lziy7JhFoc3zXIjS
-Ea1M4M9hcqe78lJYPeIH3z04o/+vlcLLgQRlmSz7NESmO/QtGkEcAezhuh0CQHji
-pzO4LeO/gXslut3eGcpiYuiZquOjToecMBRwv+5AIKd367Che4uJdh6iPcyGURvh
-IewfZFFdyZqnx20ui90CQQC1W2rK5Y30wAunOtSLVA30TLK/tKrTppMC3corjKlB
-FTX8IvYBNTbpEttc1VCf/0ccnNpfb0CrFNSPWxRj7t7D
------END RSA PRIVATE KEY-----
-PEM
-
- alice.public_key = <<-PEM
------BEGIN PUBLIC KEY-----
-MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHgPoPJlrfMZrVcuF39UbVssa8
-r4ObLP3dYl9Y17Mgp5K4mSYDR/Y2ag58tSi6ar2zM3Ze3QYsNfTq0NqN1g89eAu0
-MbSjWqpOsgntRPJiFuj3hai2X2Im8TBrkiM/UyfTRgn8q8WvMoKbXk8Lu6nqv420
-eyqhhLxfUoCpxuem1QIDAQAB
------END PUBLIC KEY-----
-PEM
-
- alice.save!
+ subject do
+ get :show, params: { resource: resource }, format: :json
end
- around(:each) do |example|
- before = Rails.configuration.x.alternate_domains
- example.run
- Rails.configuration.x.alternate_domains = before
+ shared_examples 'a successful response' do
+ it 'returns http success' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns application/jrd+json' do
+ expect(response.content_type).to eq 'application/jrd+json'
+ end
+
+ it 'returns links for the account' do
+ json = body_as_json
+ expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
+ expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
+ end
end
- it 'returns JSON when account can be found' do
- get :show, params: { resource: alice.to_webfinger_s }, format: :json
+ context 'when an account exists' do
+ let(:resource) { alice.to_webfinger_s }
- json = body_as_json
+ before do
+ subject
+ end
- expect(response).to have_http_status(200)
- expect(response.content_type).to eq 'application/jrd+json'
- expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
- expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
+ it_behaves_like 'a successful response'
end
- it 'returns http not found when account cannot be found' do
- get :show, params: { resource: 'acct:not@existing.com' }, format: :json
+ context 'when an account is temporarily suspended' do
+ let(:resource) { alice.to_webfinger_s }
- expect(response).to have_http_status(:not_found)
+ before do
+ alice.suspend!
+ subject
+ end
+
+ it_behaves_like 'a successful response'
end
- it 'returns JSON when account can be found with alternate domains' do
- Rails.configuration.x.alternate_domains = ['foo.org']
- username, = alice.to_webfinger_s.split('@')
+ context 'when an account is permanently suspended or deleted' do
+ let(:resource) { alice.to_webfinger_s }
+
+ before do
+ alice.suspend!
+ alice.deletion_request.destroy
+ subject
+ end
- get :show, params: { resource: "#{username}@foo.org" }, format: :json
+ 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' }
- json = body_as_json
+ before do
+ subject
+ end
- expect(response).to have_http_status(200)
- expect(response.content_type).to eq 'application/jrd+json'
- expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
- expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
+ it 'returns http not found' do
+ expect(response).to have_http_status(404)
+ end
end
- it 'returns http not found when account can not be found with alternate domains' do
- Rails.configuration.x.alternate_domains = ['foo.org']
- username, = alice.to_webfinger_s.split('@')
+ context 'with an alternate domain' do
+ let(:alternate_domains) { ['foo.org'] }
+
+ before do
+ subject
+ end
+
+ context 'when an account exists' do
+ let(:resource) do
+ username, = alice.to_webfinger_s.split('@')
+ "#{username}@foo.org"
+ end
+
+ it_behaves_like 'a successful response'
+ end
- get :show, params: { resource: "#{username}@bar.org" }, format: :json
+ context 'when the domain is wrong' do
+ let(:resource) do
+ username, = alice.to_webfinger_s.split('@')
+ "#{username}@bar.org"
+ end
- expect(response).to have_http_status(:not_found)
+ it 'returns http not found' do
+ expect(response).to have_http_status(404)
+ end
+ end
end
- it 'returns http bad request when not given a resource parameter' do
- get :show, params: { }, format: :json
- expect(response).to have_http_status(:bad_request)
+ context 'with no resource parameter' do
+ let(:resource) { nil }
+
+ before do
+ subject
+ end
+
+ it 'returns http bad request' do
+ expect(response).to have_http_status(400)
+ end
end
- it 'returns http bad request when given a nonsense parameter' do
- get :show, params: { resource: 'df/:dfkj' }
- expect(response).to have_http_status(:bad_request)
+ context 'with a nonsense parameter' do
+ let(:resource) { 'df/:dfkj' }
+
+ before do
+ subject
+ end
+
+ it 'returns http bad request' do
+ expect(response).to have_http_status(400)
+ end
end
end
end
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:john) { Fabricate(:user).account }
+ let(:alice) { Fabricate(:user).account }
- permissions :index?, :show?, :unsuspend?, :unsensitive?, :unsilence?, :remove_avatar?, :remove_header? do
+ permissions :index? do
context 'staff' do
it 'permits' do
expect(subject).to permit(admin)
end
end
+ permissions :show?, :unsilence?, :unsensitive?, :remove_avatar?, :remove_header? do
+ context 'staff' do
+ it 'permits' do
+ expect(subject).to permit(admin, alice)
+ end
+ end
+
+ context 'not staff' do
+ it 'denies' do
+ expect(subject).to_not permit(john, alice)
+ end
+ end
+ end
+
+ permissions :unsuspend? do
+ before do
+ alice.suspend!
+ end
+
+ context 'staff' do
+ it 'permits' do
+ expect(subject).to permit(admin, alice)
+ end
+ end
+
+ context 'not staff' do
+ it 'denies' do
+ expect(subject).to_not permit(john, alice)
+ end
+ end
+ end
+
permissions :redownload?, :subscribe?, :unsubscribe? do
context 'admin' do
it 'permits' do
expect(ProofProvider::Keybase::Worker).to have_received(:perform_async)
end
end
+
+ context 'when account is not suspended' do
+ let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com') }
+
+ let(:payload) do
+ {
+ id: 'https://foo.test',
+ type: 'Actor',
+ inbox: 'https://foo.test/inbox',
+ suspended: true,
+ }.with_indifferent_access
+ end
+
+ before do
+ allow(Admin::SuspensionWorker).to receive(:perform_async)
+ end
+
+ subject { described_class.new.call('alice', 'example.com', payload) }
+
+ it 'suspends account remotely' do
+ expect(subject.suspended?).to be true
+ expect(subject.suspension_origin_remote?).to be true
+ end
+
+ it 'queues suspension worker' do
+ subject
+ expect(Admin::SuspensionWorker).to have_received(:perform_async)
+ end
+ end
+
+ context 'when account is suspended' do
+ let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com', display_name: '') }
+
+ let(:payload) do
+ {
+ id: 'https://foo.test',
+ type: 'Actor',
+ inbox: 'https://foo.test/inbox',
+ suspended: false,
+ name: 'Hoge',
+ }.with_indifferent_access
+ end
+
+ before do
+ allow(Admin::UnsuspensionWorker).to receive(:perform_async)
+
+ account.suspend!(origin: suspension_origin)
+ end
+
+ subject { described_class.new.call('alice', 'example.com', payload) }
+
+ context 'locally' do
+ let(:suspension_origin) { :local }
+
+ it 'does not unsuspend it' do
+ expect(subject.suspended?).to be true
+ end
+
+ it 'does not update any attributes' do
+ expect(subject.display_name).to_not eq 'Hoge'
+ end
+ end
+
+ context 'remotely' do
+ let(:suspension_origin) { :remote }
+
+ it 'unsuspends it' do
+ expect(subject.suspended?).to be false
+ end
+
+ it 'queues unsuspension worker' do
+ subject
+ expect(Admin::UnsuspensionWorker).to have_received(:perform_async)
+ end
+
+ it 'updates attributes' do
+ expect(subject.display_name).to eq 'Hoge'
+ end
+ end
+ end
end
subject { described_class.new }
describe '#call' do
- context 'when actor is the sender'
+ context 'when actor is suspended' do
+ before do
+ actor.suspend!(origin: :remote)
+ end
+
+ %w(Accept Add Announce Block Create Flag Follow Like Move Remove).each do |activity_type|
+ context "with #{activity_type} activity" do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'foo',
+ type: activity_type,
+ actor: ActivityPub::TagManager.instance.uri_for(actor),
+ }
+ end
+
+ it 'does not process payload' do
+ expect(ActivityPub::Activity).not_to receive(:factory)
+ subject.call(json, actor)
+ end
+ end
+ end
+
+ %w(Delete Reject Undo Update).each do |activity_type|
+ context "with #{activity_type} activity" do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'foo',
+ type: activity_type,
+ actor: ActivityPub::TagManager.instance.uri_for(actor),
+ }
+ end
+
+ it 'processes the payload' do
+ expect(ActivityPub::Activity).to receive(:factory)
+ subject.call(json, actor)
+ end
+ end
+ end
+ end
+
context 'when actor differs from sender' do
let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') }
stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt'))
stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt'))
stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404)
+ stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:hoge@example.com').to_return(status: 410)
end
- it 'raises error if no such user can be resolved via webfinger' do
+ it 'returns nil if no such user can be resolved via webfinger' do
expect(subject.call('catsrgr8@quitter.no')).to be_nil
end
- it 'raises error if the domain does not have webfinger' do
+ it 'returns nil if the domain does not have webfinger' do
expect(subject.call('catsrgr8@example.com')).to be_nil
end
+ context 'when webfinger returns http gone' do
+ context 'for a previously known account' do
+ before do
+ Fabricate(:account, username: 'hoge', domain: 'example.com', last_webfingered_at: nil)
+ allow(AccountDeletionWorker).to receive(:perform_async)
+ end
+
+ it 'returns nil' do
+ expect(subject.call('hoge@example.com')).to be_nil
+ end
+
+ it 'queues account deletion worker' do
+ subject.call('hoge@example.com')
+ expect(AccountDeletionWorker).to have_received(:perform_async)
+ end
+ end
+
+ context 'for a previously unknown account' do
+ it 'returns nil' do
+ expect(subject.call('hoge@example.com')).to be_nil
+ end
+ end
+ end
+
context 'with an ActivityPub account' do
it 'returns new remote account' do
account = subject.call('foo@ap.example.com')