diff --git a/spec/requests/activitypub/inboxes_controller_spec.rb b/spec/requests/activitypub/inboxes_controller_spec.rb index 1fc5b889de..adb75561c0 100644 --- a/spec/requests/activitypub/inboxes_controller_spec.rb +++ b/spec/requests/activitypub/inboxes_controller_spec.rb @@ -3,6 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::InboxesController do + let!(:current_datetime) { 'Wed, 20 Dec 2023 10:00:00 GMT' } let!(:remote_actor_keypair) do OpenSSL::PKey.read(<<~PEM_TEXT) -----BEGIN RSA PRIVATE KEY----- @@ -34,25 +35,27 @@ RSpec.describe ActivityPub::InboxesController do -----END RSA PRIVATE KEY----- PEM_TEXT end - let(:remote_actor_original_username) { 'original_username' } - let(:remote_actor) do + let!(:remote_actor_inbox_url) { 'https://remote.domain/users/bob/inbox' } + let!(:remote_actor_original_username) { 'original_username' } + let!(:remote_actor) do Fabricate(:account, domain: 'remote.domain', uri: 'https://remote.domain/users/bob', private_key: nil, public_key: remote_actor_keypair.public_key.to_pem, username: remote_actor_original_username, - protocol: 1) # activitypub + protocol: 1, # activitypub + inbox_url: remote_actor_inbox_url) end - let(:local_actor) { Fabricate(:account) } - let(:base_headers) do + let!(:local_actor) { Fabricate(:account) } + let!(:base_headers) do { 'Host' => 'www.remote.domain', - 'Date' => 'Wed, 20 Dec 2023 10:00:00 GMT', + 'Date' => current_datetime, } end - let(:note_content) { 'Note from remote actor' } - let(:object_json) do + let!(:note_content) { 'note from remote actor' } + let!(:object_json) do { id: 'https://remote.domain/activities/objects/1', type: 'Note', @@ -60,46 +63,21 @@ RSpec.describe ActivityPub::InboxesController do to: ActivityPub::TagManager.instance.uri_for(local_actor), } end - let(:json) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'https://remote.domain/activities/create/1', - type: 'Create', - actor: remote_actor_json[:id], - object: object_json, - }.with_indifferent_access - end - let(:digest_header) { digest_value(json.to_json) } - let(:signature_header) do - build_signature_string( - remote_actor_keypair, - 'https://remote.domain/users/bob#main-key', - "post /users/#{local_actor.username}/inbox", - base_headers.merge( - 'Digest' => digest_header - ) - ) - end - let(:headers) do - base_headers.merge( - 'Digest' => digest_header, - 'Signature' => signature_header - ) - end before do - travel_to '2023-12-20T10:00:00Z' + Sidekiq::Testing.inline! + travel_to current_datetime end context 'when remote actor username has changed' do let(:remote_actor_new_username) { 'new_username' } - let(:remote_actor_json) do + let(:updated_remote_actor_json) do { '@context': 'https://www.w3.org/ns/activitystreams', id: remote_actor.uri, type: 'Person', preferredUsername: remote_actor_new_username, - inbox: "#{remote_actor.uri}#inbox", + inbox: remote_actor.inbox_url, publicKey: { id: "#{remote_actor.uri}#main-key", owner: remote_actor.uri, @@ -111,7 +89,7 @@ RSpec.describe ActivityPub::InboxesController do before do stub_request(:get, 'https://remote.domain/users/bob#main-key') .to_return( - body: remote_actor_json.to_json, + body: updated_remote_actor_json.to_json, headers: { 'Content-Type' => 'application/activity+json', }, @@ -119,7 +97,7 @@ RSpec.describe ActivityPub::InboxesController do ) stub_request(:get, 'https://remote.domain/users/bob') .to_return( - body: remote_actor_json.to_json, + body: updated_remote_actor_json.to_json, headers: { 'Content-Type' => 'application/activity+json', }, @@ -127,26 +105,195 @@ RSpec.describe ActivityPub::InboxesController do ) end - it 'successfuly processes note' do - Sidekiq::Testing.inline! - post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers - expect(response).to have_http_status(202) - expect(Status.exists?(uri: object_json[:id])).to be(true) - # we don't expect the remote actor username to change - expect(remote_actor.reload.username).to eq(remote_actor_original_username) + context 'with a create note' do + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://remote.domain/activities/create/1', + type: 'Create', + actor: remote_actor.uri, + object: object_json, + }.with_indifferent_access + end + let(:digest_header) { digest_value(json.to_json) } + let(:signature_header) do + build_signature_string( + remote_actor_keypair, + 'https://remote.domain/users/bob#main-key', + "post /users/#{local_actor.username}/inbox", + base_headers.merge( + 'Digest' => digest_header + ) + ) + end + let(:headers) do + base_headers.merge( + 'Digest' => digest_header, + 'Signature' => signature_header + ) + end + + it 'creates the note' do + post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers + expect(response).to have_http_status(202) + expect(Status.exists?(uri: object_json[:id])).to be(true) + end + + it 'does not change the local record of the remote actor' do + post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers + expect(remote_actor.reload.username).to eq(remote_actor_original_username) + end + end + + context 'with an update note' do + let!(:status) do + Fabricate(:status, + uri: object_json[:id], + text: note_content, + account: remote_actor) + end + let(:updated_content) { 'updated note from remote actor' } + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://remote.domain/activities/update/1', + type: 'Update', + actor: remote_actor.uri, + object: object_json.merge( + updated: current_datetime, + content: updated_content + ), + }.with_indifferent_access + end + let(:digest_header) { digest_value(json.to_json) } + let(:signature_header) do + build_signature_string( + remote_actor_keypair, + 'https://remote.domain/users/bob#main-key', + "post /users/#{local_actor.username}/inbox", + base_headers.merge( + 'Digest' => digest_header + ) + ) + end + let(:headers) do + base_headers.merge( + 'Digest' => digest_header, + 'Signature' => signature_header + ) + end + + it 'updates the Note' do + post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers + expect(response).to have_http_status(202) + expect(Status.exists?(id: status.id, text: updated_content)).to be(true) + end + + it 'does not change the local record of the remote actor' do + post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers + expect(remote_actor.reload.username).to eq(remote_actor_original_username) + end + end + + context 'with an update actor' do + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://remote.domain/activities/update/1', + type: 'Update', + actor: remote_actor.uri, + object: updated_remote_actor_json, + }.with_indifferent_access + end + let(:digest_header) { digest_value(json.to_json) } + let(:signature_header) do + build_signature_string( + remote_actor_keypair, + 'https://remote.domain/users/bob#main-key', + "post /users/#{local_actor.username}/inbox", + base_headers.merge( + 'Digest' => digest_header + ) + ) + end + let(:headers) do + base_headers.merge( + 'Digest' => digest_header, + 'Signature' => signature_header + ) + end + + it 'does not increase the number of accounts' do + expect do + post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers + end.to(not_change { Account.count }) + end + + it 'does not update the remote actors username' do + post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers + expect(response).to have_http_status(202) + expect(remote_actor.reload.username).to eq(remote_actor_original_username) + end + end + + context 'with a follow' do + context 'when the remote actor is already following the actor' do + before do + remote_actor.follow!(local_actor) + stub_request(:post, remote_actor_inbox_url) + end + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://remote.domain/activities/update/1', + type: 'Follow', + actor: remote_actor.uri, + object: ActivityPub::TagManager.instance.uri_for(local_actor), + }.with_indifferent_access + end + let(:digest_header) { digest_value(json.to_json) } + let(:signature_header) do + build_signature_string( + remote_actor_keypair, + 'https://remote.domain/users/bob#main-key', + "post /users/#{local_actor.username}/inbox", + base_headers.merge( + 'Digest' => digest_header + ) + ) + end + let(:headers) do + base_headers.merge( + 'Digest' => digest_header, + 'Signature' => signature_header + ) + end + + it 'does not increase the number of accounts' do + expect do + post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers + end.to(not_change { Account.count }) + end + + it 'does not increase the number of follows' do + expect do + post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers + end.to(not_change { Follow.count }) + end + + it 'posts an acceptance to the remote actors inbox' do + post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers + expect( + a_request(:post, remote_actor_inbox_url) + ).to have_been_made.once + end + + it 'does not change the local record of the remote actor' do + post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers + expect(remote_actor.reload.username).to eq(remote_actor_original_username) + end + end end end - - def build_signature_string(keypair, key_id, request_target, headers) - algorithm = 'rsa-sha256' - signed_headers = headers.merge({ '(request-target)' => request_target }) - signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") - signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) - - "keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\"" - end - - def digest_value(body) - "SHA-256=#{Digest::SHA256.base64digest(body)}" - end end diff --git a/spec/requests/signature_verification_spec.rb b/spec/requests/signature_verification_spec.rb index 128e7c0787..770c399042 100644 --- a/spec/requests/signature_verification_spec.rb +++ b/spec/requests/signature_verification_spec.rb @@ -382,17 +382,4 @@ RSpec.describe 'signature verification concern' do alias_method :signature_required, :success end end - - def digest_value(body) - "SHA-256=#{Digest::SHA256.base64digest(body)}" - end - - def build_signature_string(keypair, key_id, request_target, headers) - algorithm = 'rsa-sha256' - signed_headers = headers.merge({ '(request-target)' => request_target }) - signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") - signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) - - "keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\"" - end end diff --git a/spec/support/signed_request_helpers.rb b/spec/support/signed_request_helpers.rb index 8a52179cae..25e89f1ebf 100644 --- a/spec/support/signed_request_helpers.rb +++ b/spec/support/signed_request_helpers.rb @@ -1,6 +1,19 @@ # frozen_string_literal: true module SignedRequestHelpers + def digest_value(body) + "SHA-256=#{Digest::SHA256.base64digest(body)}" + end + + def build_signature_string(keypair, key_id, request_target, headers) + algorithm = 'rsa-sha256' + signed_headers = headers.merge({ '(request-target)' => request_target }) + signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") + signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + + "keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\"" + end + def get(path, headers: nil, sign_with: nil, **args) return super(path, headers: headers, **args) if sign_with.nil?