From 7374661e2fac78b9e30c75fb39165ab65dfbee81 Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Mon, 24 Apr 2017 13:41:49 +0300 Subject: [PATCH] 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 = "