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 | 160 ++++++---------------- spec/misc_spec.rb | 2 +- spec/shared_behaviors/account_deletion.rb | 47 +++---- spec/support/data_generator.rb | 122 +++++++++++++++++ 6 files changed, 193 insertions(+), 153 deletions(-) create mode 100644 spec/support/data_generator.rb diff --git a/spec/factories.rb b/spec/factories.rb index fadc1d172..1f5f59d2e 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -342,6 +342,11 @@ FactoryGirl.define do 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 f7406a3ef..720f28058 100644 --- a/spec/helper_methods.rb +++ b/spec/helper_methods.rb @@ -27,12 +27,12 @@ module HelperMethods 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 dfe3ee7c0..7c9346204 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 - - it "deletes all aspects" do - expect(bob.aspects).to be_empty - end - - 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 - end - - it_should_behave_like "it removes the person associations" +describe "deleteing account", type: :request do + def account_removal_method + AccountDeleter.new(subject.diaspora_handle).perform! + subject.reload end - context "remote person" do + context "of local user" do + subject(:user) { FactoryGirl.create(:user_with_aspect) } + 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 - - # 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 + DataGenerator.create(subject, :generic_user_data) end - it_should_behave_like "it removes the person associations" + 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 + end + + context "of remote person" do + subject(:person) { remote_raphael } + + before do + DataGenerator.create(subject, :generic_person_data) + end + + it_behaves_like "it removes the person associations" end end diff --git a/spec/misc_spec.rb b/spec/misc_spec.rb index 1e9851fa9..2661bc774 100644 --- a/spec/misc_spec.rb +++ b/spec/misc_spec.rb @@ -78,7 +78,7 @@ describe 'making sure the spec runner works' do 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 48500e0d0..19f7bf4fc 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 +shared_examples_for "it removes the person associations" do + RSpec::Matchers.define_negated_matcher :remain, :change - 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 - - 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 000000000..bf93a8bfe --- /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 423ea01fc..a0c9d727b 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 000000000..9cbec03c1 --- /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 000000000..c6335cbb4 --- /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 8dba27465..7e653bb65 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 000000000..5876c52cd --- /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 000000000..95595739f --- /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 000000000..f3cfc4801 --- /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 000000000..cac8c3ca0 --- /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 576717f79..58dbf12cf 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 fd7b44274..550fdc74d 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -174,6 +174,50 @@ describe Post, :type => :model do 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 002169503..639f7cbf4 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 798e80238..b07bb4e82 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 | 91 +--- 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, 1469 insertions(+), 171 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 639f7cbf4..30aa76763 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 b07bb4e82..a987f2839 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 d76cfcb92..3f20a3a84 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -37,7 +37,9 @@ class Person < ActiveRecord::Base 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 58b5bde1a..000000000 --- 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 be025304f..871486777 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 000000000..96a819c38 --- /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 000000000..150ecd427 --- /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 000000000..a38f83ccf --- /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 000000000..911062e39 --- /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 84b6a91ed..000000000 --- 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 b8eb2001f..000000000 --- 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 7e3b6b42d..09f26d3bd 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 000000000..0458fc73b --- /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 000000000..ea865be8a --- /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 83c31c528..a466a3f2e 100644 --- a/lib/account_deleter.rb +++ b/lib/account_deleter.rb @@ -101,7 +101,8 @@ class AccountDeleter 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 e6f65ac7a..2c9e27909 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 9cbec03c1..00461c55b 100644 --- a/lib/diaspora/exporter/posts_with_activity.rb +++ b/lib/diaspora/exporter/posts_with_activity.rb @@ -3,12 +3,13 @@ module Diaspora # 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 @@ module Diaspora 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 @@ module Diaspora 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 96895f521..97078dd6a 100644 --- a/lib/diaspora/shareable.rb +++ b/lib/diaspora/shareable.rb @@ -50,6 +50,14 @@ module Diaspora 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 000000000..11ba60a72 --- /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 1f5f59d2e..30c8cb5ac 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -149,9 +149,9 @@ FactoryGirl.define do 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 000000000..8cea5501a --- /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 c6335cbb4..4dc27495c 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 @@ describe Diaspora::Exporter::PostsWithActivity do 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 d12cfd2d0..7db86e82f 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 + describe "#execute" do + it "calls exporters and forms JSON" do + expect_any_instance_of(Export::UserSerializer).to receive(:as_json).and_return(user: "user_data") + expect_any_instance_of(Export::OthersDataSerializer).to receive(:as_json).and_return(others_date: "others_data") - before do - @user1 = alice - - @user1.person.profile.first_name = "