diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb deleted file mode 100644 index feca543cb7..0000000000 --- a/spec/controllers/activitypub/inboxes_controller_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::InboxesController do - let(:remote_account) { nil } - - before do - allow(controller).to receive(:signed_request_actor).and_return(remote_account) - end - - describe 'POST #create' do - context 'with signature' do - let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) } - - before do - post :create, body: '{}' - end - - it 'returns http accepted' do - expect(response).to have_http_status(202) - end - - context 'with a specific account' do - subject(:response) { post :create, params: { account_username: account.username }, body: '{}' } - - let(:account) { Fabricate(:account) } - - 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 - let(:remote_account) { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) } - let(:synchronization_collection) { remote_account.followers_url } - let(:synchronization_url) { 'https://example.com/followers-for-domain' } - let(:synchronization_hash) { 'somehash' } - let(:synchronization_header) { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" } - - before do - allow(ActivityPub::FollowersSynchronizationWorker).to receive(:perform_async).and_return(nil) - allow(remote_account).to receive(:local_followers_hash).and_return('somehash') - - request.headers['Collection-Synchronization'] = synchronization_header - post :create, body: '{}' - end - - context 'with mismatching target collection' do - let(:synchronization_collection) { 'https://example.com/followers2' } - - it 'does not start a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async) - end - end - - context 'with mismatching domain in partial collection attribute' do - let(:synchronization_url) { 'https://example.org/followers' } - - it 'does not start a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async) - end - end - - context 'with matching digest' do - it 'does not start a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async) - end - end - - context 'with mismatching digest' do - let(:synchronization_hash) { 'wronghash' } - - it 'starts a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to have_received(:perform_async) - end - end - - it 'returns http accepted' do - expect(response).to have_http_status(202) - end - end - - context 'without signature' do - before do - post :create, body: '{}' - end - - it 'returns http not authorized' do - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/requests/activitypub/inboxes_spec.rb b/spec/requests/activitypub/inboxes_spec.rb new file mode 100644 index 0000000000..b21881b10f --- /dev/null +++ b/spec/requests/activitypub/inboxes_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'ActivityPub Inboxes' do + let(:remote_account) { nil } + + describe 'POST #create' do + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) } + + context 'without a named account' do + subject { post inbox_path, params: {}.to_json, sign_with: remote_account } + + it 'returns http accepted' do + subject + + expect(response) + .to have_http_status(202) + end + end + + context 'with a specific account' do + subject { post account_inbox_path(account_username: account.username), params: {}.to_json, sign_with: remote_account } + + let(:account) { Fabricate(:account) } + + context 'when account is permanently suspended' do + before do + account.suspend! + account.deletion_request.destroy + end + + it 'returns http gone' do + subject + + expect(response) + .to have_http_status(410) + end + end + + context 'when account is temporarily suspended' do + before { account.suspend! } + + it 'returns http accepted' do + subject + + expect(response) + .to have_http_status(202) + end + end + end + end + + context 'with Collection-Synchronization header' do + subject { post inbox_path, params: {}.to_json, headers: { 'Collection-Synchronization' => synchronization_header }, sign_with: remote_account } + + let(:remote_account) { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) } + let(:synchronization_collection) { remote_account.followers_url } + let(:synchronization_url) { 'https://example.com/followers-for-domain' } + let(:synchronization_hash) { 'somehash' } + let(:synchronization_header) { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" } + + before do + stub_follow_sync_worker + stub_followers_hash + end + + context 'with mismatching target collection' do + let(:synchronization_collection) { 'https://example.com/followers2' } + + it 'does not start a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to_not have_received(:perform_async) + end + end + + context 'with mismatching domain in partial collection attribute' do + let(:synchronization_url) { 'https://example.org/followers' } + + it 'does not start a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to_not have_received(:perform_async) + end + end + + context 'with matching digest' do + it 'does not start a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to_not have_received(:perform_async) + end + end + + context 'with mismatching digest' do + let(:synchronization_hash) { 'wronghash' } + + it 'starts a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to have_received(:perform_async) + end + end + + it 'returns http accepted' do + subject + + expect(response) + .to have_http_status(202) + end + + def stub_follow_sync_worker + allow(ActivityPub::FollowersSynchronizationWorker) + .to receive(:perform_async) + .and_return(nil) + end + + def stub_followers_hash + Rails.cache.write("followers_hash:#{remote_account.id}:local", 'somehash') # Populate value to match request + end + end + + context 'without signature' do + subject { post inbox_path, params: {}.to_json } + + it 'returns http not authorized' do + subject + + expect(response) + .to have_http_status(401) + end + end + end +end diff --git a/spec/support/signed_request_helpers.rb b/spec/support/signed_request_helpers.rb index 8a52179cae..a4423af748 100644 --- a/spec/support/signed_request_helpers.rb +++ b/spec/support/signed_request_helpers.rb @@ -18,4 +18,24 @@ module SignedRequestHelpers super(path, headers: headers, **args) end + + def post(path, headers: nil, sign_with: nil, **args) + return super(path, headers: headers, **args) if sign_with.nil? + + headers ||= {} + headers['Date'] = Time.now.utc.httpdate + headers['Host'] = Rails.configuration.x.local_domain + headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(args[:params].to_s)}" + + signed_headers = headers.merge('(request-target)' => "post #{path}").slice('(request-target)', 'Host', 'Date', 'Digest') + + key_id = ActivityPub::TagManager.instance.key_uri_for(sign_with) + keypair = sign_with.keypair + signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") + signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + + headers['Signature'] = "keyId=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\"" + + super(path, headers: headers, **args) + end end