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.
This commit is contained in:
cmrd Senya 2017-04-24 13:41:49 +03:00
parent 1b1db3bb0c
commit 7374661e2f
No known key found for this signature in database
GPG key ID: 5FCC5BA680E67BFE
41 changed files with 1469 additions and 171 deletions

View file

@ -13,6 +13,7 @@ gem "unicorn-worker-killer", "0.4.4"
# Federation # Federation
gem "diaspora_federation-json_schema", "0.2.1"
gem "diaspora_federation-rails", "0.2.1" gem "diaspora_federation-rails", "0.2.1"
# API and JSON # API and JSON
@ -277,6 +278,8 @@ group :test do
gem "fixture_builder", "0.5.0" gem "fixture_builder", "0.5.0"
gem "fuubar", "2.2.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" gem "test_after_commit", "1.1.0"
# Cucumber (integration tests) # Cucumber (integration tests)

View file

@ -168,6 +168,7 @@ GEM
nokogiri (~> 1.6, >= 1.6.8) nokogiri (~> 1.6, >= 1.6.8)
typhoeus (~> 1.0) typhoeus (~> 1.0)
valid (~> 1.0) valid (~> 1.0)
diaspora_federation-json_schema (0.2.1)
diaspora_federation-rails (0.2.1) diaspora_federation-rails (0.2.1)
actionpack (>= 4.2, < 6) actionpack (>= 4.2, < 6)
diaspora_federation (= 0.2.1) diaspora_federation (= 0.2.1)
@ -334,6 +335,9 @@ GEM
url_safe_base64 url_safe_base64
json-schema (2.8.0) json-schema (2.8.0)
addressable (>= 2.4) addressable (>= 2.4)
json-schema-rspec (0.0.4)
json-schema (~> 2.5)
rspec
jsonpath (0.8.5) jsonpath (0.8.5)
multi_json multi_json
jwt (1.5.6) jwt (1.5.6)
@ -582,6 +586,7 @@ GEM
rspec-expectations (3.6.0) rspec-expectations (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.6.0)
rspec-json_expectations (2.1.0)
rspec-mocks (3.6.0) rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.6.0)
@ -772,6 +777,7 @@ DEPENDENCIES
devise (= 4.3.0) devise (= 4.3.0)
devise_lastseenable (= 0.0.6) devise_lastseenable (= 0.0.6)
diaspora-prosody-config (= 0.0.7) diaspora-prosody-config (= 0.0.7)
diaspora_federation-json_schema (= 0.2.1)
diaspora_federation-rails (= 0.2.1) diaspora_federation-rails (= 0.2.1)
diaspora_federation-test (= 0.2.1) diaspora_federation-test (= 0.2.1)
entypo-rails (= 3.0.0) entypo-rails (= 3.0.0)
@ -800,6 +806,7 @@ DEPENDENCIES
js_image_paths (= 0.1.0) js_image_paths (= 0.1.0)
json (= 2.1.0) json (= 2.1.0)
json-schema (= 2.8.0) json-schema (= 2.8.0)
json-schema-rspec (= 0.0.4)
leaflet-rails (= 1.1.0) leaflet-rails (= 1.1.0)
logging-rails (= 0.6.0) logging-rails (= 0.6.0)
markerb (= 1.1.0) markerb (= 1.1.0)
@ -858,6 +865,7 @@ DEPENDENCIES
rb-inotify (= 0.9.10) rb-inotify (= 0.9.10)
redcarpet (= 3.4.0) redcarpet (= 3.4.0)
responders (= 2.4.0) responders (= 2.4.0)
rspec-json_expectations (~> 2.1)
rspec-rails (= 3.6.0) rspec-rails (= 3.6.0)
rubocop (= 0.49.1) rubocop (= 0.49.1)
ruby-oembed (= 0.12.0) 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 :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 :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 :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 :participations, :foreign_key => :author_id, :dependent => :destroy
has_many :poll_participations, foreign_key: :author_id, dependent: :destroy
has_many :conversation_visibilities has_many :conversation_visibilities
has_many :roles has_many :roles

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 class ContactSerializer < ActiveModel::Serializer
attributes :sharing, attributes :sharing,
:receiving, :receiving,
:following,
:followed,
:person_guid, :person_guid,
:person_name, :person_name,
:person_first_name, :account_id,
:person_diaspora_handle :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
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 module Export
class UserSerializer < ActiveModel::Serializer class UserSerializer < ActiveModel::Serializer
attributes :name, attributes :username,
:email, :email,
:language, :language,
:username, :private_key,
:serialized_private_key,
:disable_mail, :disable_mail,
:show_community_spotlight_in_stream, :show_community_spotlight_in_stream,
:auto_follow_back, :auto_follow_back,
:auto_follow_back_aspect, :auto_follow_back_aspect,
:strip_exif :strip_exif
has_one :profile, serializer: Export::ProfileSerializer has_one :profile, serializer: FederationEntitySerializer
has_many :aspects, each_serializer: Export::AspectSerializer has_many :contact_groups, each_serializer: Export::AspectSerializer
has_many :contacts, each_serializer: Export::ContactSerializer has_many :contacts, each_serializer: Export::ContactSerializer
has_many :posts, each_serializer: Export::PostSerializer has_many :posts, each_serializer: Export::OwnPostSerializer
has_many :comments, each_serializer: Export::CommentSerializer has_many :followed_tags
has_many :post_subscriptions
def comments has_many :relayables, each_serializer: Export::OwnRelayablesSerializer
object.person.comments
private
def relayables
[*comments, *likes, *poll_participations]
end 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
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

@ -101,7 +101,8 @@ class AccountDeleter
end end
def ignored_or_special_ar_person_associations 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 end
def mark_account_deletion_complete def mark_account_deletion_complete

View file

@ -6,22 +6,23 @@ module Diaspora
class Exporter class Exporter
SERIALIZED_VERSION = '1.0' SERIALIZED_VERSION = "2.0".freeze
def initialize(user) def initialize(user)
@user = user @user = user
end end
def execute def execute
@export ||= JSON.generate serialized_user.merge(version: SERIALIZED_VERSION) JSON.generate full_archive
end end
private private
def serialized_user def full_archive
@serialized_user ||= Export::UserSerializer.new(@user).as_json {version: SERIALIZED_VERSION}
.merge(Export::UserSerializer.new(@user).as_json)
.merge(Export::OthersDataSerializer.new(@user).as_json)
end end
end end
end end

View file

@ -3,12 +3,13 @@ module Diaspora
# This class allows to query posts where a person made any activity (submitted comments, # This class allows to query posts where a person made any activity (submitted comments,
# likes, participations or poll participations). # likes, participations or poll participations).
class PostsWithActivity class PostsWithActivity
# TODO: docs # @param user [User] user who the activity belongs to (the one who liked, commented posts, etc)
def initialize(user) def initialize(user)
@user = user @user = user
end end
# TODO: docs # Create a request of posts with activity
# @return [Post::ActiveRecord_Relation]
def query def query
Post.from("(#{sql_union_all_activities}) AS posts") Post.from("(#{sql_union_all_activities}) AS posts")
end end
@ -26,7 +27,7 @@ module Diaspora
end end
def all_activities def all_activities
[comments_activity, likes_activity, subscriptions, polls_activity].compact [comments_activity, likes_activity, subscriptions, polls_activity, reshares_activity]
end end
def likes_activity def likes_activity
@ -41,6 +42,10 @@ module Diaspora
other_people_posts.subscribed_by(user) other_people_posts.subscribed_by(user)
end end
def reshares_activity
other_people_posts.reshared_by(person)
end
def polls_activity def polls_activity
StatusMessage.where.not(author_id: person.id).joins(:poll_participations) StatusMessage.where.not(author_id: person.id).joins(:poll_participations)
.where(poll_participations: {author_id: person.id}) .where(poll_participations: {author_id: person.id})

View file

@ -50,6 +50,14 @@ module Diaspora
end end
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 module QueryMethods
def owned_or_visible_by_user(user) def owned_or_visible_by_user(user)
with_visibility.where( 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 end
factory(:location) do factory(:location) do
address "Fernsehturm Berlin, Berlin, Germany" sequence(:address) {|n| "Fernsehturm Berlin, #{n}, Berlin, Germany" }
lat 52.520645 sequence(:lat) {|n| 52.520645 + 0.0000001 * n }
lng 13.409779 sequence(:lng) {|n| 13.409779 + 0.0000001 * n }
end end
factory :participation do factory :participation do

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

@ -8,7 +8,8 @@ describe Diaspora::Exporter::PostsWithActivity do
user.person.likes.first.target, user.person.likes.first.target,
user.person.comments.first.parent, user.person.comments.first.parent,
user.person.poll_participations.first.parent.status_message, user.person.poll_participations.first.parent.status_message,
user.person.participations.first.target user.person.participations.first.target,
user.person.posts.reshares.first.root
] ]
} }

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 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 json = Diaspora::Exporter.new(nil).execute
@user1 = alice expect(json).to include_json(
version: "2.0",
@user1.person.profile.first_name = "<script>" user: "user_data",
@user1.person.profile.gender = "<script>" others_date: "others_data"
@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
end 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
end end

View file

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

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

@ -15,6 +15,7 @@ require "webmock/rspec"
require "factory_girl" require "factory_girl"
require "sidekiq/testing" require "sidekiq/testing"
require "shoulda/matchers" require "shoulda/matchers"
require "diaspora_federation/schemas"
include HelperMethods include HelperMethods
@ -133,6 +134,16 @@ RSpec.configure do |config|
end end
config.include FactoryGirl::Syntax::Methods 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 end
Shoulda::Matchers.configure do |config| Shoulda::Matchers.configure do |config|

View file

@ -1,4 +1,5 @@
# TODO: docs # This is a helper class for tests that is capable of generating different sets of data, which are possibly
# interrelated.
class DataGenerator class DataGenerator
def person def person
@person || user.person @person || user.person
@ -23,7 +24,7 @@ class DataGenerator
if type.is_a? Symbol if type.is_a? Symbol
generator.send(type) generator.send(type)
elsif type.is_a? Array elsif type.is_a? Array
type.each {|type| type.map {|type|
generator.send(type) generator.send(type)
} }
end end
@ -100,23 +101,83 @@ class DataGenerator
user.aspects.first || FactoryGirl.create(:aspect, user: user) user.aspects.first || FactoryGirl.create(:aspect, user: user)
end end
def public_status_message
FactoryGirl.create(:status_message, author: person, public: true)
end
def private_status_message def private_status_message
post = FactoryGirl.create(:status_message, author: person) post = FactoryGirl.create(:status_message, author: person)
person.contacts.each do |contact| person.contacts.each do |contact|
ShareVisibility.create!(user_id: contact.user.id, shareable: post) ShareVisibility.create!(user_id: contact.user_id, shareable: post)
end end
end end
%i(photo participation).each do |factory| %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 define_method factory do
FactoryGirl.create(factory, author: person) FactoryGirl.create(factory, author: person)
end end
end end
alias subscription participation
%i[mention role].each do |factory| %i[mention role].each do |factory|
define_method factory do define_method factory do
FactoryGirl.create(factory, person: person) FactoryGirl.create(factory, person: person)
end end
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 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