activitypub-academy/spec/services/resolve_account_service_spec.rb
Claire 1145dbd327
Improve error reporting and logging when processing remote accounts (#15605)
* Add a more descriptive PrivateNetworkAddressError exception class

* Remove unnecessary exception class to rescue clause

* Remove unnecessary include to JsonLdHelper

* Give more neutral error message when too many webfinger redirects

* Remove unnecessary guard condition

* Rework how “ActivityPub::FetchRemoteAccountService” handles errors

Add “suppress_errors” keyword argument to avoid raising errors in
ActivityPub::FetchRemoteAccountService#call (default/previous behavior).

* Rework how “ActivityPub::FetchRemoteKeyService” handles errors

Add “suppress_errors” keyword argument to avoid raising errors in
ActivityPub::FetchRemoteKeyService#call (default/previous behavior).

* Fix Webfinger::RedirectError not being a subclass of Webfinger::Error

* Add suppress_errors option to ResolveAccountService

Defaults to true (to preserve previous behavior). If set to false,
errors will be raised instead of caught, allowing the caller to be
informed of what went wrong.

* Return more precise error when failing to fetch account signing AP payloads

* Add tests

* Fixes

* Refactor error handling a bit

* Fix various issues

* Add specific error when provided Digest is not 256 bits of base64-encoded data

* Please CodeClimate

* Improve webfinger error reporting
2022-09-20 23:30:26 +02:00

235 lines
9.4 KiB
Ruby

require 'rails_helper'
RSpec.describe ResolveAccountService, type: :service do
subject { described_class.new }
before do
stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404)
stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt'))
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
context 'using skip_webfinger' do
context 'when account is known' do
let!(:remote_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', protocol: 'activitypub') }
context 'when domain is banned' do
let!(:domain_block) { Fabricate(:domain_block, domain: 'ap.example.com', severity: :suspend) }
it 'does not return an account' do
expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil
end
it 'does not make a webfinger query' do
subject.call('foo@ap.example.com', skip_webfinger: true)
expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made
end
end
context 'when domain is not banned' do
it 'returns the expected account' do
expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to eq remote_account
end
it 'does not make a webfinger query' do
subject.call('foo@ap.example.com', skip_webfinger: true)
expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made
end
end
end
context 'when account is not known' do
it 'does not return an account' do
expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil
end
it 'does not make a webfinger query' do
subject.call('foo@ap.example.com', skip_webfinger: true)
expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made
end
end
end
context 'when there is an LRDD endpoint but no resolvable account' do
before do
stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404)
end
it 'returns nil' do
expect(subject.call('catsrgr8@quitter.no')).to be_nil
end
end
context 'when there is no LRDD endpoint nor resolvable account' do
before do
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com").to_return(status: 404)
end
it 'returns nil' do
expect(subject.call('catsrgr8@example.com')).to be_nil
end
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 a legitimate webfinger redirection' do
before do
webfinger = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end
it 'returns new remote account' do
account = subject.call('Foo@redirected.example.com')
expect(account.activitypub?).to eq true
expect(account.acct).to eq 'foo@ap.example.com'
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
end
end
context 'with a misconfigured redirection' do
before do
webfinger = { subject: 'acct:Foo@redirected.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end
it 'returns new remote account' do
account = subject.call('Foo@redirected.example.com')
expect(account.activitypub?).to eq true
expect(account.acct).to eq 'foo@ap.example.com'
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
end
end
context 'with too many webfinger redirections' do
before do
webfinger = { subject: 'acct:foo@evil.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
webfinger2 = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: Oj.dump(webfinger2), headers: { 'Content-Type': 'application/jrd+json' })
end
it 'does not return a new remote account' do
expect(subject.call('Foo@redirected.example.com')).to be_nil
end
end
context 'with an ActivityPub account' do
it 'returns new remote account' do
account = subject.call('foo@ap.example.com')
expect(account.activitypub?).to eq true
expect(account.domain).to eq 'ap.example.com'
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
end
context 'with multiple types' do
before do
stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-individual.txt'))
end
it 'returns new remote account' do
account = subject.call('foo@ap.example.com')
expect(account.activitypub?).to eq true
expect(account.domain).to eq 'ap.example.com'
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
expect(account.actor_type).to eq 'Person'
end
end
end
context 'with an already-known actor changing acct: URI' do
let!(:duplicate) { Fabricate(:account, username: 'foo', domain: 'old.example.com', uri: 'https://ap.example.com/users/foo') }
let!(:status) { Fabricate(:status, account: duplicate, text: 'foo') }
it 'returns new remote account' do
account = subject.call('foo@ap.example.com')
expect(account.activitypub?).to eq true
expect(account.domain).to eq 'ap.example.com'
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
expect(account.uri).to eq 'https://ap.example.com/users/foo'
end
it 'merges accounts' do
account = subject.call('foo@ap.example.com')
expect(status.reload.account_id).to eq account.id
expect(Account.where(uri: account.uri).count).to eq 1
end
end
context 'with an already-known acct: URI changing ActivityPub id' do
let!(:old_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', uri: 'https://old.example.com/users/foo', last_webfingered_at: nil) }
let!(:status) { Fabricate(:status, account: old_account, text: 'foo') }
it 'returns new remote account' do
account = subject.call('foo@ap.example.com')
expect(account.activitypub?).to eq true
expect(account.domain).to eq 'ap.example.com'
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
expect(account.uri).to eq 'https://ap.example.com/users/foo'
end
end
it 'processes one remote account at a time using locks' do
wait_for_start = true
fail_occurred = false
return_values = Concurrent::Array.new
# Preload classes that throw circular dependency errors in threads
Account
TagManager
DomainBlock
threads = Array.new(5) do
Thread.new do
true while wait_for_start
begin
return_values << described_class.new.call('foo@ap.example.com')
rescue ActiveRecord::RecordNotUnique
fail_occurred = true
ensure
RedisConfiguration.pool.checkin if Thread.current[:redis]
end
end
end
wait_for_start = false
threads.each(&:join)
expect(fail_occurred).to be false
expect(return_values).to_not include(nil)
end
end