From 7db4f825a6a7901a345e85fb0a1cda49722b922b Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Thu, 13 Apr 2017 19:20:55 +0300 Subject: [PATCH 01/10] Refactor account deletion spec This commit refactors account deletion spec by moving data creation to a helper object DataGenerator. --- spec/factories.rb | 5 + spec/helper_methods.rb | 10 +- spec/integration/account_deletion_spec.rb | 154 +++++----------------- spec/misc_spec.rb | 2 +- spec/shared_behaviors/account_deletion.rb | 47 +++---- spec/support/data_generator.rb | 122 +++++++++++++++++ 6 files changed, 190 insertions(+), 150 deletions(-) create mode 100644 spec/support/data_generator.rb diff --git a/spec/factories.rb b/spec/factories.rb index fadc1d1722a..1f5f59d2e49 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -342,6 +342,11 @@ def r_str additional_data { {"new_property" => "some text"} } end + factory :role do + association :person + name "moderator" + end + factory(:poll_participation_signature) do author_signature "some signature" association :signature_order, order: "guid parent_guid author poll_answer_guid new_property" diff --git a/spec/helper_methods.rb b/spec/helper_methods.rb index f7406a3efe9..720f28058eb 100644 --- a/spec/helper_methods.rb +++ b/spec/helper_methods.rb @@ -27,12 +27,12 @@ def uploaded_photo File.open(fixture_name) end - def create_conversation_with_message(sender, recipient_person, subject, text) + def create_conversation_with_message(sender_person, recipient_person, subject, text) create_hash = { - :author => sender.person, - :participant_ids => [sender.person.id, recipient_person.id], - :subject => subject, - :messages_attributes => [ {:author => sender.person, :text => text} ] + author: sender_person, + participant_ids: [sender_person.id, recipient_person.id], + subject: subject, + messages_attributes: [{author: sender_person, text: text}] } Conversation.create!(create_hash) diff --git a/spec/integration/account_deletion_spec.rb b/spec/integration/account_deletion_spec.rb index dfe3ee7c00d..7c934620443 100644 --- a/spec/integration/account_deletion_spec.rb +++ b/spec/integration/account_deletion_spec.rb @@ -1,128 +1,46 @@ -describe "deleteing your account", type: :request do - context "user" do - before do - @person = bob.person - @alices_post = alice.post(:status_message, - text: "@{bob Grimn; #{bob.person.diaspora_handle}} you are silly", - to: alice.aspects.find_by_name("generic")) - - # bob's own content - bob.post(:status_message, text: "asldkfjs", to: bob.aspects.first) - FactoryGirl.create(:photo, author: bob.person) - - @aspect_vis = AspectVisibility.where(aspect_id: bob.aspects.map(&:id)) - - # objects on post - bob.like!(@alices_post) - bob.comment!(@alices_post, "here are some thoughts on your post") - - # conversations - create_conversation_with_message(alice, bob.person, "Subject", "Hey bob") - - # join tables - @users_sv = ShareVisibility.where(user_id: bob.id).load - @persons_sv = ShareVisibility.where(shareable_id: bob.posts.map(&:id), shareable_type: "Post").load - - # user associated objects - @prefs = [] - %w(mentioned liked reshared).each do |pref| - @prefs << bob.user_preferences.create!(email_type: pref) - end - - # notifications - @notifications = [] - 3.times do - @notifications << FactoryGirl.create(:notification, recipient: bob) - end - - # services - @services = [] - 3.times do - @services << FactoryGirl.create(:service, user: bob) - end - - # block - @block = bob.blocks.create!(person: eve.person) - - AccountDeleter.new(bob.person.diaspora_handle).perform! - bob.reload - end - - it "deletes all of the user's preferences" do - expect(UserPreference.where(id: @prefs.map(&:id))).to be_empty - end - - it "deletes all of the user's notifications" do - expect(Notification.where(id: @notifications.map(&:id))).to be_empty - end - - it "deletes all of the users's blocked users" do - expect(Block.where(id: @block.id)).to be_empty - end - - it "deletes all of the user's services" do - expect(Service.where(id: @services.map(&:id))).to be_empty - end - - it "deletes all of bobs share visiblites" do - expect(ShareVisibility.where(id: @users_sv.map(&:id))).to be_empty - expect(ShareVisibility.where(id: @persons_sv.map(&:id))).to be_empty - end - - it "deletes all of bobs aspect visiblites" do - expect(AspectVisibility.where(id: @aspect_vis.map(&:id))).to be_empty - end +describe "deleteing account", type: :request do + def account_removal_method + AccountDeleter.new(subject.diaspora_handle).perform! + subject.reload + end - it "deletes all aspects" do - expect(bob.aspects).to be_empty - end + context "of local user" do + subject(:user) { FactoryGirl.create(:user_with_aspect) } - it "deletes all user contacts" do - expect(bob.contacts).to be_empty - end - - it "clears the account fields" do - bob.send(:clearable_fields).each do |field| - expect(bob.reload[field]).to be_blank - end + before do + DataGenerator.create(subject, :generic_user_data) + end + + it "deletes all of the user data" do + expect { + account_removal_method + }.to change(nil, "user preferences empty?") { UserPreference.where(user_id: user.id).empty? }.to(be_truthy) + .and(change(nil, "notifications empty?") { Notification.where(recipient_id: user.id).empty? }.to(be_truthy)) + .and(change(nil, "blocks empty?") { Block.where(user_id: user.id).empty? }.to(be_truthy)) + .and(change(nil, "services empty?") { Service.where(user_id: user.id).empty? }.to(be_truthy)) + .and(change(nil, "share visibilities empty?") { ShareVisibility.where(user_id: user.id).empty? }.to(be_truthy)) + .and(change(nil, "aspects empty?") { user.aspects.empty? }.to(be_truthy)) + .and(change(nil, "contacts empty?") { user.contacts.empty? }.to(be_truthy)) + .and(change(nil, "tag followings empty?") { user.tag_followings.empty? }.to(be_truthy)) + .and(change(nil, "clearable fields blank?") { + user.send(:clearable_fields).map {|field| + user.reload[field].blank? + } + }.to(eq([true] * user.send(:clearable_fields).count))) + end + + it_behaves_like "it removes the person associations" do + subject(:person) { user.person } end - - it_should_behave_like "it removes the person associations" end - context "remote person" do - before do - @person = remote_raphael - - # contacts - @contacts = @person.contacts - - # posts - @posts = (1..3).map do - FactoryGirl.create(:status_message, author: @person) - end - - @persons_sv = @posts.each do |post| - @contacts.each do |contact| - ShareVisibility.create!(user_id: contact.user.id, shareable: post) - end - end + context "of remote person" do + subject(:person) { remote_raphael } - # photos - @photo = FactoryGirl.create(:photo, author: @person) - - # mentions - @mentions = 3.times do - FactoryGirl.create(:mention, person: @person) - end - - # conversations - create_conversation_with_message(alice, @person, "Subject", "Hey bob") - - AccountDeleter.new(@person.diaspora_handle).perform! - @person.reload + before do + DataGenerator.create(subject, :generic_person_data) end - it_should_behave_like "it removes the person associations" + it_behaves_like "it removes the person associations" end end diff --git a/spec/misc_spec.rb b/spec/misc_spec.rb index 1e9851fa9b6..2661bc7746f 100644 --- a/spec/misc_spec.rb +++ b/spec/misc_spec.rb @@ -78,7 +78,7 @@ describe "#create_conversation_with_message" do it 'creates a conversation and a message' do - conversation = create_conversation_with_message(alice, bob.person, "Subject", "Hey Bob") + conversation = create_conversation_with_message(alice.person, bob.person, "Subject", "Hey Bob") expect(conversation.participants).to eq([alice.person, bob.person]) expect(conversation.subject).to eq("Subject") diff --git a/spec/shared_behaviors/account_deletion.rb b/spec/shared_behaviors/account_deletion.rb index 48500e0d098..19f7bf4fc67 100644 --- a/spec/shared_behaviors/account_deletion.rb +++ b/spec/shared_behaviors/account_deletion.rb @@ -2,32 +2,27 @@ # licensed under the Affero General Public License version 3 or later. See # the COPYRIGHT file. -shared_examples_for 'it removes the person associations' do - it "removes all of the person's posts" do - expect(Post.where(:author_id => @person.id).count).to eq(0) - end - - it 'deletes all person contacts' do - expect(Contact.where(:person_id => @person.id)).to be_empty - end - - it 'deletes all mentions' do - expect(@person.mentions).to be_empty - end - - it "removes all of the person's photos" do - expect(Photo.where(:author_id => @person.id)).to be_empty - end - - it 'sets the person object as closed and the profile is cleared' do - expect(@person.reload.closed_account).to be true - - expect(@person.profile.reload.first_name).to be_blank - expect(@person.profile.reload.last_name).to be_blank - end +shared_examples_for "it removes the person associations" do + RSpec::Matchers.define_negated_matcher :remain, :change - it 'deletes only the converersation visibility for the deleted user' do - expect(ConversationVisibility.where(:person_id => alice.person.id)).not_to be_empty - expect(ConversationVisibility.where(:person_id => @person.id)).to be_empty + it "removes all of the person associations" do + expect { + account_removal_method + }.to change(nil, "posts empty?") { Post.where(author_id: person.id).empty? }.to(be_truthy) + .and(change(nil, "contacts empty?") { Contact.where(person_id: person.id).empty? }.to(be_truthy)) + .and(change(nil, "mentions empty?") { person.mentions.empty? }.to(be_truthy)) + .and(change(nil, "photos empty?") { Photo.where(author_id: person.id).empty? }.to(be_truthy)) + .and(change(nil, "participations empty?") { Participation.where(author_id: person.id).empty? }.to(be_truthy)) + .and(change(nil, "roles empty?") { Role.where(person_id: person.id).empty? }.to(be_truthy)) + .and(change(person, :closed_account).to(be_truthy)) + .and(change(nil, "first name is blank?") { person.profile.first_name.blank? }.to(be_truthy)) + .and(change(nil, "last name is blank?") { person.profile.last_name.blank? }.to(be_truthy)) + .and(change(nil, "conversation visibilities empty?") { + ConversationVisibility.where(person_id: person.id).empty? + }.to(be_truthy)) + .and(remain(nil, "conversations empty?") { Conversation.where(author: person).empty? }.from(be_falsey)) + .and(remain(nil, "conversation visibilities of other participants empty?") { + ConversationVisibility.where(conversation: Conversation.where(author: person)).empty? + }.from(be_falsey)) end end diff --git a/spec/support/data_generator.rb b/spec/support/data_generator.rb new file mode 100644 index 00000000000..bf93a8bfefc --- /dev/null +++ b/spec/support/data_generator.rb @@ -0,0 +1,122 @@ +# TODO: docs +class DataGenerator + def person + @person || user.person + end + + def user + @user || person.owner + end + + def initialize(user_or_person) + if user_or_person.is_a? User + @user = user_or_person + elsif user_or_person.is_a? Person + @person = user_or_person + else + raise ArgumentError + end + end + + def self.create(user_or_person, type) + generator = new(user_or_person) + if type.is_a? Symbol + generator.send(type) + elsif type.is_a? Array + type.each {|type| + generator.send(type) + } + end + end + + def generic_user_data + preferences + notifications + blocks + service + private_post_as_receipient + tag_following + generic_person_data + end + + def generic_person_data + private_status_message + mention + photo + conversations + role + participation + end + + def preferences + %w[mentioned liked reshared].each do |pref| + user.user_preferences.create!(email_type: pref) + end + end + + def notifications + FactoryGirl.create(:notification, recipient: user) + end + + def conversations + a_friend = person.contacts.first.user.person + create_conversation_with_message(a_friend, person, "Subject", "Hey #{person.name}") + create_conversation_with_message(person, a_friend, "Subject", "Hey #{a_friend.name}") + end + + def blocks + user.blocks.create!(person: eve.person) + eve.blocks.create!(person: person) + end + + def service + FactoryGirl.create(:service, user: user) + end + + def private_post_as_receipient + friend = mutual_friend + friend.post( + :status_message, + text: text_mentioning(user), + to: friend.aspects.first + ) + end + + def tag_following + TagFollowing.create!(tag: random_tag, user: user) + end + + def random_tag + ActsAsTaggableOn::Tag.create!(name: "partytimeexcellent#{r_str}") + end + + def mutual_friend + FactoryGirl.create(:user_with_aspect).tap {|friend| + connect_users(user, first_aspect, friend, friend.aspects.first) + } + end + + def first_aspect + user.aspects.first || FactoryGirl.create(:aspect, user: user) + end + + def private_status_message + post = FactoryGirl.create(:status_message, author: person) + + person.contacts.each do |contact| + ShareVisibility.create!(user_id: contact.user.id, shareable: post) + end + end + + %i(photo participation).each do |factory| + define_method factory do + FactoryGirl.create(factory, author: person) + end + end + + %i[mention role].each do |factory| + define_method factory do + FactoryGirl.create(factory, person: person) + end + end +end From fca6121c6a90b26db61aabf36b5d99d724bada9c Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Sun, 2 Apr 2017 19:37:47 +0300 Subject: [PATCH 02/10] Exporter::PostsWithActivity class This class allows to query posts where a person made any activity (submitted comments, likes, participations or poll participations). --- app/models/status_message.rb | 1 + lib/diaspora/exporter/posts_with_activity.rb | 54 +++++++++++++++++++ .../exporter/posts_with_activity_spec.rb | 23 ++++++++ 3 files changed, 78 insertions(+) create mode 100644 lib/diaspora/exporter/posts_with_activity.rb create mode 100644 spec/lib/diaspora/exporter/posts_with_activity_spec.rb diff --git a/app/models/status_message.rb b/app/models/status_message.rb index 423ea01fce4..a0c9d727ba2 100644 --- a/app/models/status_message.rb +++ b/app/models/status_message.rb @@ -19,6 +19,7 @@ class StatusMessage < Post has_one :location has_one :poll, autosave: true + has_many :poll_participations, through: :poll attr_accessor :oembed_url attr_accessor :open_graph_url diff --git a/lib/diaspora/exporter/posts_with_activity.rb b/lib/diaspora/exporter/posts_with_activity.rb new file mode 100644 index 00000000000..9cbec03c17b --- /dev/null +++ b/lib/diaspora/exporter/posts_with_activity.rb @@ -0,0 +1,54 @@ +module Diaspora + class Exporter + # This class allows to query posts where a person made any activity (submitted comments, + # likes, participations or poll participations). + class PostsWithActivity + # TODO: docs + def initialize(user) + @user = user + end + + # TODO: docs + def query + Post.from("(#{sql_union_all_activities}) AS posts") + end + + private + + attr_reader :user + + def person + user.person + end + + def sql_union_all_activities + all_activities.map(&:to_sql).join(" UNION ") + end + + def all_activities + [comments_activity, likes_activity, subscriptions, polls_activity].compact + end + + def likes_activity + other_people_posts.liked_by(person) + end + + def comments_activity + other_people_posts.commented_by(person) + end + + def subscriptions + other_people_posts.subscribed_by(user) + end + + def polls_activity + StatusMessage.where.not(author_id: person.id).joins(:poll_participations) + .where(poll_participations: {author_id: person.id}) + end + + def other_people_posts + Post.where.not(author_id: person.id) + end + end + end +end diff --git a/spec/lib/diaspora/exporter/posts_with_activity_spec.rb b/spec/lib/diaspora/exporter/posts_with_activity_spec.rb new file mode 100644 index 00000000000..c6335cbb458 --- /dev/null +++ b/spec/lib/diaspora/exporter/posts_with_activity_spec.rb @@ -0,0 +1,23 @@ +describe Diaspora::Exporter::PostsWithActivity do + let(:user) { FactoryGirl.create(:user) } + let(:instance) { Diaspora::Exporter::PostsWithActivity.new(user) } + + describe "#query" do + let(:activity) { + [ + user.person.likes.first.target, + user.person.comments.first.parent, + user.person.poll_participations.first.parent.status_message, + user.person.participations.first.target + ] + } + + before do + DataGenerator.create(user, %i[activity participation]) + end + + it "returns all posts with person's activity" do + expect(instance.query).to match_array(activity) + end + end +end From c985af1f85b6fb38794c6ecdd245a736561588f3 Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Sun, 2 Apr 2017 20:18:04 +0300 Subject: [PATCH 03/10] New Exporter::OthersRelayables class This class implements methods that allow to query relayables (comments, likes, participations, poll_participations) of other people for posts of the given person. --- app/models/poll_participation.rb | 1 + lib/diaspora/exporter/others_relayables.rb | 42 +++++++++++++++++++ .../exporter/others_relayables_spec.rb | 35 ++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 lib/diaspora/exporter/others_relayables.rb create mode 100644 spec/lib/diaspora/exporter/others_relayables_spec.rb diff --git a/app/models/poll_participation.rb b/app/models/poll_participation.rb index 8dba2746573..7e653bb65c0 100644 --- a/app/models/poll_participation.rb +++ b/app/models/poll_participation.rb @@ -6,6 +6,7 @@ class PollParticipation < ActiveRecord::Base belongs_to :poll belongs_to :poll_answer, counter_cache: :vote_count + has_one :status_message, through: :poll has_one :signature, class_name: "PollParticipationSignature", dependent: :delete diff --git a/lib/diaspora/exporter/others_relayables.rb b/lib/diaspora/exporter/others_relayables.rb new file mode 100644 index 00000000000..5876c52cdaa --- /dev/null +++ b/lib/diaspora/exporter/others_relayables.rb @@ -0,0 +1,42 @@ +module Diaspora + class Exporter + # This class implements methods that allow to query relayables (comments, likes, participations, + # poll_participations) of other people for posts of the given person. + class OthersRelayables + # @param person_id [Integer] Database id of a person for whom we want to request relayalbes + def initialize(person_id) + @person_id = person_id + end + + # Comments of other people to the person's post + # @return [Comment::ActiveRecord_Relation] + def comments + Comment + .where.not(author_id: person_id) + .joins("INNER JOIN posts ON (commentable_type = 'Post' AND posts.id = commentable_id)") + .where("posts.author_id = ?", person_id) + end + + # Likes of other people to the person's post + # @return [Like::ActiveRecord_Relation] + def likes + Like + .where.not(author_id: person_id) + .joins("INNER JOIN posts ON (target_type = 'Post' AND posts.id = target_id)") + .where("posts.author_id = ?", person_id) + end + + # Poll participations of other people to the person's polls + # @return [PollParticipation::ActiveRecord_Relation] + def poll_participations + PollParticipation + .where.not(author_id: person_id).joins(:status_message) + .where("posts.author_id = ?", person_id) + end + + private + + attr_reader :person_id + end + end +end diff --git a/spec/lib/diaspora/exporter/others_relayables_spec.rb b/spec/lib/diaspora/exporter/others_relayables_spec.rb new file mode 100644 index 00000000000..95595739fae --- /dev/null +++ b/spec/lib/diaspora/exporter/others_relayables_spec.rb @@ -0,0 +1,35 @@ +describe Diaspora::Exporter::OthersRelayables do + let(:status_message) { FactoryGirl.create(:status_message) } + let(:person) { status_message.author } + let(:instance) { Diaspora::Exporter::OthersRelayables.new(person.id) } + + describe "#comments" do + let(:comment) { FactoryGirl.create(:comment, post: status_message) } + + it "has a comment in the data set" do + expect(instance.comments).to eq([comment]) + end + end + + describe "#likes" do + let(:like) { FactoryGirl.create(:like, target: status_message) } + + it "has a like in the data set" do + expect(instance.likes).to eq([like]) + end + end + + describe "#poll_participations" do + let(:status_message) { FactoryGirl.create(:status_message_with_poll) } + let(:poll_participation) { + FactoryGirl.create( + :poll_participation, + poll_answer: status_message.poll.poll_answers.first + ) + } + + it "has a poll participation in the data set" do + expect(instance.poll_participations).to eq([poll_participation]) + end + end +end From c63493b0d1a46f5482bdabf740fbd1efaf8c9134 Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Sun, 2 Apr 2017 20:49:14 +0300 Subject: [PATCH 04/10] New Exporter::NonContactAuthors class This class is capable of quering a list of people from authors of given posts that are non-contacts of a given user. --- lib/diaspora/exporter/non_contact_authors.rb | 36 +++++++++++++++++++ .../exporter/non_contact_authors_spec.rb | 25 +++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 lib/diaspora/exporter/non_contact_authors.rb create mode 100644 spec/lib/diaspora/exporter/non_contact_authors_spec.rb diff --git a/lib/diaspora/exporter/non_contact_authors.rb b/lib/diaspora/exporter/non_contact_authors.rb new file mode 100644 index 00000000000..f3cfc4801a1 --- /dev/null +++ b/lib/diaspora/exporter/non_contact_authors.rb @@ -0,0 +1,36 @@ +module Diaspora + class Exporter + # This class is capable of quering a list of people from authors of given posts that are non-contacts of a given + # user. + class NonContactAuthors + # @param posts [Post::ActiveRecord_Relation] posts that we fetch authors from to make authors list + # @param user [User] a user we fetch a contact list from + def initialize(posts, user) + @posts = posts + @user = user + end + + # Create a request of non-contact authors of the posts for the user + # @return [Post::ActiveRecord_Relation] + def query + Person.where(id: non_contact_authors_ids) + end + + private + + def non_contact_authors_ids + posts_authors_ids - contacts_ids + end + + def posts_authors_ids + posts.pluck(:author_id).uniq + end + + def contacts_ids + user.contacts.pluck(:person_id) + end + + attr_reader :posts, :user + end + end +end diff --git a/spec/lib/diaspora/exporter/non_contact_authors_spec.rb b/spec/lib/diaspora/exporter/non_contact_authors_spec.rb new file mode 100644 index 00000000000..cac8c3ca047 --- /dev/null +++ b/spec/lib/diaspora/exporter/non_contact_authors_spec.rb @@ -0,0 +1,25 @@ +describe Diaspora::Exporter::NonContactAuthors do + describe "#query" do + let(:user) { FactoryGirl.create(:user_with_aspect) } + let(:post) { FactoryGirl.create(:status_message) } + let(:instance) { + Diaspora::Exporter::NonContactAuthors.new(Post.where(id: post.id), user) + } + + context "without contact relationship" do + it "includes post author to the result set" do + expect(instance.query).to eq([post.author]) + end + end + + context "with contact relationship" do + before do + user.share_with(post.author, user.aspects.first) + end + + it "doesn't include post author to the result set" do + expect(instance.query).to be_empty + end + end + end +end From 2c3f1163265c0dc19dcf83d13b485f6835c987e0 Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Wed, 9 Aug 2017 16:49:39 +0300 Subject: [PATCH 05/10] Add new scopes for the Post model --- app/models/post.rb | 11 ++++++++++ spec/models/post_spec.rb | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/app/models/post.rb b/app/models/post.rb index 576717f7985..58dbf12cfbc 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -54,6 +54,17 @@ class Post < ActiveRecord::Base joins(:likes).where(:likes => {:author_id => person.id}) } + scope :subscribed_by, ->(user) { + joins(:participations).where(participations: {author_id: user.person_id}) + } + + scope :reshares, -> { where(type: "Reshare") } + + scope :reshared_by, ->(person) { + # we join on the same table, Rails renames "posts" to "reshares_posts" for the right table + joins(:reshares).where(reshares_posts: {author_id: person.id}) + } + def post_type self.class.name end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index fd7b4427414..550fdc74d3d 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -174,6 +174,50 @@ end end end + + describe ".subscribed_by" do + let(:user) { FactoryGirl.create(:user) } + + context "when the user has a participation on a post" do + let(:post) { FactoryGirl.create(:status_message_with_participations, participants: [user]) } + + it "includes the post to the result set" do + expect(Post.subscribed_by(user)).to eq([post]) + end + end + + context "when the user doens't have a participation on a post" do + before do + FactoryGirl.create(:status_message) + end + + it "returns empty result set" do + expect(Post.subscribed_by(user)).to be_empty + end + end + end + + describe ".reshared_by" do + let(:person) { FactoryGirl.create(:person) } + + context "when the person has a reshare for a post" do + let(:post) { FactoryGirl.create(:reshare, author: person).root } + + it "includes the post to the result set" do + expect(Post.reshared_by(person)).to eq([post]) + end + end + + context "when the person has no reshare for a post" do + before do + FactoryGirl.create(:status_message) + end + + it "returns empty result set" do + expect(Post.reshared_by(person)).to be_empty + end + end + end end describe 'validations' do From 1b1db3bb0ce4e186c9e2ff4ce8a335c59a518ead Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Wed, 9 Aug 2017 20:14:49 +0300 Subject: [PATCH 06/10] Bump diaspora_federation --- Gemfile | 4 ++-- Gemfile.lock | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 0021695037c..639f7cbf4db 100644 --- a/Gemfile +++ b/Gemfile @@ -13,7 +13,7 @@ gem "unicorn-worker-killer", "0.4.4" # Federation -gem "diaspora_federation-rails", "0.2.0" +gem "diaspora_federation-rails", "0.2.1" # API and JSON @@ -294,7 +294,7 @@ group :test do gem "timecop", "0.9.1" gem "webmock", "3.0.1", require: false - gem "diaspora_federation-test", "0.2.0" + gem "diaspora_federation-test", "0.2.1" # Coverage gem 'coveralls', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 798e8023830..b07bb4e8201 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -162,17 +162,17 @@ GEM devise rails (>= 3.0.4) diaspora-prosody-config (0.0.7) - diaspora_federation (0.2.0) + diaspora_federation (0.2.1) faraday (>= 0.9.0, < 0.13.0) - faraday_middleware (>= 0.10.0, < 0.12.0) + faraday_middleware (>= 0.10.0, < 0.13.0) nokogiri (~> 1.6, >= 1.6.8) typhoeus (~> 1.0) valid (~> 1.0) - diaspora_federation-rails (0.2.0) + diaspora_federation-rails (0.2.1) actionpack (>= 4.2, < 6) - diaspora_federation (= 0.2.0) - diaspora_federation-test (0.2.0) - diaspora_federation (= 0.2.0) + diaspora_federation (= 0.2.1) + diaspora_federation-test (0.2.1) + diaspora_federation (= 0.2.1) fabrication (~> 2.16.0) uuid (~> 2.3.8) diff-lcs (1.3) @@ -199,7 +199,7 @@ GEM sigar (~> 0.7.3) state_machines thor - fabrication (2.16.1) + fabrication (2.16.2) factory_girl (4.8.0) activesupport (>= 3.0.0) factory_girl_rails (4.8.0) @@ -772,8 +772,8 @@ DEPENDENCIES devise (= 4.3.0) devise_lastseenable (= 0.0.6) diaspora-prosody-config (= 0.0.7) - diaspora_federation-rails (= 0.2.0) - diaspora_federation-test (= 0.2.0) + diaspora_federation-rails (= 0.2.1) + diaspora_federation-test (= 0.2.1) entypo-rails (= 3.0.0) eye (= 0.9.2) factory_girl_rails (= 4.8.0) From 7374661e2fac78b9e30c75fb39165ab65dfbee81 Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Mon, 24 Apr 2017 13:41:49 +0300 Subject: [PATCH 07/10] Update the user data export archive format. This commit introduces changes to the user data export archive format. This extends data set which is included in the archive. This data can be then imported to other pods when this feature is implemented. Also the commit adds the archive format json schema. ATM it is used in automatic tests only, but in future it will also be used to validate incoming archives. --- Gemfile | 3 + Gemfile.lock | 8 + app/models/person.rb | 2 + app/serializers/export/comment_serializer.rb | 11 - app/serializers/export/contact_serializer.rb | 28 +- .../export/others_data_serializer.rb | 34 ++ app/serializers/export/own_post_serializer.rb | 33 ++ .../export/own_relayables_serializer.rb | 13 + .../export/person_metadata_serializer.rb | 17 + app/serializers/export/post_serializer.rb | 13 - app/serializers/export/profile_serializer.rb | 14 - app/serializers/export/user_serializer.rb | 43 +- .../federation_entity_serializer.rb | 16 + app/serializers/serializer_post_processing.rb | 20 + lib/account_deleter.rb | 3 +- lib/diaspora/exporter.rb | 11 +- lib/diaspora/exporter/posts_with_activity.rb | 11 +- lib/diaspora/shareable.rb | 8 + lib/schemas/archive-format.json | 255 ++++++++++ spec/factories.rb | 6 +- spec/integration/exporter_spec.rb | 444 ++++++++++++++++++ .../exporter/posts_with_activity_spec.rb | 3 +- spec/lib/diaspora/exporter_spec.rb | 93 +--- spec/models/status_message_spec.rb | 4 + spec/serializers/comment_serializer_spec.rb | 8 - .../export/aspect_serializer_spec.rb | 12 + .../export/contact_serializer_spec.rb | 25 + .../export/others_data_serializer_spec.rb | 43 ++ .../export/own_post_serializer_spec.rb | 48 ++ .../export/own_relayables_serializer_spec.rb | 9 + .../export/person_metadata_serializer_spec.rb | 12 + .../export/user_serializer_spec.rb | 79 ++++ .../federation_entity_serializer_spec.rb | 17 + spec/serializers/post_serializer_spec.rb | 15 - .../serializer_post_processing_spec.rb | 33 ++ .../federation_entity_serializer.rb | 14 + spec/shared_behaviors/shareable.rb | 18 + spec/spec/data_generator_spec.rb | 57 +++ spec/spec_helper.rb | 11 + spec/support/data_generator.rb | 69 ++- spec/support/serializer_matchers.rb | 79 ++++ 41 files changed, 1470 insertions(+), 172 deletions(-) delete mode 100644 app/serializers/export/comment_serializer.rb create mode 100644 app/serializers/export/others_data_serializer.rb create mode 100644 app/serializers/export/own_post_serializer.rb create mode 100644 app/serializers/export/own_relayables_serializer.rb create mode 100644 app/serializers/export/person_metadata_serializer.rb delete mode 100644 app/serializers/export/post_serializer.rb delete mode 100644 app/serializers/export/profile_serializer.rb create mode 100644 app/serializers/federation_entity_serializer.rb create mode 100644 app/serializers/serializer_post_processing.rb create mode 100644 lib/schemas/archive-format.json create mode 100644 spec/integration/exporter_spec.rb delete mode 100644 spec/serializers/comment_serializer_spec.rb create mode 100644 spec/serializers/export/aspect_serializer_spec.rb create mode 100644 spec/serializers/export/contact_serializer_spec.rb create mode 100644 spec/serializers/export/others_data_serializer_spec.rb create mode 100644 spec/serializers/export/own_post_serializer_spec.rb create mode 100644 spec/serializers/export/own_relayables_serializer_spec.rb create mode 100644 spec/serializers/export/person_metadata_serializer_spec.rb create mode 100644 spec/serializers/export/user_serializer_spec.rb create mode 100644 spec/serializers/federation_entity_serializer_spec.rb delete mode 100644 spec/serializers/post_serializer_spec.rb create mode 100644 spec/serializers/serializer_post_processing_spec.rb create mode 100644 spec/shared_behaviors/federation_entity_serializer.rb create mode 100644 spec/shared_behaviors/shareable.rb create mode 100644 spec/spec/data_generator_spec.rb create mode 100644 spec/support/serializer_matchers.rb diff --git a/Gemfile b/Gemfile index 639f7cbf4db..30aa76763bc 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gem "unicorn-worker-killer", "0.4.4" # Federation +gem "diaspora_federation-json_schema", "0.2.1" gem "diaspora_federation-rails", "0.2.1" # API and JSON @@ -277,6 +278,8 @@ group :test do gem "fixture_builder", "0.5.0" gem "fuubar", "2.2.0" + gem "json-schema-rspec", "0.0.4" + gem "rspec-json_expectations", "~> 2.1" gem "test_after_commit", "1.1.0" # Cucumber (integration tests) diff --git a/Gemfile.lock b/Gemfile.lock index b07bb4e8201..a987f2839d5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -168,6 +168,7 @@ GEM nokogiri (~> 1.6, >= 1.6.8) typhoeus (~> 1.0) valid (~> 1.0) + diaspora_federation-json_schema (0.2.1) diaspora_federation-rails (0.2.1) actionpack (>= 4.2, < 6) diaspora_federation (= 0.2.1) @@ -334,6 +335,9 @@ GEM url_safe_base64 json-schema (2.8.0) addressable (>= 2.4) + json-schema-rspec (0.0.4) + json-schema (~> 2.5) + rspec jsonpath (0.8.5) multi_json jwt (1.5.6) @@ -582,6 +586,7 @@ GEM rspec-expectations (3.6.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.6.0) + rspec-json_expectations (2.1.0) rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.6.0) @@ -772,6 +777,7 @@ DEPENDENCIES devise (= 4.3.0) devise_lastseenable (= 0.0.6) diaspora-prosody-config (= 0.0.7) + diaspora_federation-json_schema (= 0.2.1) diaspora_federation-rails (= 0.2.1) diaspora_federation-test (= 0.2.1) entypo-rails (= 3.0.0) @@ -800,6 +806,7 @@ DEPENDENCIES js_image_paths (= 0.1.0) json (= 2.1.0) json-schema (= 2.8.0) + json-schema-rspec (= 0.0.4) leaflet-rails (= 1.1.0) logging-rails (= 0.6.0) markerb (= 1.1.0) @@ -858,6 +865,7 @@ DEPENDENCIES rb-inotify (= 0.9.10) redcarpet (= 3.4.0) responders (= 2.4.0) + rspec-json_expectations (~> 2.1) rspec-rails (= 3.6.0) rubocop (= 0.49.1) ruby-oembed (= 0.12.0) diff --git a/app/models/person.rb b/app/models/person.rb index d76cfcb927a..3f20a3a847e 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -37,7 +37,9 @@ def downcase_diaspora_handle has_many :posts, :foreign_key => :author_id, :dependent => :destroy # This person's own posts has_many :photos, :foreign_key => :author_id, :dependent => :destroy # This person's own photos has_many :comments, :foreign_key => :author_id, :dependent => :destroy # This person's own comments + has_many :likes, foreign_key: :author_id, dependent: :destroy # This person's own likes has_many :participations, :foreign_key => :author_id, :dependent => :destroy + has_many :poll_participations, foreign_key: :author_id, dependent: :destroy has_many :conversation_visibilities has_many :roles diff --git a/app/serializers/export/comment_serializer.rb b/app/serializers/export/comment_serializer.rb deleted file mode 100644 index 58b5bde1a33..00000000000 --- a/app/serializers/export/comment_serializer.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Export - class CommentSerializer < ActiveModel::Serializer - attributes :guid, - :text, - :post_guid - - def post_guid - object.post.guid - end - end -end diff --git a/app/serializers/export/contact_serializer.rb b/app/serializers/export/contact_serializer.rb index be025304f02..871486777cc 100644 --- a/app/serializers/export/contact_serializer.rb +++ b/app/serializers/export/contact_serializer.rb @@ -2,11 +2,33 @@ module Export class ContactSerializer < ActiveModel::Serializer attributes :sharing, :receiving, + :following, + :followed, :person_guid, :person_name, - :person_first_name, - :person_diaspora_handle + :account_id, + :public_key - has_many :aspects, each_serializer: Export::AspectSerializer + has_many :contact_groups_membership + + def following + object.sharing + end + + def followed + object.receiving + end + + def account_id + object.person_diaspora_handle + end + + def contact_groups_membership + object.aspects.map(&:name) + end + + def public_key + object.person.serialized_public_key + end end end diff --git a/app/serializers/export/others_data_serializer.rb b/app/serializers/export/others_data_serializer.rb new file mode 100644 index 00000000000..96a819c38bb --- /dev/null +++ b/app/serializers/export/others_data_serializer.rb @@ -0,0 +1,34 @@ +module Export + class OthersDataSerializer < ActiveModel::Serializer + # Relayables of other people in the archive: comments, likes, participations, poll participations where author is + # the archive owner + has_many :relayables, each_serializer: FederationEntitySerializer + + # Parent posts of user's own relayables. We have to save metadata to use + # it in case when posts temporary unavailable on the target pod. + has_many :posts, each_serializer: FederationEntitySerializer + + # Authors of posts where we participated and authors are not in contacts + has_many :non_contact_authors, each_serializer: PersonMetadataSerializer + + private + + def relayables + %i[comments likes poll_participations].map {|relayable| + others_relayables.send(relayable) + }.sum + end + + def others_relayables + @others_relayables ||= Diaspora::Exporter::OthersRelayables.new(object.person_id) + end + + def posts + @posts ||= Diaspora::Exporter::PostsWithActivity.new(object).query + end + + def non_contact_authors + Diaspora::Exporter::NonContactAuthors.new(posts, object).query + end + end +end diff --git a/app/serializers/export/own_post_serializer.rb b/app/serializers/export/own_post_serializer.rb new file mode 100644 index 00000000000..150ecd42780 --- /dev/null +++ b/app/serializers/export/own_post_serializer.rb @@ -0,0 +1,33 @@ +module Export + # This is a serializer for the user's own posts + class OwnPostSerializer < FederationEntitySerializer + # Only for public posts. + # Includes URIs of pods which must be notified on the post updates. + # Must always include local pod URI since we will want all the updates on the post if user migrates. + has_many :subscribed_pods_uris + + # Only for private posts. + # Includes diaspora* IDs of people who must be notified on post updates. + has_many :subscribed_users_ids + + # Normally accepts Post as an object. + def initialize(*) + super + self.except = [excluded_subscription_key] + end + + private + + def subscribed_pods_uris + object.subscribed_pods_uris.push(AppConfig.pod_uri.to_s) + end + + def subscribed_users_ids + object.subscribers.map(&:diaspora_handle) + end + + def excluded_subscription_key + entity.public ? :subscribed_users_ids : :subscribed_pods_uris + end + end +end diff --git a/app/serializers/export/own_relayables_serializer.rb b/app/serializers/export/own_relayables_serializer.rb new file mode 100644 index 00000000000..a38f83ccf1e --- /dev/null +++ b/app/serializers/export/own_relayables_serializer.rb @@ -0,0 +1,13 @@ +module Export + # This is a serializer for the user's own relayables. We remove signature from the own relayables since it isn't + # useful and takes space. + class OwnRelayablesSerializer < FederationEntitySerializer + private + + def modify_serializable_object(hash) + super.tap {|hash| + hash[:entity_data].delete(:author_signature) + } + end + end +end diff --git a/app/serializers/export/person_metadata_serializer.rb b/app/serializers/export/person_metadata_serializer.rb new file mode 100644 index 00000000000..911062e39e2 --- /dev/null +++ b/app/serializers/export/person_metadata_serializer.rb @@ -0,0 +1,17 @@ +module Export + class PersonMetadataSerializer < ActiveModel::Serializer + attributes :guid, + :account_id, + :public_key + + private + + def account_id + object.diaspora_handle + end + + def public_key + object.serialized_public_key + end + end +end diff --git a/app/serializers/export/post_serializer.rb b/app/serializers/export/post_serializer.rb deleted file mode 100644 index 84b6a91ed23..00000000000 --- a/app/serializers/export/post_serializer.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Export - class PostSerializer < ActiveModel::Serializer - attributes :guid, - :text, - :public, - :diaspora_handle, - :type, - :likes_count, - :comments_count, - :reshares_count, - :created_at - end -end diff --git a/app/serializers/export/profile_serializer.rb b/app/serializers/export/profile_serializer.rb deleted file mode 100644 index b8eb2001f27..00000000000 --- a/app/serializers/export/profile_serializer.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Export - class ProfileSerializer < ActiveModel::Serializer - attributes :first_name, - :last_name, - :gender, - :bio, - :birthday, - :location, - :image_url, - :diaspora_handle, - :searchable, - :nsfw - end -end diff --git a/app/serializers/export/user_serializer.rb b/app/serializers/export/user_serializer.rb index 7e3b6b42ddc..09f26d3bdb8 100644 --- a/app/serializers/export/user_serializer.rb +++ b/app/serializers/export/user_serializer.rb @@ -1,24 +1,49 @@ module Export class UserSerializer < ActiveModel::Serializer - attributes :name, + attributes :username, :email, :language, - :username, - :serialized_private_key, + :private_key, :disable_mail, :show_community_spotlight_in_stream, :auto_follow_back, :auto_follow_back_aspect, :strip_exif - has_one :profile, serializer: Export::ProfileSerializer - has_many :aspects, each_serializer: Export::AspectSerializer + has_one :profile, serializer: FederationEntitySerializer + has_many :contact_groups, each_serializer: Export::AspectSerializer has_many :contacts, each_serializer: Export::ContactSerializer - has_many :posts, each_serializer: Export::PostSerializer - has_many :comments, each_serializer: Export::CommentSerializer + has_many :posts, each_serializer: Export::OwnPostSerializer + has_many :followed_tags + has_many :post_subscriptions - def comments - object.person.comments + has_many :relayables, each_serializer: Export::OwnRelayablesSerializer + + private + + def relayables + [*comments, *likes, *poll_participations] + end + + %i[comments likes poll_participations].each {|collection| + delegate collection, to: :person + } + + delegate :person, to: :object + + def contact_groups + object.aspects end + def private_key + object.serialized_private_key + end + + def followed_tags + object.followed_tags.map(&:name) + end + + def post_subscriptions + Post.subscribed_by(object).pluck(:guid) + end end end diff --git a/app/serializers/federation_entity_serializer.rb b/app/serializers/federation_entity_serializer.rb new file mode 100644 index 00000000000..0458fc73b54 --- /dev/null +++ b/app/serializers/federation_entity_serializer.rb @@ -0,0 +1,16 @@ +# This is an ActiveModel::Serializer based class which uses DiasporaFederation::Entity JSON serialization +# features in order to serialize local DB objects. To determine a type of entity class to use the same routines +# are used as for federation messages generation. +class FederationEntitySerializer < ActiveModel::Serializer + include SerializerPostProcessing + + private + + def modify_serializable_object(hash) + hash.merge(entity.to_json) + end + + def entity + @entity ||= Diaspora::Federation::Entities.build(object) + end +end diff --git a/app/serializers/serializer_post_processing.rb b/app/serializers/serializer_post_processing.rb new file mode 100644 index 00000000000..ea865be8abf --- /dev/null +++ b/app/serializers/serializer_post_processing.rb @@ -0,0 +1,20 @@ +# This module encapsulates knowledge about the way AMS works with the serializable object. +# The main responsibility of this module is to allow changing resulting object just before the +# JSON serialization happens. +module SerializerPostProcessing + # serializable_object output is used in AMS to produce a hash from input object that is passed to JSON serializer. + # serializable_object of ActiveModel::Serializer is not documented as officialy available API + # NOTE: if we ever move to AMS 0.10, this method was renamed there to serializable_hash + def serializable_object(options={}) + modify_serializable_object(super) + end + + # Users of this module may override this method in order to change serializable_object after + # the serializable hash generation and before its serialization. + def modify_serializable_object(hash) + hash + end + + # except is an array of keys that are excluded from serialized_object before JSON serialization + attr_accessor :except +end diff --git a/lib/account_deleter.rb b/lib/account_deleter.rb index 83c31c5285e..a466a3f2e43 100644 --- a/lib/account_deleter.rb +++ b/lib/account_deleter.rb @@ -101,7 +101,8 @@ def normal_ar_person_associates_to_delete end def ignored_or_special_ar_person_associations - %i(comments contacts notification_actors notifications owner profile conversation_visibilities pod) + %i[comments likes poll_participations contacts notification_actors notifications owner profile + conversation_visibilities pod] end def mark_account_deletion_complete diff --git a/lib/diaspora/exporter.rb b/lib/diaspora/exporter.rb index e6f65ac7a67..2c9e2790949 100644 --- a/lib/diaspora/exporter.rb +++ b/lib/diaspora/exporter.rb @@ -6,22 +6,23 @@ module Diaspora class Exporter - SERIALIZED_VERSION = '1.0' + SERIALIZED_VERSION = "2.0".freeze def initialize(user) @user = user end def execute - @export ||= JSON.generate serialized_user.merge(version: SERIALIZED_VERSION) + JSON.generate full_archive end private - def serialized_user - @serialized_user ||= Export::UserSerializer.new(@user).as_json + def full_archive + {version: SERIALIZED_VERSION} + .merge(Export::UserSerializer.new(@user).as_json) + .merge(Export::OthersDataSerializer.new(@user).as_json) end - end end diff --git a/lib/diaspora/exporter/posts_with_activity.rb b/lib/diaspora/exporter/posts_with_activity.rb index 9cbec03c17b..00461c55b18 100644 --- a/lib/diaspora/exporter/posts_with_activity.rb +++ b/lib/diaspora/exporter/posts_with_activity.rb @@ -3,12 +3,13 @@ class Exporter # This class allows to query posts where a person made any activity (submitted comments, # likes, participations or poll participations). class PostsWithActivity - # TODO: docs + # @param user [User] user who the activity belongs to (the one who liked, commented posts, etc) def initialize(user) @user = user end - # TODO: docs + # Create a request of posts with activity + # @return [Post::ActiveRecord_Relation] def query Post.from("(#{sql_union_all_activities}) AS posts") end @@ -26,7 +27,7 @@ def sql_union_all_activities end def all_activities - [comments_activity, likes_activity, subscriptions, polls_activity].compact + [comments_activity, likes_activity, subscriptions, polls_activity, reshares_activity] end def likes_activity @@ -41,6 +42,10 @@ def subscriptions other_people_posts.subscribed_by(user) end + def reshares_activity + other_people_posts.reshared_by(person) + end + def polls_activity StatusMessage.where.not(author_id: person.id).joins(:poll_participations) .where(poll_participations: {author_id: person.id}) diff --git a/lib/diaspora/shareable.rb b/lib/diaspora/shareable.rb index 96895f521a7..97078dd6a69 100644 --- a/lib/diaspora/shareable.rb +++ b/lib/diaspora/shareable.rb @@ -50,6 +50,14 @@ def subscribers end end + # Remote pods which are known to be subscribed to the post. Must include all pods which received the post in the + # past. + # + # @return [Array] The list of pods' URIs + def subscribed_pods_uris + Pod.find(subscribers.select(&:remote?).map(&:pod_id).uniq).map {|pod| pod.url_to("") } + end + module QueryMethods def owned_or_visible_by_user(user) with_visibility.where( diff --git a/lib/schemas/archive-format.json b/lib/schemas/archive-format.json new file mode 100644 index 00000000000..11ba60a724d --- /dev/null +++ b/lib/schemas/archive-format.json @@ -0,0 +1,255 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://diaspora.github.io/diaspora/schemas/archive_format.json", + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "email": { "type": "string" }, + "language": { "type": "string" }, + "username": { "type": "string" }, + "private_key": { "type": "string" }, + "disable_mail": { "type": "boolean" }, + "show_community_spotlight_in_stream": { "type": "boolean" }, + "auto_follow_back": { "type": "boolean" }, + "auto_follow_back_aspect": { + "type": [ + "string", + "null" + ] + }, + "strip_exif": { "type": "boolean" }, + + "profile": { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#definitions/profile" + }, + + "contact_groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "contacts_visible": { "type": "boolean" }, + "chat_enabled": { "type": "boolean" } + }, + "required": [ + "name" + ] + } + }, + + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sharing": { "type": "boolean" }, + "following": { "type": "boolean" }, + "receiving": { "type": "boolean" }, + "followed": { "type": "boolean" }, + "account_id": { "type": "string" }, + "contact_groups_membership": { + "type": "array", + "items": { "type": "string" } + }, + "person_name": { "type": "string" }, + "person_guid": { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/guid" + }, + "public_key": { "type": "string" } + }, + "required": [ + "sharing", + "following", + "receiving", + "followed", + "account_id", + "contact_groups_membership" + ] + } + }, + + "posts": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/posts" + }, + { + "oneOf": [ + { "$ref": "#/definitions/remote_subscription/public" }, + { "$ref": "#/definitions/remote_subscription/private" } + ] + } + ] + + } + }, + + "relayables": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/comment" + }, + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/like" + }, + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/poll_participation" + } + ] + } + }, + + "followed_tags": { + "type": "array", + "items": { + "type": "string" + } + }, + + "post_subscriptions": { + "type": "array", + "description": "GUIDs of posts for which changes we want to be subscribed", + "items": { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/guid" + } + } + }, + "required": [ + "username", + "email", + "private_key", + "profile" + ] + }, + "others_data": { + "type": "object", + "properties": { + "relayables": { + "type": "array", + "items": { + "allOf": [ + { + "oneOf": [ + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/comment" + }, + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/like" + }, + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/poll_participation" + } + ] + } + ] + } + }, + + "non_contact_authors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "account_id": { + "type": "string" + }, + "guid": { + "type": "string" + }, + "public_key": { + "type": "string" + } + }, + "required": [ + "account_id", + "guid", + "public_key" + ] + } + }, + + "posts": { + "type": "array", + "items": { + "$ref": "#/definitions/posts" + } + } + } + }, + "version": { + "type": "string", + "pattern": "^2\.0$" + } + }, + "required": [ + "user", + "version" + ], + "definitions": { + "posts": { + "oneOf": [ + { "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/status_message" }, + { "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/reshare" } + ] + }, + + "remote_subscription": { + "public": { + "type": "object", + "properties": { + "subscribed_pods_uris": { + "type": "array", + "items": { + "type": "string" + } + }, + "entity_data": { + "type": "object", + "properties": { + "public": { + "enum": [ true ] + } + }, + "required": [ + "public" + ] + } + }, + "required": [ + "entity_data" + ] + }, + + "private": { + "type": "object", + "properties": { + "subscribed_users_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + + "entity_data": { + "type": "object", + "properties": { + "public": { + "enum": [ false ] + } + }, + "required": [ + "public" + ] + } + } + } + } + } +} diff --git a/spec/factories.rb b/spec/factories.rb index 1f5f59d2e49..30c8cb5acff 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -149,9 +149,9 @@ def r_str end factory(:location) do - address "Fernsehturm Berlin, Berlin, Germany" - lat 52.520645 - lng 13.409779 + sequence(:address) {|n| "Fernsehturm Berlin, #{n}, Berlin, Germany" } + sequence(:lat) {|n| 52.520645 + 0.0000001 * n } + sequence(:lng) {|n| 13.409779 + 0.0000001 * n } end factory :participation do diff --git a/spec/integration/exporter_spec.rb b/spec/integration/exporter_spec.rb new file mode 100644 index 00000000000..8cea5501af1 --- /dev/null +++ b/spec/integration/exporter_spec.rb @@ -0,0 +1,444 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +describe Diaspora::Exporter do + let(:user) { FactoryGirl.create(:user_with_aspect) } + + context "output json" do + let(:json) { Diaspora::Exporter.new(user).execute } + + it "matches archive schema" do + DataGenerator.create( + user, + %i[generic_user_data activity status_messages_flavours work_aspect] + ) + + expect(JSON.parse(json)).to match_json_schema(:archive_schema) + end + + it "contains basic user data" do + user_properties = build_property_hash( + user, + %i[email username language disable_mail show_community_spotlight_in_stream auto_follow_back + auto_follow_back_aspect strip_exif], + private_key: :serialized_private_key + ) + + user_properties[:profile] = { + entity_type: "profile", + entity_data: build_property_hash( + user.profile, + %i[first_name last_name gender bio location image_url birthday searchable nsfw tag_string], + author: :diaspora_handle + ) + } + + expect(json).to include_json(user: user_properties) + end + + it "contains aspects" do + DataGenerator.create(user, :work_aspect) + + expect(json).to include_json( + user: { + "contact_groups": [ + { + "name": "generic", + "contacts_visible": true, + "chat_enabled": false + }, + { + "name": "Work", + "contacts_visible": false, + "chat_enabled": false + } + ] + } + ) + end + + it "contains contacts" do + friends = DataGenerator.create(user, Array.new(2, :mutual_friend)) + serialized_contacts = friends.map {|friend| + contact = Contact.find_by(person_id: friend.person_id) + hash = build_property_hash( + contact, + %i[sharing receiving person_guid person_name], + following: :sharing, followed: :receiving, account_id: :person_diaspora_handle + ) + hash[:public_key] = contact.person.serialized_public_key + hash[:contact_groups_membership] = contact.aspects.map(&:name) + hash + } + + expect(json).to include_json(user: {contacts: serialized_contacts}) + end + + it "contains a public status message" do + status_message = FactoryGirl.create(:status_message, author: user.person, public: true) + serialized = { + "subscribed_pods_uris": [AppConfig.pod_uri.to_s], + "entity_type": "status_message", + "entity_data": { + "author": user.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "public": true + } + } + + expect(json).to include_json(user: {posts: [serialized]}) + end + + it "contains a status message with subscribers" do + subscriber, status_message = DataGenerator.create(user, :status_message_with_subscriber) + serialized = { + "subscribed_users_ids": [subscriber.diaspora_handle], + "entity_type": "status_message", + "entity_data": { + "author": user.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "public": false + } + } + + expect(json).to include_json(user: {posts: [serialized]}) + end + + it "contains a status message with a poll" do + status_message = FactoryGirl.create(:status_message_with_poll, author: user.person) + serialized = { + "entity_type": "status_message", + "entity_data": { + "author": user.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "poll": { + "entity_type": "poll", + "entity_data": { + "guid": status_message.poll.guid, + "question": status_message.poll.question, + "poll_answers": status_message.poll.poll_answers.map {|answer| + { + "entity_type": "poll_answer", + "entity_data": { + "guid": answer.guid, + "answer": answer.answer + } + } + } + } + }, + "public": false + } + } + + expect(json).to include_json(user: {posts: [serialized]}) + end + + it "contains a status message with a photo" do + status_message = FactoryGirl.create(:status_message_with_photo, author: user.person) + + serialized = { + "entity_type": "status_message", + "entity_data": { + "author": user.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "photos": [ + { + "entity_type": "photo", + "entity_data": { + "guid": status_message.photos.first.guid, + "author": user.diaspora_handle, + "public": false, + "created_at": status_message.photos.first.created_at.iso8601, + "remote_photo_path": "#{AppConfig.pod_uri}uploads\/images\/", + "remote_photo_name": status_message.photos.first.remote_photo_name, + "status_message_guid": status_message.guid, + "height": 42, + "width": 23 + } + } + ], + "public": false + } + } + + expect(json).to include_json(user: {posts: [serialized]}) + end + + it "contains a status message with a location" do + status_message = FactoryGirl.create(:status_message_with_location, author: user.person) + + serialized = { + "entity_type": "status_message", + "entity_data": { + "author": user.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "location": { + "entity_type": "location", + "entity_data": { + "address": status_message.location.address, + "lat": status_message.location.lat, + "lng": status_message.location.lng + } + }, + "public": false + } + } + + expect(json).to include_json(user: {posts: [serialized]}) + end + + it "contains a reshare and its root" do + reshare = FactoryGirl.create(:reshare, author: user.person) + serialized_reshare = { + "subscribed_pods_uris": [reshare.root.author.pod.url_to(""), AppConfig.pod_uri.to_s], + "entity_type": "reshare", + "entity_data": { + "author": user.diaspora_handle, + "guid": reshare.guid, + "created_at": reshare.created_at.iso8601, + "public": true, + "root_author": reshare.root_author.diaspora_handle, + "root_guid": reshare.root_guid + } + } + + status_message = reshare.root + serialized_parent = { + "entity_type": "status_message", + "entity_data": { + "author": status_message.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "public": true + } + } + + expect(json).to include_json( + user: {posts: [serialized_reshare]}, + others_data: {posts: [serialized_parent]} + ) + end + + it "contains followed tags" do + tag_following = DataGenerator.create(user, :tag_following) + expect(json).to include_json(user: {followed_tags: [tag_following.tag.name]}) + end + + it "contains post subscriptions" do + subscription = DataGenerator.create(user, :subscription) + expect(json).to include_json(user: {post_subscriptions: [subscription.target.guid]}) + end + + it "contains a comment and the commented post" do + comment = FactoryGirl.create(:comment, author: user.person) + serialized_comment = { + "entity_type": "comment", + "entity_data": { + "author": user.diaspora_handle, + "guid": comment.guid, + "parent_guid": comment.parent.guid, + "text": comment.text, + "created_at": comment.created_at.iso8601 + }, + "property_order": %w[author guid parent_guid text created_at] + } + + status_message = comment.parent + serialized_post = { + "entity_type": "status_message", + "entity_data": { + "author": status_message.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "public": false + } + } + + expect(json).to include_json( + user: {relayables: [serialized_comment]}, + others_data: {posts: [serialized_post]} + ) + end + + it "contains a like and the liked post" do + like = FactoryGirl.create(:like, author: user.person) + serialized_like = { + "entity_type": "like", + "entity_data": { + "author": user.diaspora_handle, + "guid": like.guid, + "parent_guid": like.parent.guid, + "parent_type": like.target_type, + "positive": like.positive + }, + "property_order": %w[author guid parent_guid parent_type positive] + } + + status_message = like.target + serialized_post = { + "entity_type": "status_message", + "entity_data": { + "author": status_message.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "public": false + } + } + + expect(json).to include_json( + user: {relayables: [serialized_like]}, + others_data: {posts: [serialized_post]} + ) + end + + it "contains a poll participation and post with this poll" do + poll_participation = FactoryGirl.create(:poll_participation, author: user.person) + serialized_participation = { + "entity_type": "poll_participation", + "entity_data": { + "author": user.diaspora_handle, + "guid": poll_participation.guid, + "parent_guid": poll_participation.parent.guid, + "poll_answer_guid": poll_participation.poll_answer.guid + }, + "property_order": %w[author guid parent_guid poll_answer_guid] + } + + poll = poll_participation.poll + status_message = poll_participation.status_message + serialized_post = { + "entity_type": "status_message", + "entity_data": { + "author": status_message.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "poll": { + "entity_type": "poll", + "entity_data": { + "guid": poll.guid, + "question": poll.question, + "poll_answers": poll.poll_answers.map {|answer| + { + "entity_type": "poll_answer", + "entity_data": { + "guid": answer.guid, + "answer": answer.answer + } + } + } + } + }, + "public": false + } + } + + expect(json).to include_json( + user: {relayables: [serialized_participation]}, + others_data: {posts: [serialized_post]} + ) + end + + it "contains a comment for the user's post" do + status_message, comment = DataGenerator.create(user, :status_message_with_comment) + serialized = { + "entity_type": "comment", + "entity_data": { + "author": comment.diaspora_handle, + "guid": comment.guid, + "parent_guid": status_message.guid, + "text": comment.text, + "created_at": comment.created_at.iso8601, + "author_signature": Diaspora::Federation::Entities.build(comment).to_h[:author_signature] + }, + "property_order": %w[author guid parent_guid text created_at] + } + + expect(json).to include_json(others_data: {relayables: [serialized]}) + end + + it "contains a like for the user's post" do + status_message, like = DataGenerator.create(user, :status_message_with_like) + serialized = { + "entity_type": "like", + "entity_data": { + "author": like.diaspora_handle, + "guid": like.guid, + "parent_guid": status_message.guid, + "parent_type": like.target_type, + "positive": like.positive, + "author_signature": Diaspora::Federation::Entities.build(like).to_h[:author_signature] + }, + "property_order": %w[author guid parent_guid parent_type positive] + } + + expect(json).to include_json(others_data: {relayables: [serialized]}) + end + + it "contains a poll participation for the user's post" do + _, poll_participation = DataGenerator.create(user, :status_message_with_poll_participation) + serialized = { + "entity_type": "poll_participation", + "entity_data": { + "author": poll_participation.diaspora_handle, + "guid": poll_participation.guid, + "parent_guid": poll_participation.parent.guid, + "poll_answer_guid": poll_participation.poll_answer.guid, + "author_signature": Diaspora::Federation::Entities.build(poll_participation).to_h[:author_signature] + }, + "property_order": %w[author guid parent_guid poll_answer_guid] + } + + expect(json).to include_json(others_data: {relayables: [serialized]}) + end + + it "contains metadata of a non-contact author of a post where we commented" do + comment = FactoryGirl.create(:comment, author: user.person) + + author = comment.parent.author + expect(json).to include_json( + others_data: { + non_contact_authors: [ + { + "guid": author.guid, + "account_id": author.diaspora_handle, + "public_key": author.serialized_public_key + } + ] + } + ) + end + + def transform_value(value) + return value.iso8601 if value.is_a? Date + value + end + + def build_property_hash(object, direct_properties, aliased_properties={}) + props = direct_properties.map {|key| + [key, transform_value(object.send(key))] + }.to_h + + aliased = aliased_properties.map {|key, key_alias| + [key, object.send(key_alias)] + }.to_h + + props.merge(aliased) + end + end +end diff --git a/spec/lib/diaspora/exporter/posts_with_activity_spec.rb b/spec/lib/diaspora/exporter/posts_with_activity_spec.rb index c6335cbb458..4dc27495c84 100644 --- a/spec/lib/diaspora/exporter/posts_with_activity_spec.rb +++ b/spec/lib/diaspora/exporter/posts_with_activity_spec.rb @@ -8,7 +8,8 @@ user.person.likes.first.target, user.person.comments.first.parent, user.person.poll_participations.first.parent.status_message, - user.person.participations.first.target + user.person.participations.first.target, + user.person.posts.reshares.first.root ] } diff --git a/spec/lib/diaspora/exporter_spec.rb b/spec/lib/diaspora/exporter_spec.rb index d12cfd2d0bd..7db86e82fd0 100644 --- a/spec/lib/diaspora/exporter_spec.rb +++ b/spec/lib/diaspora/exporter_spec.rb @@ -1,86 +1,15 @@ -# Copyright (c) 2010-2011, Diaspora Inc. This file is -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. - -require Rails.root.join('lib', 'diaspora', 'exporter') - describe Diaspora::Exporter do - - before do - @user1 = alice - - @user1.person.profile.first_name = "