Merge pull request #6726 from cmrd-senya/archive-schema

Update the user data export archive format.
This commit is contained in:
Benjamin Neff 2017-08-10 10:49:32 +02:00
commit 9d2d20e854
No known key found for this signature in database
GPG key ID: 971464C3F1A90194
56 changed files with 1982 additions and 341 deletions

View file

@ -25,6 +25,7 @@ If so, please delete it since it will prevent the federation from working proper
* Change sender for mails [#7495](https://github.com/diaspora/diaspora/pull/7495)
* Move back to top to the right to avoid misclicks [#7516](https://github.com/diaspora/diaspora/pull/7516)
* Include count in mobile post action link [#7520](https://github.com/diaspora/diaspora/pull/7520)
* Update the user data export archive format [#6726](https://github.com/diaspora/diaspora/pull/6726)
## Bug fixes

View file

@ -13,7 +13,8 @@ gem "unicorn-worker-killer", "0.4.4"
# Federation
gem "diaspora_federation-rails", "0.2.0"
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)
@ -294,7 +297,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

View file

@ -162,17 +162,18 @@ 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-json_schema (0.2.1)
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 +200,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)
@ -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,8 +777,9 @@ 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-json_schema (= 0.2.1)
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)
@ -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)

View file

@ -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
@ -76,8 +78,8 @@ class Person < ActiveRecord::Base
#not defensive
scope :in_aspects, ->(aspect_ids) {
joins(:contacts => :aspect_memberships).
where(:aspect_memberships => {:aspect_id => aspect_ids})
joins(contacts: :aspect_memberships)
.where(aspect_memberships: {aspect_id: aspect_ids}).distinct
}
scope :profile_tagged_with, ->(tag_name) {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,11 +0,0 @@
module Export
class CommentSerializer < ActiveModel::Serializer
attributes :guid,
:text,
:post_guid
def post_guid
object.post.guid
end
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -53,3 +53,8 @@ Feature: Change settings
And I go to the stream page
And I expand the publisher
Then I should see "Public" within ".aspect_dropdown"
Scenario: exporting profile data
When I click on the first selector "#account_data a"
Then I should see "Download my profile"
And I should have 1 email delivery

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,59 @@
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
# @param user [User] user who the activity belongs to (the one who liked, commented posts, etc)
def initialize(user)
@user = user
end
# Create a request of posts with activity
# @return [Post::ActiveRecord_Relation]
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, reshares_activity]
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 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})
end
def other_people_posts
Post.where.not(author_id: person.id)
end
end
end
end

View file

@ -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<String>] 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(

View file

@ -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"
]
}
}
}
}
}
}

View file

@ -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
@ -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"

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,24 @@
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,
user.person.posts.reshares.first.root
]
}
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

View file

@ -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 = "<script>"
@user1.person.profile.gender = "<script>"
@user1.person.profile.bio = "<script>"
@user1.person.profile.location = "<script>"
@user1.person.profile.save
@aspect = @user1.aspects.first
@aspect1 = @user1.aspects.create(:name => "Work", :contacts_visible => false)
@aspect.name = "<script>"
@aspect.save
end
context "json" do
def json
@json ||= JSON.parse Diaspora::Exporter.new(@user1).execute
json = Diaspora::Exporter.new(nil).execute
expect(json).to include_json(
version: "2.0",
user: "user_data",
others_date: "others_data"
)
end
it { matches :version, to: '1.0' }
it { matches :user, :name }
it { matches :user, :email }
it { matches :user, :username }
it { matches :user, :language }
it { matches :user, :disable_mail }
it { matches :user, :show_community_spotlight_in_stream }
it { matches :user, :auto_follow_back }
it { matches :user, :auto_follow_back_aspect }
it { matches :user, :strip_exif }
it { matches :user, :profile, :first_name, root: @user1.person.profile }
it { matches :user, :profile, :last_name, root: @user1.person.profile }
it { matches :user, :profile, :gender, root: @user1.person.profile }
it { matches :user, :profile, :bio, root: @user1.person.profile }
it { matches :user, :profile, :location, root: @user1.person.profile }
it { matches :user, :profile, :image_url, root: @user1.person.profile }
it { matches :user, :profile, :diaspora_handle, root: @user1.person.profile }
it { matches :user, :profile, :searchable, root: @user1.person.profile }
it { matches :user, :profile, :nsfw, root: @user1.person.profile }
it { matches_relation :aspects, :name,
:contacts_visible,
:chat_enabled }
it { matches_relation :contacts, :sharing,
:receiving,
:person_guid,
:person_name,
:person_first_name,
:person_diaspora_handle }
private
def matches(*fields, to: nil, root: @user1)
expected = to || root.send(fields.last)
expect(recurse_field(json, fields)).to eq expected
end
def matches_relation(relation, *fields, to: nil, root: @user1)
array = json['user'][to || relation.to_s]
fields.each do |field|
expected = root.send(relation).map(&:"#{field}")
expect(array.map { |f| f[field.to_s] }).to eq expected
end
end
def recurse_field(json, fields)
if fields.any?
recurse_field json[fields.shift.to_s], fields
else
json
end
end
end
end

View file

@ -175,6 +175,27 @@ describe Person, :type => :model do
expect(result[1].id).to eq(person1.id)
end
end
describe ".in_aspects" do
it "returns person that is in the aspect" do
aspect = FactoryGirl.create(:aspect)
contact = FactoryGirl.create(:contact, user: aspect.user)
aspect.contacts << contact
expect(Person.in_aspects([aspect.id])).to include(contact.person)
end
it "returns same person in multiple aspects only once" do
user = bob
contact = FactoryGirl.create(:contact, user: user)
ids = Array.new(2) do
aspect = FactoryGirl.create(:aspect, user: user, name: r_str)
aspect.contacts << contact
aspect.id
end
expect(Person.in_aspects(ids)).to eq([contact.person])
end
end
end
describe "delegating" do

View file

@ -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

View file

@ -9,6 +9,10 @@ describe StatusMessage, type: :model do
let!(:aspect) { user.aspects.first }
let(:status) { build(:status_message) }
it_behaves_like "a shareable" do
let(:object) { status }
end
describe "scopes" do
describe ".where_person_is_mentioned" do
it "returns status messages where the given person is mentioned" do

View file

@ -1,8 +0,0 @@
describe Export::CommentSerializer do
let(:comment) { create(:comment) }
subject(:json_output) { Export::CommentSerializer.new(comment).to_json }
it { is_expected.to include %("guid":"#{comment.guid}") }
it { is_expected.to include %("post_guid":"#{comment.post.guid}") }
it { is_expected.to include %("text":"#{comment.text}") }
end

View file

@ -0,0 +1,12 @@
describe Export::AspectSerializer do
let(:aspect) { FactoryGirl.create(:aspect) }
let(:serializer) { Export::AspectSerializer.new(aspect) }
it "has aspect attributes" do
expect(serializer.attributes).to eq(
name: aspect.name,
contacts_visible: aspect.contacts_visible,
chat_enabled: aspect.chat_enabled
)
end
end

View file

@ -0,0 +1,25 @@
describe Export::ContactSerializer do
let(:contact) { FactoryGirl.create(:contact) }
let(:serializer) { Export::ContactSerializer.new(contact) }
let(:aspect) { FactoryGirl.create(:aspect) }
it "has contact attributes" do
expect(serializer.attributes).to eq(
sharing: contact.sharing,
following: contact.sharing,
receiving: contact.receiving,
followed: contact.receiving,
person_guid: contact.person_guid,
person_name: contact.person_name,
account_id: contact.person_diaspora_handle,
public_key: contact.person.serialized_public_key
)
end
it "serializes aspects membership" do
contact.aspects << aspect
expect(Export::ContactSerializer).to serialize_association(:contact_groups_membership)
.with_objects(contact.aspects.map(&:name))
expect(serializer.associations[:contact_groups_membership]).to eq([aspect.name])
end
end

View file

@ -0,0 +1,43 @@
describe Export::OthersDataSerializer do
let(:user) { FactoryGirl.create(:user) }
let(:serializer) { Export::OthersDataSerializer.new(user) }
let(:others_posts) {
[
*user.person.likes.map(&:target),
*user.person.comments.map(&:parent),
*user.person.posts.reshares.map(&:root),
*user.person.poll_participations.map(&:status_message)
]
}
it "uses FederationEntitySerializer for array serializing relayables" do
sm = DataGenerator.new(user).status_message_with_activity
expect(Export::OthersDataSerializer).to serialize_association(:relayables)
.with_each_serializer(FederationEntitySerializer)
.with_objects([*sm.likes, *sm.comments, *sm.poll_participations])
serializer.associations
end
context "with user's activity" do
before do
DataGenerator.new(user).activity
end
it "uses FederationEntitySerializer for array serializing posts" do
expect(Export::OthersDataSerializer).to serialize_association(:posts)
.with_each_serializer(FederationEntitySerializer)
.with_objects(others_posts)
serializer.associations
end
it "uses PersonMetadataSerializer for array serializing non_contact_authors" do
non_contact_authors = others_posts.map(&:author)
expect(Export::OthersDataSerializer).to serialize_association(:non_contact_authors)
.with_each_serializer(Export::PersonMetadataSerializer)
.with_objects(non_contact_authors)
serializer.associations
end
end
end

View file

@ -0,0 +1,48 @@
describe Export::OwnPostSerializer do
let(:author) { FactoryGirl.create(:user_with_aspect).person }
before do
author.owner.share_with(FactoryGirl.create(:person), author.owner.aspects.first)
end
it_behaves_like "a federation entity serializer" do
let(:object) { create(:status_message_with_photo, author: author) }
end
let(:json) { Export::OwnPostSerializer.new(post, root: false).to_json }
context "with private post" do
let(:post) { create(:status_message_in_aspect, author: author) }
it "includes remote people subscriptions" do
expect(JSON.parse(json)["subscribed_users_ids"]).not_to be_empty
expect(json).to include_json(subscribed_users_ids: post.subscribers.map(&:diaspora_handle))
end
it "doesn't include remote pods subscriptions" do
expect(JSON.parse(json)).not_to have_key("subscribed_pods_uris")
end
end
context "with public post" do
let(:post) {
FactoryGirl.create(
:status_message_with_participations,
author: author,
participants: Array.new(2) { FactoryGirl.create(:person) },
public: true
)
}
it "includes pods subscriptions" do
expect(JSON.parse(json)["subscribed_pods_uris"]).not_to be_empty
expect(json).to include_json(
subscribed_pods_uris: post.subscribed_pods_uris.push(AppConfig.pod_uri.to_s)
)
end
it "doesn't include remote people subscriptions" do
expect(JSON.parse(json)).not_to have_key("subscribed_users_ids")
end
end
end

View file

@ -0,0 +1,9 @@
describe Export::OwnRelayablesSerializer do
let(:comment) { FactoryGirl.create(:comment) }
let!(:signature) { FactoryGirl.create(:comment_signature, comment: comment) }
let(:instance) { Export::OwnRelayablesSerializer.new(comment, root: false) }
it "doesn't include author signature to the entity data" do
expect(JSON.parse(instance.to_json)["entity_data"]).not_to have_key("author_signature")
end
end

View file

@ -0,0 +1,12 @@
describe Export::PersonMetadataSerializer do
let(:person) { FactoryGirl.create(:person) }
let(:serializer) { Export::PersonMetadataSerializer.new(person) }
it "has person metadata attributes" do
expect(serializer.attributes).to eq(
guid: person.guid,
account_id: person.diaspora_handle,
public_key: person.serialized_public_key
)
end
end

View file

@ -0,0 +1,79 @@
describe Export::UserSerializer do
let(:user) { FactoryGirl.create(:user) }
let(:serializer) { Export::UserSerializer.new(user, root: false) }
it "has basic user's attributes" do
expect(serializer.attributes).to eq(
username: user.username,
email: user.email,
language: user.language,
private_key: user.serialized_private_key,
disable_mail: user.disable_mail,
show_community_spotlight_in_stream: user.show_community_spotlight_in_stream,
auto_follow_back: user.auto_follow_back,
auto_follow_back_aspect: user.auto_follow_back_aspect,
strip_exif: user.strip_exif
)
end
it "uses FederationEntitySerializer to serialize user profile" do
expect(Export::UserSerializer).to serialize_association(:profile)
.with_serializer(FederationEntitySerializer)
.with_object(user.profile)
serializer.associations
end
it "uses AspectSerializer for array serializing contact_groups" do
DataGenerator.create(user, %i[first_aspect work_aspect])
expect(Export::UserSerializer).to serialize_association(:contact_groups)
.with_each_serializer(Export::AspectSerializer)
.with_objects([user.aspects.first, user.aspects.second])
serializer.associations
end
it "uses ContactSerializer for array serializing contacts" do
DataGenerator.create(user, %i[mutual_friend mutual_friend])
expect(Export::UserSerializer).to serialize_association(:contacts)
.with_each_serializer(Export::ContactSerializer)
.with_objects([user.contacts.first, user.contacts.second])
serializer.associations
end
it "uses OwnPostSerializer for array serializing posts" do
DataGenerator.create(user, %i[public_status_message private_status_message])
expect(Export::UserSerializer).to serialize_association(:posts)
.with_each_serializer(Export::OwnPostSerializer)
.with_objects([user.posts.first, user.posts.second])
serializer.associations
end
it "serializes followed tags" do
DataGenerator.create(user, %i[tag_following tag_following])
expect(Export::UserSerializer).to serialize_association(:followed_tags)
.with_objects([user.followed_tags.first.name, user.followed_tags.second.name])
serializer.associations
end
it "uses OwnRelayablesSerializer for array serializing relayables" do
DataGenerator.create(user, :activity)
objects = %i[comments likes poll_participations].map do |association|
user.person.send(association).first
end
expect(Export::UserSerializer).to serialize_association(:relayables)
.with_each_serializer(Export::OwnRelayablesSerializer)
.with_objects(objects)
serializer.associations
end
it "serializes post subscriptions" do
DataGenerator.create(user, %i[participation participation])
subscriptions = user.person.participations.map do |participation|
participation.target.guid
end
expect(Export::UserSerializer).to serialize_association(:post_subscriptions)
.with_objects(subscriptions)
serializer.associations
end
end

View file

@ -0,0 +1,17 @@
describe FederationEntitySerializer do
class TestEntity < DiasporaFederation::Entity
property :test, :string
end
let(:object) { double }
before do
# Mock a builder for a TestEntity that we define for this test
allow(Diaspora::Federation::Mappings).to receive(:builder_for).with(object.class).and_return(:test_entity)
allow(Diaspora::Federation::Entities).to receive(:test_entity).with(object) {
TestEntity.new(test: "asdf")
}
end
it_behaves_like "a federation entity serializer"
end

View file

@ -1,15 +0,0 @@
describe Export::PostSerializer do
let(:post) { create(:status_message_with_photo) }
subject(:json_output) { Export::PostSerializer.new(post).to_json }
it { is_expected.to include %("guid":"#{post.guid}") }
it { is_expected.to include %("text":"#{post.text}") }
it { is_expected.to include %("public":#{post.public}) }
it { is_expected.to include %("diaspora_handle":"#{post.diaspora_handle}") }
it { is_expected.to include %("type":"#{post.type}") }
it { is_expected.to include %("likes_count":#{post.likes_count}) }
it { is_expected.to include %("comments_count":#{post.comments_count}) }
it { is_expected.to include %("reshares_count":#{post.reshares_count}) }
it { is_expected.to include %("created_at":"#{post.created_at.to_s[0, 4]}) }
it { is_expected.to include %("created_at":"#{post.created_at.strftime('%FT%T.%LZ')}) }
end

View file

@ -0,0 +1,33 @@
describe SerializerPostProcessing do
describe "#modify_serializable_object" do
it "allows to modify serializable object of ActiveModel::Serializer ancestor" do
class TestSerializer < ActiveModel::Serializer
include SerializerPostProcessing
def modify_serializable_object(*)
{
custom_key: "custom_value"
}
end
end
serializer = TestSerializer.new({}, root: false)
expect(serializer).to receive(:modify_serializable_object).and_call_original
expect(serializer.to_json).to eq("{\"custom_key\":\"custom_value\"}")
end
end
describe "#except" do
it "allows to except a key from attributes" do
class TestSerializer2 < ActiveModel::Serializer
include SerializerPostProcessing
attributes :key_to_exclude
end
serializer = TestSerializer2.new({}, root: false)
serializer.except = [:key_to_exclude]
expect(serializer.to_json).to eq("{}")
end
end
end

View file

@ -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

View file

@ -0,0 +1,14 @@
shared_examples_for "a federation entity serializer" do
describe "#to_json" do
it "contains JSON serialized entity object" do
entity = nil
expect(Diaspora::Federation::Entities).to receive(:build)
.with(object)
.and_wrap_original do |original, object, &block|
entity = original.call(object, &block)
end
json = described_class.new(object, root: false).to_json
expect(json).to include_json(entity.to_json)
end
end
end

View file

@ -0,0 +1,18 @@
shared_examples_for "a shareable" do
describe "#subscribed_pods_uris" do
let(:pods) { Array.new(3) { FactoryGirl.create(:pod) } }
let(:subscribers) {
pods.map {|pod|
FactoryGirl.create(:person, pod: pod)
}
}
let(:pods_uris) {
pods.map {|pod| pod.url_to("") }
}
it "builds pod list basing on subscribers" do
expect(object).to receive(:subscribers).and_return(subscribers)
expect(object.subscribed_pods_uris).to match_array(pods_uris)
end
end
end

View file

@ -0,0 +1,57 @@
RSpec::Matchers.define :have_subscribers do
match do |posts|
posts.map(&:subscribers).delete_if(&:empty?).any?
end
end
# verifications of data generation (protect us from possible false positives in case of poor data preset)
describe DataGenerator do
let(:user) { FactoryGirl.create(:user) }
let(:generator) { DataGenerator.new(user) }
describe "#generic_user_data" do
it "creates different data for user" do
generator.generic_user_data
expect(user.aspects).not_to be_empty
expect(Post.subscribed_by(user)).not_to be_empty
end
end
describe "#status_messages_flavours" do
let(:user) { FactoryGirl.create(:user_with_aspect) }
it "creates posts of different types" do
expect(generator).to receive(:status_message_with_activity).and_call_original
generator.status_messages_flavours
expect(user.posts).not_to be_empty
expect(user.posts.where(public: true)).to have_subscribers
expect(user.posts.where(public: false)).to have_subscribers
end
end
describe "#status_message_with_activity" do
it "creates a status message where presented all possible types of activity" do
status_message = generator.status_message_with_activity
expect(status_message.likes).not_to be_empty
expect(status_message.comments).not_to be_empty
expect(status_message.poll_participations).not_to be_empty
end
end
describe "#activity" do
it "creates activity of different kinds" do
generator.activity
expect(user.posts.reshares).not_to be_empty
expect(user.person.likes).not_to be_empty
expect(user.person.comments).not_to be_empty
expect(user.person.poll_participations).not_to be_empty
end
end
describe "#status_message_with_subscriber" do
it "creates a status message with a subscriber" do
subscriber, status_message = DataGenerator.create(user, :status_message_with_subscriber)
expect(status_message.subscribers).to eq([subscriber.person])
end
end
end

View file

@ -2,23 +2,23 @@
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
describe 'making sure the spec runner works' do
it 'factory creates a user with a person saved' do
describe "making sure the spec runner works" do
it "factory creates a user with a person saved" do
user = FactoryGirl.create(:user)
loaded_user = User.find(user.id)
expect(loaded_user.person.owner_id).to eq(user.id)
end
describe 'fixtures' do
it 'loads fixtures' do
describe "fixtures" do
it "loads fixtures" do
expect(User.count).not_to eq(0)
end
end
describe "#connect_users" do
before do
@user1 = User.where(:username => 'alice').first
@user2 = User.where(:username => 'eve').first
@user1 = User.where(username: "alice").first
@user2 = User.where(username: "eve").first
@aspect1 = @user1.aspects.first
@aspect2 = @user2.aspects.first
@ -26,7 +26,7 @@ describe 'making sure the spec runner works' do
connect_users(@user1, @aspect1, @user2, @aspect2)
end
it 'connects the first user to the second' do
it "connects the first user to the second" do
contact = @user1.contact_for @user2.person
expect(contact).not_to be_nil
expect(@user1.contacts.reload.include?(contact)).to be true
@ -34,7 +34,7 @@ describe 'making sure the spec runner works' do
expect(contact.aspects.include?(@aspect1)).to be true
end
it 'connects the second user to the first' do
it "connects the second user to the first" do
contact = @user2.contact_for @user1.person
expect(contact).not_to be_nil
expect(@user2.contacts.reload.include?(contact)).to be true
@ -42,8 +42,8 @@ describe 'making sure the spec runner works' do
expect(contact.aspects.include?(@aspect2)).to be true
end
it 'allows posting after running' do
message = @user1.post(:status_message, :text => "Connection!", :to => @aspect1.id)
it "allows posting after running" do
message = @user1.post(:status_message, text: "Connection!", to: @aspect1.id)
expect(@user2.reload.visible_shareables(Post)).to include message
end
end
@ -71,14 +71,18 @@ describe 'making sure the spec runner works' do
describe "#post" do
it "creates a notification with a mention" do
expect {
alice.post(:status_message, :text => "@{Bob Grimn; #{bob.person.diaspora_handle}} you are silly", :to => alice.aspects.find_by_name('generic'))
alice.post(
:status_message,
text: "@{Bob Grimn; #{bob.person.diaspora_handle}} you are silly",
to: alice.aspects.find_by(name: "generic")
)
}.to change(Notification, :count).by(1)
end
end
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")
it "creates a conversation and a message" do
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")

View file

@ -15,6 +15,7 @@ require "webmock/rspec"
require "factory_girl"
require "sidekiq/testing"
require "shoulda/matchers"
require "diaspora_federation/schemas"
include HelperMethods
@ -133,6 +134,16 @@ RSpec.configure do |config|
end
config.include FactoryGirl::Syntax::Methods
config.include JSON::SchemaMatchers
config.json_schemas[:archive_schema] = "lib/schemas/archive-format.json"
JSON::Validator.add_schema(
JSON::Schema.new(
DiasporaFederation::Schemas.federation_entities,
Addressable::URI.parse(DiasporaFederation::Schemas::FEDERATION_ENTITIES_URI)
)
)
end
Shoulda::Matchers.configure do |config|

View file

@ -0,0 +1,183 @@
# This is a helper class for tests that is capable of generating different sets of data, which are possibly
# interrelated.
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.map {|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 public_status_message
FactoryGirl.create(:status_message, author: person, public: true)
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 status_message_with_location status_message_with_poll status_message_with_photo
status_message status_message_in_aspect reshare like comment poll_participation].each do |factory|
define_method factory do
FactoryGirl.create(factory, author: person)
end
end
alias subscription participation
%i[mention role].each do |factory|
define_method factory do
FactoryGirl.create(factory, person: person)
end
end
def status_message_with_activity
status_message_with_poll.tap {|post|
mutual_friend.like!(post)
mutual_friend.comment!(post, "1")
mutual_friend.participate_in_poll!(post, post.poll.poll_answers.first)
}
end
def status_message_with_comment
post = status_message_in_aspect
[post, mutual_friend.comment!(post, "some text")]
end
def status_message_with_like
post = status_message_in_aspect
[post, mutual_friend.like!(post)]
end
def status_message_with_poll_participation
post = status_message_with_poll
[
post,
mutual_friend.participate_in_poll!(post, post.poll.poll_answers.first)
]
end
def activity
reshare
like
comment
poll_participation
end
def work_aspect
user.aspects.create(name: "Work", contacts_visible: false)
end
def status_messages_flavours
public_status_message
status_message_with_location
status_message_with_activity
status_message_with_photo
status_message_with_poll
status_message_in_aspect
end
def status_message_with_subscriber
[
mutual_friend,
status_message_in_aspect
]
end
end

View file

@ -0,0 +1,79 @@
# This file contains custom RSpec matchers for AMS.
# NOTE: It was developed for AMS v0.9 and the API might be changed in future, so that should be examined when moving
# between stable versions of AMS (e.g. 0.9 to 0.10, or 0.9 to a possible 1.0).
# This is a matcher that tests a ActiveModel Serializer derivatives to run a serializer for
# an association with expected properties and data.
#
# This matcher is expected to be used in unit testing of ActiveModel Serializer derivatives.
#
# It is mostly a wrapper around RSpec::Mocks::Matchers::Receive matcher with expectations based on
# ActiveModel::Serializer internal API knowledge.
# NOTE: this matcher uses knowledge of AMS internals
RSpec::Matchers.define :serialize_association do |association_name|
match do |root_serializer_class|
association = fetch_association(root_serializer_class, association_name)
execute_receive_matcher_with(association)
end
# Sets expectation for a specific serializer class to be used for has_one association serialization
chain :with_serializer, :association_serializer_class
# Sets expectation for a specific serializer class to be user for has_many association serialization
chain :with_each_serializer, :each_serializer_class
# Sets expectation for actual data to be passed to the serializer of the association
chain :with_object, :association_object
alias_method :with_objects, :with_object
private
# subject is what comes from the expect method argument in usual notation.
# So this method is equivalent of calling `expect(subject).to receive(:build_serializer)` but valid within a custom
# RSpec matcher's match method.
def execute_receive_matcher_with(subject)
receive_matcher.matches?(subject)
end
def receive_matcher
receive(:build_serializer).and_wrap_original do |original, object, options, &block|
with_object_expectation(object)
original.call(object, options, &block).tap do |serializer|
expect(serializer).to be_an_instance_of(serializer_class) unless serializer_class.nil?
expect(serializer).to serialize_each_with(each_serializer_class) unless each_serializer_class.nil?
end
end
end
def with_object_expectation(object)
if association_object.is_a?(Array)
expect(object).to match_array(association_object)
elsif !association_object.nil?
expect(object).to eq(association_object)
end
end
def fetch_association(serializer_class, association_name)
serializer_class._associations[association_name]
end
def serializer_class
@serializer_class ||= pick_serializer_class
end
def pick_serializer_class
return association_serializer_class unless association_serializer_class.nil?
return ActiveModel::ArraySerializer unless each_serializer_class.nil?
end
end
# This serializer tests that ActiveModel::ArraySerializer uses specific serializer for each member object of the array.
# We could also set a mock expectation on each serializer, but it maybe overly complicated for our present
# requirements.
# NOTE: this matcher uses knowledge of AMS internals
RSpec::Matchers.define :serialize_each_with do |expected|
match do |actual|
actual.is_a?(ActiveModel::ArraySerializer) && actual.instance_variable_get("@each_serializer") == expected
end
end