From 922d26f9768b206127b13642f476ca959e9e2dc9 Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Tue, 10 Nov 2015 21:34:25 +0300 Subject: [PATCH] Implement integration tests for the federation messages receive feature These are some initial tests, more to come. It tests some features of Request, StatusMessage, Comment, Like, Participation, Retraction, SignedRetraction, RelayableRetraction entities receive process. --- app/models/person.rb | 30 ++++ config/initializers/diaspora_federation.rb | 58 +++---- spec/federation_callbacks_spec.rb | 119 ++++++++++++++ .../federation_messages_generation.rb | 125 +++++++++++++++ .../federation/receive_federation_messages.rb | 146 ++++++++++++++++++ .../federation/shared_receive_relayable.rb | 31 ++++ .../federation/shared_receive_retraction.rb | 44 ++++++ 7 files changed, 525 insertions(+), 28 deletions(-) create mode 100644 spec/integration/federation/federation_messages_generation.rb create mode 100644 spec/integration/federation/receive_federation_messages.rb create mode 100644 spec/integration/federation/shared_receive_relayable.rb create mode 100644 spec/integration/federation/shared_receive_retraction.rb diff --git a/app/models/person.rb b/app/models/person.rb index f338daf3b..03c929330 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -317,6 +317,36 @@ class Person < ActiveRecord::Base self end + def webfinger + DiasporaFederation::Discovery::WebFinger.new( + acct_uri: "acct:#{diaspora_handle}", + alias_url: AppConfig.url_to("/people/#{guid}"), + hcard_url: AppConfig.url_to(DiasporaFederation::Engine.routes.url_helpers.hcard_path(guid)), + seed_url: AppConfig.pod_uri, + profile_url: profile_url, + atom_url: atom_url, + salmon_url: receive_url, + guid: guid, + public_key: serialized_public_key + ) + end + + def hcard + DiasporaFederation::Discovery::HCard.new( + guid: guid, + nickname: username, + full_name: "#{profile.first_name} #{profile.last_name}".strip, + url: AppConfig.pod_uri, + photo_large_url: image_url, + photo_medium_url: image_url(:thumb_medium), + photo_small_url: image_url(:thumb_small), + public_key: serialized_public_key, + searchable: searchable, + first_name: profile.first_name, + last_name: profile.last_name + ) + end + protected def clean_url diff --git a/config/initializers/diaspora_federation.rb b/config/initializers/diaspora_federation.rb index f15dcf5cf..b9f3670d0 100644 --- a/config/initializers/diaspora_federation.rb +++ b/config/initializers/diaspora_federation.rb @@ -8,38 +8,12 @@ DiasporaFederation.configure do |config| config.define_callbacks do on :fetch_person_for_webfinger do |handle| person = Person.find_local_by_diaspora_handle(handle) - if person - DiasporaFederation::Discovery::WebFinger.new( - acct_uri: "acct:#{person.diaspora_handle}", - alias_url: AppConfig.url_to("/people/#{person.guid}"), - hcard_url: AppConfig.url_to(DiasporaFederation::Engine.routes.url_helpers.hcard_path(person.guid)), - seed_url: AppConfig.pod_uri, - profile_url: person.profile_url, - atom_url: person.atom_url, - salmon_url: person.receive_url, - guid: person.guid, - public_key: person.serialized_public_key - ) - end + person.webfinger if person end on :fetch_person_for_hcard do |guid| person = Person.find_local_by_guid(guid) - if person - DiasporaFederation::Discovery::HCard.new( - guid: person.guid, - nickname: person.username, - full_name: "#{person.profile.first_name} #{person.profile.last_name}".strip, - url: AppConfig.pod_uri, - photo_large_url: person.image_url, - photo_medium_url: person.image_url(:thumb_medium), - photo_small_url: person.image_url(:thumb_small), - public_key: person.serialized_public_key, - searchable: person.searchable, - first_name: person.profile.first_name, - last_name: person.profile.last_name - ) - end + person.hcard if person end on :save_person_after_webfinger do |person| @@ -61,5 +35,33 @@ DiasporaFederation.configure do |config| person_entity.save! end + + on :fetch_private_key_by_diaspora_id do |diaspora_id| + key = Person.where(diaspora_handle: diaspora_id).joins(:owner).pluck(:serialized_private_key).first + OpenSSL::PKey::RSA.new key unless key.nil? + end + + on :fetch_author_private_key_by_entity_guid do |entity_type, guid| + key = entity_type.constantize.where(guid: guid).joins(author: :owner).pluck(:serialized_private_key).first + OpenSSL::PKey::RSA.new key unless key.nil? + end + + on :fetch_public_key_by_diaspora_id do |diaspora_id| + key = Person.where(diaspora_handle: diaspora_id).pluck(:serialized_public_key).first + OpenSSL::PKey::RSA.new key unless key.nil? + end + + on :fetch_author_public_key_by_entity_guid do |entity_type, guid| + key = entity_type.constantize.where(guid: guid).joins(:author).pluck(:serialized_public_key).first + OpenSSL::PKey::RSA.new key unless key.nil? + end + + on :entity_author_is_local? do |entity_type, guid| + entity_type.constantize.where(guid: guid).joins(author: :owner).exists? + end + + on :fetch_entity_author_id_by_guid do |entity_type, guid| + entity_type.constantize.where(guid: guid).joins(:author).pluck(:diaspora_handle).first + end end end diff --git a/spec/federation_callbacks_spec.rb b/spec/federation_callbacks_spec.rb index a2f3432b5..7c5cd9640 100644 --- a/spec/federation_callbacks_spec.rb +++ b/spec/federation_callbacks_spec.rb @@ -1,4 +1,5 @@ require "spec_helper" +require "diaspora_federation/test" describe "diaspora federation callbacks" do describe ":fetch_person_for_webfinger" do @@ -147,4 +148,122 @@ describe "diaspora federation callbacks" do end end end + + def create_a_local_person + FactoryGirl.create(:user).person + end + + def create_a_remote_person + FactoryGirl.create(:person) + end + + def create_post_by_a_local_person + FactoryGirl.create(:status_message, author: create_a_local_person).guid + end + + def create_post_by_a_remote_person + FactoryGirl.create(:status_message, author: create_a_remote_person).guid + end + + describe :fetch_private_key_by_diaspora_id do + it "returns a private key for a local user" do + expect( + DiasporaFederation.callbacks.trigger(described_class, create_a_local_person.diaspora_handle) + ).not_to be_nil + end + + it "returns nil for a remote user" do + expect( + DiasporaFederation.callbacks.trigger(described_class, create_a_remote_person.diaspora_handle) + ).to be_nil + end + + it "returns nil for an unknown id" do + expect( + DiasporaFederation.callbacks.trigger(described_class, FactoryGirl.generate(:diaspora_id)) + ).to be_nil + end + end + + describe :fetch_author_private_key_by_entity_guid do + it "returns a private key for a post by a local user" do + expect( + DiasporaFederation.callbacks.trigger(described_class, "Post", create_post_by_a_local_person) + ).not_to be_nil + end + + it "returns nil for a post by a remote user" do + expect( + DiasporaFederation.callbacks.trigger(described_class, "Post", create_post_by_a_remote_person) + ).to be_nil + end + + it "returns nil for an unknown post" do + expect( + DiasporaFederation.callbacks.trigger(described_class, "Post", FactoryGirl.generate(:guid)) + ).to be_nil + end + end + + describe :fetch_public_key_by_diaspora_id do + it "returns a public key for a person" do + expect( + DiasporaFederation.callbacks.trigger(described_class, create_a_remote_person.diaspora_handle) + ).not_to be_nil + end + + it "returns nil for an unknown person" do + expect( + DiasporaFederation.callbacks.trigger(described_class, FactoryGirl.generate(:diaspora_id)) + ).to be_nil + end + end + + describe :fetch_author_public_key_by_entity_guid do + it "returns a public key for a known post" do + expect( + DiasporaFederation.callbacks.trigger(described_class, "Post", create_post_by_a_remote_person) + ).not_to be_nil + end + + it "returns nil for an unknown post" do + expect( + DiasporaFederation.callbacks.trigger(described_class, "Post", FactoryGirl.generate(:guid)) + ).to be_nil + end + end + + describe :entity_author_is_local? do + it "returns true for a post by a local user" do + expect( + DiasporaFederation.callbacks.trigger(described_class, "Post", create_post_by_a_local_person) + ).to be(true) + end + + it "returns false for a post by a remote user" do + expect( + DiasporaFederation.callbacks.trigger(described_class, "Post", create_post_by_a_remote_person) + ).to be(false) + end + + it "returns false for a unknown post" do + expect( + DiasporaFederation.callbacks.trigger(described_class, "Post", FactoryGirl.generate(:diaspora_id)) + ).to be(false) + end + end + + describe :fetch_entity_author_id_by_guid do + it "returns id for a existing guid" do + expect( + DiasporaFederation.callbacks.trigger(described_class, "Post", create_post_by_a_remote_person) + ).not_to be_nil + end + + it "returns nil for a non-existing guid" do + expect( + DiasporaFederation.callbacks.trigger(described_class, "Post", FactoryGirl.generate(:guid)) + ).to be_nil + end + end end diff --git a/spec/integration/federation/federation_messages_generation.rb b/spec/integration/federation/federation_messages_generation.rb new file mode 100644 index 000000000..6dff58e40 --- /dev/null +++ b/spec/integration/federation/federation_messages_generation.rb @@ -0,0 +1,125 @@ +def generate_xml(entity, remote_user, user) + DiasporaFederation::Salmon::EncryptedSlap.generate_xml( + remote_user.diaspora_handle, + OpenSSL::PKey::RSA.new(remote_user.encryption_key), + entity, + OpenSSL::PKey::RSA.new(user.encryption_key) + ) +end + +def generate_status_message + @entity = FactoryGirl.build( + :status_message_entity, + diaspora_id: @remote_user.diaspora_handle, + public: false + ) + + generate_xml(@entity, @remote_user, @user) +end + +def generate_forged_status_message + substitute_wrong_key(@remote_user, 1) + generate_status_message +end + +def mock_private_key_for_user(user) + expect(DiasporaFederation.callbacks).to receive(:trigger) + .with(:fetch_private_key_by_diaspora_id, user.person.diaspora_handle) + .once + .and_return(user.encryption_key) +end + +def retraction_mock_callbacks(entity, sender) + return unless [ + DiasporaFederation::Entities::SignedRetraction, + DiasporaFederation::Entities::RelayableRetraction + ].include?(entity.class) + + mock_private_key_for_user(sender) + + allow(DiasporaFederation.callbacks).to receive(:trigger) + .with( + :fetch_entity_author_id_by_guid, + entity.target_type, + entity.target_guid + ) + .once + .and_return(sender.encryption_key) +end + +def generate_retraction(entity_name, target_object, sender=@remote_user) + @entity = FactoryGirl.build( + entity_name, + diaspora_id: sender.diaspora_handle, + target_guid: target_object.guid, + target_type: target_object.class.to_s + ) + + retraction_mock_callbacks(@entity, sender) + + generate_xml(@entity, sender, @user) +end + +def generate_forged_retraction(entity_name, target_object, sender=@remote_user) + times = 1 + if %i(signed_retraction_entity relayable_retraction_entity).include?(entity_name) + times += 2 + end + + substitute_wrong_key(sender, times) + generate_retraction(entity_name, target_object, sender) +end + +def generate_relayable_local_parent(entity_name) + @entity = FactoryGirl.build( + entity_name, + parent_guid: @local_message.guid, + diaspora_id: @remote_user.person.diaspora_handle + ) + + mock_private_key_for_user(@remote_user) + + expect(DiasporaFederation.callbacks).to receive(:trigger) + .with(:fetch_author_private_key_by_entity_guid, "Post", kind_of(String)) + .and_return(nil) + generate_xml(@entity, @remote_user, @user) +end + +def generate_relayable_remote_parent(entity_name) + @entity = FactoryGirl.build( + entity_name, + parent_guid: @remote_message.guid, + diaspora_id: @remote_user2.person.diaspora_handle + ) + + mock_private_key_for_user(@remote_user2) + + expect(DiasporaFederation.callbacks).to receive(:trigger) + .with( + :fetch_author_private_key_by_entity_guid, + "Post", + @remote_message.guid + ) + .once + .and_return(@remote_user.encryption_key) + generate_xml(@entity, @remote_user, @user) +end + +def substitute_wrong_key(user, times_number) + expect(user).to receive(:encryption_key).exactly(times_number).times.and_return( + OpenSSL::PKey::RSA.new(1024) + ) +end + +# Checks when a remote pod wants to send us a relayable without having a key for declared diaspora ID +def generate_relayable_local_parent_wrong_author_key(entity_name) + substitute_wrong_key(@remote_user, 2) + generate_relayable_local_parent(entity_name) +end + +# Checks when a remote pod C wants to send us a relayable from its user, but bypassing the pod B where +# remote status came from. +def generate_relayable_remote_parent_wrong_parent_key(entity_name) + substitute_wrong_key(@remote_user, 2) + generate_relayable_remote_parent(entity_name) +end diff --git a/spec/integration/federation/receive_federation_messages.rb b/spec/integration/federation/receive_federation_messages.rb new file mode 100644 index 000000000..4436c109d --- /dev/null +++ b/spec/integration/federation/receive_federation_messages.rb @@ -0,0 +1,146 @@ +require "spec_helper" +require "diaspora_federation/test" +require "integration/federation/federation_messages_generation" +require "integration/federation/shared_receive_relayable" +require "integration/federation/shared_receive_retraction" + +describe Workers::ReceiveEncryptedSalmon do + before do + @user = alice + allow(User).to receive(:find) { |id| + @user if id == @user.id + } + + @remote_user = FactoryGirl.build(:user) # user on pod B + @remote_user2 = FactoryGirl.build(:user) # user on pod C + + allow_any_instance_of(DiasporaFederation::Discovery::Discovery) + .to receive(:webfinger) {|instance| + [@remote_user, @remote_user2].find {|user| user.diaspora_handle == instance.diaspora_id }.person.webfinger + } + allow_any_instance_of(DiasporaFederation::Discovery::Discovery) + .to receive(:hcard) {|instance| + [@remote_user, @remote_user2].find {|user| user.diaspora_handle == instance.diaspora_id }.person.hcard + } + + @remote_person = Person.find_or_fetch_by_identifier(@remote_user.diaspora_handle) + @remote_person2 = Person.find_or_fetch_by_identifier(@remote_user2.diaspora_handle) + end + + it "treats sharing request recive correctly" do + entity = FactoryGirl.build(:request_entity, recipient_id: @user.diaspora_handle) + + expect(Diaspora::Fetcher::Public).to receive(:queue_for).exactly(1).times + + Workers::ReceiveEncryptedSalmon.new.perform(@user.id, generate_xml(entity, @remote_user, @user)) + + expect(@user.contacts.count).to eq(2) + new_contact = @user.contacts.order(created_at: :asc).last + expect(new_contact).not_to be_nil + expect(new_contact.sharing).to eq(true) + expect(new_contact.person.diaspora_handle).to eq(@remote_user.diaspora_handle) + end + + it "doesn't save the status message if there is no sharing" do + Workers::ReceiveEncryptedSalmon.new.perform(@user.id, generate_status_message) + + expect(StatusMessage.exists?(guid: @entity.guid)).to be(false) + end + + describe "with messages which require sharing" do + before do + @remote_person = Person.find_or_fetch_by_identifier(@remote_user.diaspora_handle) + contact = @user.contacts.find_or_initialize_by(person_id: @remote_person.id) + contact.sharing = true + contact.save + end + + it "treats status message receive correctly" do + Workers::ReceiveEncryptedSalmon.new.perform(@user.id, generate_status_message) + + expect(StatusMessage.exists?(guid: @entity.guid)).to be(true) + end + + it "doesn't accept status message with wrong signature" do + Workers::ReceiveEncryptedSalmon.new.perform(@user.id, generate_forged_status_message) + + expect(StatusMessage.exists?(guid: @entity.guid)).to be(false) + end + + describe "retractions for non-relayable objects" do + %w( + retraction + signed_retraction + ).each do |retraction_entity_name| + context "with #{retraction_entity_name}" do + %w(status_message photo).each do |target| + context "with #{target}" do + it_behaves_like "it retracts non-relayable object" do + let(:target_object) { FactoryGirl.create(target.to_sym, author: @remote_person) } + let(:entity_name) { "#{retraction_entity_name}_entity".to_sym } + end + end + end + end + end + end + + describe "with messages which require a status to operate on" do + before do + @local_message = FactoryGirl.create(:status_message, author: @user.person) + @remote_message = FactoryGirl.create(:status_message, author: @remote_person) + end + + %w(comment like participation).each do |entity| + context "with #{entity}" do + it_behaves_like "it deals correctly with a relayable" do + let(:entity_name) { "#{entity}_entity".to_sym } + let(:klass) { entity.camelize.constantize } + end + end + end + + describe "retractions for relayable objects" do + %w( + retraction + signed_retraction + relayable_retraction + ).each do |retraction_entity_name| + context "with #{retraction_entity_name}" do + context "with comment" do + it_behaves_like "it retracts relayable object" do + # case for to-upstream federation + let(:entity_name) { "#{retraction_entity_name}_entity".to_sym } + let(:target_object) { FactoryGirl.create(:comment, author: @remote_person, post: @local_message) } + let(:sender) { @remote_user } + end + + it_behaves_like "it retracts relayable object" do + # case for to-downsteam federation + let(:target_object) { FactoryGirl.create(:comment, author: @remote_person2, post: @remote_message) } + let(:entity_name) { "#{retraction_entity_name}_entity".to_sym } + let(:sender) { @remote_user } + end + end + + context "with like" do + it_behaves_like "it retracts relayable object" do + # case for to-upstream federation + let(:entity_name) { "#{retraction_entity_name}_entity".to_sym } + let(:target_object) { FactoryGirl.create(:like, author: @remote_person, target: @local_message) } + let(:sender) { @remote_user } + end + + it_behaves_like "it retracts relayable object" do + # case for to-downsteam federation + let(:target_object) { FactoryGirl.create(:like, author: @remote_person2, target: @remote_message) } + let(:entity_name) { "#{retraction_entity_name}_entity".to_sym } + let(:sender) { @remote_user } + end + end + end + end + end + end + end +end diff --git a/spec/integration/federation/shared_receive_relayable.rb b/spec/integration/federation/shared_receive_relayable.rb new file mode 100644 index 000000000..b4ad41db3 --- /dev/null +++ b/spec/integration/federation/shared_receive_relayable.rb @@ -0,0 +1,31 @@ +shared_examples_for "it deals correctly with a relayable" do + it "treats upstream receive correctly" do + Workers::ReceiveEncryptedSalmon.new.perform(@user.id, generate_relayable_local_parent(entity_name)) + received_entity = klass.find_by(guid: @entity.guid) + expect(received_entity).not_to be_nil + expect(received_entity.author.diaspora_handle).to eq(@remote_person.diaspora_handle) + end + + it "rejects an upstream entity with a malformed author signature" do + Workers::ReceiveEncryptedSalmon.new.perform( + @user.id, + generate_relayable_local_parent_wrong_author_key(entity_name) + ) + expect(klass.exists?(guid: @entity.guid)).to be(false) + end + + it "treats downstream receive correctly" do + Workers::ReceiveEncryptedSalmon.new.perform(@user.id, generate_relayable_remote_parent(entity_name)) + received_entity = klass.find_by(guid: @entity.guid) + expect(received_entity).not_to be_nil + expect(received_entity.author.diaspora_handle).to eq(@remote_person2.diaspora_handle) + end + + it "declines downstream receive when sender signed with a wrong key" do + Workers::ReceiveEncryptedSalmon.new.perform( + @user.id, + generate_relayable_remote_parent_wrong_parent_key(entity_name) + ) + expect(klass.exists?(guid: @entity.guid)).to be(false) + end +end diff --git a/spec/integration/federation/shared_receive_retraction.rb b/spec/integration/federation/shared_receive_retraction.rb new file mode 100644 index 000000000..af02defab --- /dev/null +++ b/spec/integration/federation/shared_receive_retraction.rb @@ -0,0 +1,44 @@ +shared_examples_for "it retracts non-relayable object" do + it "retracts object by a correct retraction message" do + target_klass = target_object.class.to_s.constantize + Workers::ReceiveEncryptedSalmon.new.perform(@user.id, generate_retraction(entity_name, target_object)) + + expect(target_klass.exists?(guid: target_object.guid)).to be(false) + end + + it "doesn't retract object when retraction has wrong signatures" do + target_klass = target_object.class.to_s.constantize + Workers::ReceiveEncryptedSalmon.new.perform(@user.id, generate_forged_retraction(entity_name, target_object)) + + expect(target_klass.exists?(guid: target_object.guid)).to be(true) + end + + it "doesn't retract object when sender is different from target object" do + target_klass = target_object.class.to_s.constantize + Workers::ReceiveEncryptedSalmon.new.perform( + @user.id, + generate_retraction(entity_name, target_object, @remote_user2) + ) + + expect(target_klass.exists?(guid: target_object.guid)).to be(true) + end +end + +shared_examples_for "it retracts relayable object" do + it "retracts object by a correct message" do + target_klass = target_object.class.to_s.constantize + Workers::ReceiveEncryptedSalmon.new.perform(@user.id, generate_retraction(entity_name, target_object, sender)) + + expect(target_klass.exists?(guid: target_object.guid)).to be(false) + end + + it "doesn't retract object when retraction has wrong signatures" do + target_klass = target_object.class.to_s.constantize + Workers::ReceiveEncryptedSalmon.new.perform( + @user.id, + generate_forged_retraction(entity_name, target_object, sender) + ) + + expect(target_klass.exists?(guid: target_object.guid)).to be(true) + end +end