diff --git a/Gemfile b/Gemfile
index e084db3..d4587d2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -51,7 +51,9 @@ group :test do
# test helpers
gem "factory_girl_rails", "~> 4.7"
gem "fixture_builder", "0.5.0"
+ gem "json-schema-rspec", "0.0.4"
gem "rspec-collection_matchers", "~> 1.1.2"
+ gem "rspec-json_expectations", "~> 2.1"
gem "webmock", "~> 2.0"
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 3808d87..ecb667f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -7,6 +7,7 @@ PATH
nokogiri (~> 1.6, >= 1.6.8)
typhoeus (~> 1.0)
valid (~> 1.0)
+ diaspora_federation-json_schema (0.1.8)
diaspora_federation-rails (0.1.8)
diaspora_federation (= 0.1.8)
rails (>= 4.2, < 6)
@@ -117,6 +118,11 @@ GEM
multi_xml (>= 0.5.2)
i18n (0.8.1)
json (1.8.6)
+ json-schema (2.7.0)
+ addressable (>= 2.4)
+ json-schema-rspec (0.0.4)
+ json-schema (~> 2.5)
+ rspec
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
@@ -217,6 +223,7 @@ GEM
rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
+ rspec-json_expectations (2.1.0)
rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
@@ -294,6 +301,7 @@ PLATFORMS
DEPENDENCIES
codeclimate-test-reporter (~> 1.0.0)
diaspora_federation!
+ diaspora_federation-json_schema!
diaspora_federation-rails!
diaspora_federation-test!
factory_girl_rails (~> 4.7)
@@ -301,6 +309,7 @@ DEPENDENCIES
fuubar (= 2.2.0)
guard-rspec
guard-rubocop
+ json-schema-rspec (= 0.0.4)
logging-rails (= 0.5.0)
nyan-cat-formatter
pronto (= 0.8.1)
@@ -309,6 +318,7 @@ DEPENDENCIES
pry-byebug
rspec-collection_matchers (~> 1.1.2)
rspec-core (~> 3.5.1)
+ rspec-json_expectations (~> 2.1)
rspec-rails (~> 3.5.1)
rubocop (= 0.47.1)
simplecov (= 0.13.0)
@@ -321,4 +331,4 @@ DEPENDENCIES
yard
BUNDLED WITH
- 1.14.5
+ 1.14.6
diff --git a/diaspora_federation-json_schema.gemspec b/diaspora_federation-json_schema.gemspec
new file mode 100644
index 0000000..f5a982b
--- /dev/null
+++ b/diaspora_federation-json_schema.gemspec
@@ -0,0 +1,19 @@
+$LOAD_PATH.push File.expand_path("../lib", __FILE__)
+
+# Maintain your gem's version:
+require "diaspora_federation/version"
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "diaspora_federation-json_schema"
+ s.version = DiasporaFederation::VERSION
+ s.authors = ["Benjamin Neff", "cmrd Senya"]
+ s.email = ["benjamin@coding4.coffee", "senya@riseup.net"]
+ s.homepage = "https://github.com/diaspora/diaspora_federation"
+ s.summary = "diaspora* federation json schemas"
+ s.description = "This gem provides JSON schemas (currently one schema) for "\
+ "validating JSON serialized federation objects."
+ s.license = "AGPL-3.0"
+
+ s.files = Dir["lib/diaspora_federation/schemas/*.json"]
+end
diff --git a/lib/diaspora_federation.rb b/lib/diaspora_federation.rb
index e62363a..dd632aa 100644
--- a/lib/diaspora_federation.rb
+++ b/lib/diaspora_federation.rb
@@ -10,6 +10,7 @@ require "diaspora_federation/validators"
require "diaspora_federation/http_client"
require "diaspora_federation/entities"
+require "diaspora_federation/parsers"
require "diaspora_federation/discovery"
require "diaspora_federation/salmon"
diff --git a/lib/diaspora_federation/entities/message.rb b/lib/diaspora_federation/entities/message.rb
index d9a983b..00cccea 100644
--- a/lib/diaspora_federation/entities/message.rb
+++ b/lib/diaspora_federation/entities/message.rb
@@ -38,6 +38,20 @@ module DiasporaFederation
super.tap {|hash| hash[:created_at] = created_at.utc.iso8601 }
end
+ # @deprecated remove after {Message} doesn't include {Relayable} anymore
+ private_class_method def self.xml_parser_class
+ DiasporaFederation::Parsers::XmlParser
+ end
+
+ # Default implementation, don't verify signatures for a {Message}.
+ # @see Entity.from_hash
+ # @deprecated remove after {Message} doesn't include {Relayable} anymore
+ # @param [Hash] properties hash
+ # @return [Entity] instance
+ def self.from_hash(hash)
+ new({parent_guid: nil, parent: nil}.merge(hash))
+ end
+
private
# @deprecated remove after {Message} doesn't include {Relayable} anymore
@@ -62,15 +76,6 @@ module DiasporaFederation
super
end
end
-
- # Default implementation, don't verify signatures for a {Message}.
- # @see Entity.populate_entity
- # @deprecated remove after {Message} doesn't include {Relayable} anymore
- # @param [Nokogiri::XML::Element] root_node xml nodes
- # @return [Entity] instance
- private_class_method def self.populate_entity(root_node)
- new({parent_guid: nil, parent: nil}.merge(entity_data(root_node)))
- end
end
end
end
diff --git a/lib/diaspora_federation/entities/participation.rb b/lib/diaspora_federation/entities/participation.rb
index 075e05d..d42fd53 100644
--- a/lib/diaspora_federation/entities/participation.rb
+++ b/lib/diaspora_federation/entities/participation.rb
@@ -22,6 +22,14 @@ module DiasporaFederation
sender == author
end
+ # hackaround hacky from_hash override
+ # @deprecated remove after {Participation} doesn't include {Relayable} anymore
+ def enriched_properties
+ super.tap {|hash|
+ hash.delete(:parent) if hash[:parent].nil?
+ }
+ end
+
# Validates that the parent exists and the parent author is local
def validate_parent
parent = DiasporaFederation.callbacks.trigger(:fetch_related_entity, parent_type, parent_guid)
@@ -29,11 +37,21 @@ module DiasporaFederation
end
# Don't verify signatures for a {Participation}. Validate that the parent is local.
- # @see Entity.populate_entity
- # @param [Nokogiri::XML::Element] root_node xml nodes
+ # @see Entity.from_hash
+ # @param [Hash] hash entity initialization hash
# @return [Entity] instance
- private_class_method def self.populate_entity(root_node)
- new(entity_data(root_node).merge(parent: nil)).tap(&:validate_parent)
+ def self.from_hash(hash)
+ new(hash.merge(parent: nil)).tap(&:validate_parent)
+ end
+
+ # @deprecated remove after {Participation} doesn't include {Relayable} anymore
+ private_class_method def self.xml_parser_class
+ DiasporaFederation::Parsers::XmlParser
+ end
+
+ # @deprecated remove after {Participation} doesn't include {Relayable} anymore
+ private_class_method def self.json_parser_class
+ DiasporaFederation::Parsers::JsonParser
end
# Raised, if the parent is not owned by the receiving pod.
diff --git a/lib/diaspora_federation/entities/related_entity.rb b/lib/diaspora_federation/entities/related_entity.rb
index 850babe..b98bf14 100644
--- a/lib/diaspora_federation/entities/related_entity.rb
+++ b/lib/diaspora_federation/entities/related_entity.rb
@@ -28,6 +28,10 @@ module DiasporaFederation
def to_xml
nil
end
+
+ def to_json
+ nil
+ end
end
end
end
diff --git a/lib/diaspora_federation/entities/relayable.rb b/lib/diaspora_federation/entities/relayable.rb
index 7a06ddf..4ed432f 100644
--- a/lib/diaspora_federation/entities/relayable.rb
+++ b/lib/diaspora_federation/entities/relayable.rb
@@ -14,9 +14,9 @@ module DiasporaFederation
# @return [Array] order from xml
attr_reader :xml_order
- # Additional properties from parsed xml
- # @return [Hash] additional xml elements
- attr_reader :additional_xml_elements
+ # Additional properties from parsed input object
+ # @return [Hash] additional elements
+ attr_reader :additional_data
# On inclusion of this module the required properties for a relayable are added to the object that includes it.
#
@@ -62,18 +62,18 @@ module DiasporaFederation
entity :parent, Entities::RelatedEntity
end
- klass.extend ParseXML
+ klass.extend Parsing
end
# Initializes a new relayable Entity with order and additional xml elements
#
# @param [Hash] data entity data
# @param [Array] xml_order order from xml
- # @param [Hash] additional_xml_elements additional xml elements
+ # @param [Hash] additional_data additional xml elements
# @see DiasporaFederation::Entity#initialize
- def initialize(data, xml_order=nil, additional_xml_elements={})
+ def initialize(data, xml_order=nil, additional_data={})
@xml_order = xml_order.reject {|name| name =~ /signature/ } if xml_order
- @additional_xml_elements = additional_xml_elements
+ @additional_data = additional_data
super(data)
end
@@ -96,6 +96,15 @@ module DiasporaFederation
"#{super}#{":#{parent_type}" if respond_to?(:parent_type)}:#{parent_guid}"
end
+ def to_json
+ super.merge!(property_order: signature_order).tap {|json_hash|
+ missing_properties = json_hash[:property_order] - json_hash[:entity_data].keys
+ missing_properties.each {|property|
+ json_hash[:entity_data][property] = nil
+ }
+ }
+ end
+
private
# Check that signature is a correct signature
@@ -151,7 +160,7 @@ module DiasporaFederation
#
# @return [Hash] properties with updated signatures
def enriched_properties
- super.merge(additional_xml_elements).tap do |hash|
+ super.merge(additional_data).tap do |hash|
hash[:author_signature] = author_signature || sign_with_author
hash[:parent_author_signature] = parent_author_signature || sign_with_parent_author_if_available.to_s
end
@@ -179,41 +188,32 @@ module DiasporaFederation
# @return [String] signature data string
def signature_data
- data = normalized_properties.merge(additional_xml_elements)
+ data = normalized_properties.merge(additional_data)
signature_order.map {|name| data[name] }.join(";")
end
- # Override class methods from {Entity} to parse the xml
- module ParseXML
- private
+ # Override class methods from {Entity} to parse serialized data
+ module Parsing
+ # Does the same job as Entity.from_hash except of the following differences:
+ # 1) unknown properties from the properties_hash are stored to additional_data of the relayable instance
+ # 2) parent entity fetch is attempted
+ # 3) signatures verification is performed; property_order is used as the order in which properties are composed
+ # to compute signatures
+ # 4) unknown properties' keys must be of String type
+ #
+ # @see Entity.from_hash
+ def from_hash(properties_hash, property_order)
+ # Use all known properties to build the Entity (entity_data). All additional elements
+ # are respected and attached to a hash as string (additional_data). This is needed
+ # to support receiving objects from the future versions of diaspora*, where new elements may have been added.
+ additional_data = properties_hash.reject {|key, _| class_props.has_key?(key) }
- # @param [Nokogiri::XML::Element] root_node xml nodes
- # @return [Entity] instance
- def populate_entity(root_node)
- # Use all known properties to build the Entity (entity_data). All additional xml elements
- # are respected and attached to a hash as string (additional_xml_elements). It also remembers
- # the order of the xml-nodes (xml_order). This is needed to support receiving objects from
- # the future versions of diaspora*, where new elements may have been added.
- entity_data = {}
- additional_xml_elements = {}
-
- xml_order = root_node.element_children.map do |child|
- xml_name = child.name
- property = find_property_for_xml_name(xml_name)
-
- if property
- entity_data[property] = parse_element_from_node(xml_name, class_props[property], root_node)
- property
- else
- additional_xml_elements[xml_name] = child.text
- xml_name
- end
- end
-
- fetch_parent(entity_data)
- new(entity_data, xml_order, additional_xml_elements).tap(&:verify_signatures)
+ fetch_parent(properties_hash)
+ new(properties_hash, property_order, additional_data).tap(&:verify_signatures)
end
+ private
+
def fetch_parent(data)
type = data.fetch(:parent_type) {
break self::PARENT_TYPE if const_defined?(:PARENT_TYPE)
@@ -232,6 +232,14 @@ module DiasporaFederation
Federation::Fetcher.fetch_public(data[:author], type, guid)
data[:parent] = DiasporaFederation.callbacks.trigger(:fetch_related_entity, type, guid)
end
+
+ def xml_parser_class
+ DiasporaFederation::Parsers::RelayableXmlParser
+ end
+
+ def json_parser_class
+ DiasporaFederation::Parsers::RelayableJsonParser
+ end
end
# Raised, if creating the author_signature fails, because the private key was not found
diff --git a/lib/diaspora_federation/entities/relayable_retraction.rb b/lib/diaspora_federation/entities/relayable_retraction.rb
index 00f0768..85570ca 100644
--- a/lib/diaspora_federation/entities/relayable_retraction.rb
+++ b/lib/diaspora_federation/entities/relayable_retraction.rb
@@ -69,16 +69,14 @@ module DiasporaFederation
"RelayableRetraction:#{target_type}:#{target_guid}"
end
- private
-
- # @param [Nokogiri::XML::Element] root_node xml nodes
# @return [Retraction] instance
- private_class_method def self.populate_entity(root_node)
- entity_data = entity_data(root_node)
- entity_data[:target] = Retraction.send(:fetch_target, entity_data[:target_type], entity_data[:target_guid])
- new(entity_data).to_retraction
+ def self.from_hash(hash)
+ hash[:target] = Retraction.send(:fetch_target, hash[:target_type], hash[:target_guid])
+ new(hash).to_retraction
end
+ private
+
# It updates also the signatures with the keys of the author and the parent
# if the signatures are not there yet and if the keys are available.
#
diff --git a/lib/diaspora_federation/entities/request.rb b/lib/diaspora_federation/entities/request.rb
index 7290974..b8b44c4 100644
--- a/lib/diaspora_federation/entities/request.rb
+++ b/lib/diaspora_federation/entities/request.rb
@@ -29,10 +29,9 @@ module DiasporaFederation
"Request:#{author}:#{recipient}"
end
- # @param [Nokogiri::XML::Element] root_node xml nodes
# @return [Retraction] instance
- private_class_method def self.populate_entity(root_node)
- super(root_node).to_contact
+ def self.from_hash(hash)
+ super.to_contact
end
end
end
diff --git a/lib/diaspora_federation/entities/reshare.rb b/lib/diaspora_federation/entities/reshare.rb
index cd831d7..308e53b 100644
--- a/lib/diaspora_federation/entities/reshare.rb
+++ b/lib/diaspora_federation/entities/reshare.rb
@@ -35,11 +35,10 @@ module DiasporaFederation
end
# Fetch root post after parse
- # @see Entity.populate_entity
- # @param [Nokogiri::XML::Element] root_node xml nodes
+ # @see Entity.from_hash
# @return [Entity] instance
- private_class_method def self.populate_entity(root_node)
- super(root_node).tap(&:fetch_root)
+ def self.from_hash(hash)
+ super.tap(&:fetch_root)
end
end
end
diff --git a/lib/diaspora_federation/entities/retraction.rb b/lib/diaspora_federation/entities/retraction.rb
index cf1df09..67799b8 100644
--- a/lib/diaspora_federation/entities/retraction.rb
+++ b/lib/diaspora_federation/entities/retraction.rb
@@ -39,12 +39,11 @@ module DiasporaFederation
"Retraction:#{target_type}:#{target_guid}"
end
- # @param [Nokogiri::XML::Element] root_node xml nodes
+ # @see Entity.from_hash
# @return [Retraction] instance
- private_class_method def self.populate_entity(root_node)
- entity_data = entity_data(root_node)
- entity_data[:target] = fetch_target(entity_data[:target_type], entity_data[:target_guid])
- new(entity_data)
+ def self.from_hash(hash)
+ hash[:target] = fetch_target(hash[:target_type], hash[:target_guid])
+ new(hash)
end
private_class_method def self.fetch_target(target_type, target_guid)
diff --git a/lib/diaspora_federation/entities/signed_retraction.rb b/lib/diaspora_federation/entities/signed_retraction.rb
index ed88dbd..389543e 100644
--- a/lib/diaspora_federation/entities/signed_retraction.rb
+++ b/lib/diaspora_federation/entities/signed_retraction.rb
@@ -54,16 +54,14 @@ module DiasporaFederation
"SignedRetraction:#{target_type}:#{target_guid}"
end
- private
-
- # @param [Nokogiri::XML::Element] root_node xml nodes
# @return [Retraction] instance
- private_class_method def self.populate_entity(root_node)
- entity_data = entity_data(root_node)
- entity_data[:target] = Retraction.send(:fetch_target, entity_data[:target_type], entity_data[:target_guid])
- new(entity_data).to_retraction
+ def self.from_hash(hash)
+ hash[:target] = Retraction.send(:fetch_target, hash[:target_type], hash[:target_guid])
+ new(hash).to_retraction
end
+ private
+
# It updates also the signatures with the keys of the author and the parent
# if the signatures are not there yet and if the keys are available.
#
diff --git a/lib/diaspora_federation/entity.rb b/lib/diaspora_federation/entity.rb
index d19a242..6d738d2 100644
--- a/lib/diaspora_federation/entity.rb
+++ b/lib/diaspora_federation/entity.rb
@@ -109,10 +109,21 @@ module DiasporaFederation
# @param [Nokogiri::XML::Element] root_node xml nodes
# @return [Entity] instance
def self.from_xml(root_node)
- raise ArgumentError, "only Nokogiri::XML::Element allowed" unless root_node.instance_of?(Nokogiri::XML::Element)
- raise InvalidRootNode, "'#{root_node.name}' can't be parsed by #{name}" unless root_node.name == entity_name
+ from_hash(*xml_parser_class.new(self).parse(root_node))
+ end
- populate_entity(root_node)
+ private_class_method def self.xml_parser_class
+ DiasporaFederation::Parsers::XmlParser
+ end
+
+ # Creates an instance of self by parsing a hash in the format of JSON serialized object (which usually means
+ # data from a parsed JSON input).
+ def self.from_json(json_hash)
+ from_hash(*json_parser_class.new(self).parse(json_hash))
+ end
+
+ private_class_method def self.json_parser_class
+ DiasporaFederation::Parsers::JsonParser
end
# Makes an underscored, lowercase form of the class name
@@ -149,6 +160,33 @@ module DiasporaFederation
"#{self.class.name.rpartition('::').last}#{":#{guid}" if respond_to?(:guid)}"
end
+ # Renders entity to a hash representation of the entity JSON format
+ # @return [Hash] Returns a hash that is equal by structure to the entity in JSON format
+ def to_json
+ {
+ entity_type: self.class.entity_name,
+ entity_data: json_data
+ }
+ end
+
+ # Creates an instance of self, filling it with data from a provided hash of properties.
+ #
+ # The hash format is described as following:
+ # 1) Properties of the hash are representation of the entity's class properties
+ # 2) Keys of the hash must be of Symbol type
+ # 3) Possible values of the hash properties depend on the types of the entity's class properties
+ # 4) Basic properties, such as booleans, strings, integers and timestamps are represented by values of respective
+ # formats
+ # 5) Nested hashes and arrays of hashes are allowed to represent nested entities. Nested hashes follow the same
+ # format as the parent hash.
+ # 6) Besides, the nested entities can be passed in the hash as already instantiated objects of the respective type.
+ #
+ # @param [Hash] properties_hash A hash of the expected format
+ # @return [Entity] an instance
+ def self.from_hash(properties_hash)
+ new(properties_hash)
+ end
+
private
def validate_missing_props(entity_data)
@@ -225,7 +263,7 @@ module DiasporaFederation
def normalize_property(name, value)
case self.class.class_props[name]
- when :string, :integer, :boolean
+ when :string
value.to_s
when :timestamp
value.nil? ? "" : value.utc.iso8601
@@ -245,8 +283,8 @@ module DiasporaFederation
end
def add_property_to_xml(doc, root_element, name, value)
- if value.is_a? String
- root_element << simple_node(doc, name, value)
+ if [String, TrueClass, FalseClass, Integer].any? {|c| value.is_a? c }
+ root_element << simple_node(doc, name, value.to_s)
else
# call #to_xml for each item and append to root
[*value].compact.each do |item|
@@ -264,96 +302,29 @@ module DiasporaFederation
end
end
- # @param [Nokogiri::XML::Element] root_node xml nodes
- # @return [Entity] instance
- private_class_method def self.populate_entity(root_node)
- new(entity_data(root_node))
- end
+ # Generates a hash with entity properties which is put to the "entity_data"
+ # field of a JSON serialized object.
+ # @return [Hash] object properties in JSON format
+ def json_data
+ enriched_properties.map {|key, value|
+ type = self.class.class_props[key]
- # @param [Nokogiri::XML::Element] root_node xml nodes
- # @return [Hash] entity data
- private_class_method def self.entity_data(root_node)
- class_props.map {|name, type|
- value = parse_element_from_node(name, type, root_node)
- [name, value] unless value.nil?
- }.compact.to_h
- end
-
- # @param [String] name property name to parse
- # @param [Class, Symbol] type target type to parse
- # @param [Nokogiri::XML::Element] root_node XML node to parse
- # @return [Object] parsed data
- private_class_method def self.parse_element_from_node(name, type, root_node)
- if type.instance_of?(Symbol)
- parse_string_from_node(name, type, root_node)
- elsif type.instance_of?(Array)
- parse_array_from_node(type.first, root_node)
- elsif type.ancestors.include?(Entity)
- parse_entity_from_node(type, root_node)
- end
- end
-
- # Create simple entry in data hash
- #
- # @param [String] name xml tag to parse
- # @param [Class, Symbol] type target type to parse
- # @param [Nokogiri::XML::Element] root_node XML root_node to parse
- # @return [String] data
- private_class_method def self.parse_string_from_node(name, type, root_node)
- node = root_node.xpath(name.to_s)
- node = root_node.xpath(xml_names[name].to_s) if node.empty?
- parse_string(type, node.first.text) if node.any?
- end
-
- # @param [Symbol] type target type to parse
- # @param [String] text data as string
- # @return [String, Boolean, Integer, Time] data
- private_class_method def self.parse_string(type, text)
- case type
- when :timestamp
- begin
- Time.parse(text).utc
- rescue
- nil
+ if !value.nil? && type.instance_of?(Class) && value.respond_to?(:to_json)
+ entity_data = value.to_json
+ [key, entity_data] unless entity_data.nil?
+ elsif type.instance_of?(Array)
+ entity_data = value.nil? ? nil : value.map(&:to_json)
+ [key, entity_data] unless entity_data.nil?
+ else
+ [key, value]
end
- when :integer
- text.to_i if text =~ /\A\d+\z/
- when :boolean
- return true if text =~ /\A(true|t|yes|y|1)\z/i
- false if text =~ /\A(false|f|no|n|0)\z/i
- else
- text
- end
- end
-
- # Create an entry in the data hash for the nested entity
- #
- # @param [Class] type target type to parse
- # @param [Nokogiri::XML::Element] root_node XML node to parse
- # @return [Entity] parsed child entity
- private_class_method def self.parse_entity_from_node(type, root_node)
- node = root_node.xpath(type.entity_name)
- type.from_xml(node.first) if node.any? && node.first.children.any?
- end
-
- # Collect all nested children of that type and create an array in the data hash
- #
- # @param [Class] type target type to parse
- # @param [Nokogiri::XML::Element] root_node XML node to parse
- # @return [Array] array with parsed child entities
- private_class_method def self.parse_array_from_node(type, root_node)
- node = root_node.xpath(type.entity_name)
- node.select {|child| child.children.any? }.map {|child| type.from_xml(child) } unless node.empty?
+ }.compact.to_h
end
# Raised, if entity is not valid
class ValidationError < RuntimeError
end
- # Raised, if the root node doesn't match the class name
- class InvalidRootNode < RuntimeError
- end
-
# Raised, if the entity name in the XML is invalid
class InvalidEntityName < RuntimeError
end
diff --git a/lib/diaspora_federation/parsers.rb b/lib/diaspora_federation/parsers.rb
new file mode 100644
index 0000000..a08ba5c
--- /dev/null
+++ b/lib/diaspora_federation/parsers.rb
@@ -0,0 +1,13 @@
+module DiasporaFederation
+ # This namespace contains parsers which are used to deserialize federation entities
+ # objects from supported formats (XML, JSON) to objects of DiasporaFederation::Entity
+ # classes
+ module Parsers
+ end
+end
+
+require "diaspora_federation/parsers/base_parser"
+require "diaspora_federation/parsers/json_parser"
+require "diaspora_federation/parsers/xml_parser"
+require "diaspora_federation/parsers/relayable_json_parser"
+require "diaspora_federation/parsers/relayable_xml_parser"
diff --git a/lib/diaspora_federation/parsers/base_parser.rb b/lib/diaspora_federation/parsers/base_parser.rb
new file mode 100644
index 0000000..b277654
--- /dev/null
+++ b/lib/diaspora_federation/parsers/base_parser.rb
@@ -0,0 +1,61 @@
+module DiasporaFederation
+ module Parsers
+ # +BaseParser+ is an abstract class which is used for defining parsers for different
+ # deserialization methods.
+ class BaseParser
+ # @param [Class] entity_type type of DiasporaFederation::Entity that we want to parse with that parser instance
+ def initialize(entity_type)
+ @entity_type = entity_type
+ end
+
+ # This method is used to parse input with a serialized object data. It returns
+ # a comprehensive data which must be enough to construct a DiasporaFederation::Entity instance.
+ #
+ # Since parser method output is normally passed to a .from_hash method of an entity
+ # as arguments using * operator, the parse method must return an array of a size matching the number
+ # of arguments of .from_hash method of the entity type we link with
+ # @abstract
+ def parse(*)
+ raise NotImplementedError.new("you must override this method when creating your own parser")
+ end
+
+ private
+
+ # @param [Symbol] type target type to parse
+ # @param [String] text data as string
+ # @return [String, Boolean, Integer, Time] data
+ def parse_string(type, text)
+ case type
+ when :timestamp
+ begin
+ Time.parse(text).utc
+ rescue
+ nil
+ end
+ when :integer
+ text.to_i if text =~ /\A\d+\z/
+ when :boolean
+ return true if text =~ /\A(true|t|yes|y|1)\z/i
+ false if text =~ /\A(false|f|no|n|0)\z/i
+ else
+ text
+ end
+ end
+
+ def assert_parsability_of(entity_class)
+ return if entity_class == entity_type.entity_name
+ raise InvalidRootNode, "'#{entity_class}' can't be parsed by #{entity_type.name}"
+ end
+
+ attr_reader :entity_type
+
+ def class_properties
+ entity_type.class_props
+ end
+
+ # Raised, if the root node doesn't match the class name
+ class InvalidRootNode < RuntimeError
+ end
+ end
+ end
+end
diff --git a/lib/diaspora_federation/parsers/json_parser.rb b/lib/diaspora_federation/parsers/json_parser.rb
new file mode 100644
index 0000000..0c408cd
--- /dev/null
+++ b/lib/diaspora_federation/parsers/json_parser.rb
@@ -0,0 +1,60 @@
+module DiasporaFederation
+ module Parsers
+ # This is a parser of JSON serialized object. JSON object format is defined by
+ # JSON schema which is available at https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json.
+ # TODO: We must publish the schema at a real URL
+ class JsonParser < BaseParser
+ # @see BaseParser#parse
+ # @param [Hash] json_hash A hash acquired by running JSON.parse with JSON serialized entity
+ # @return [Array[1]] comprehensive data for an entity instantiation
+ def parse(json_hash)
+ from_json_sanity_validation(json_hash)
+ parse_entity_data(json_hash["entity_data"])
+ end
+
+ private
+
+ def parse_entity_data(entity_data)
+ hash = entity_data.map {|key, value|
+ property = entity_type.find_property_for_xml_name(key)
+ if property
+ type = entity_type.class_props[property]
+ [property, parse_element_from_value(type, entity_data[key])]
+ else
+ [key, value]
+ end
+ }.to_h
+
+ [hash]
+ end
+
+ def parse_element_from_value(type, value)
+ return if value.nil?
+ if %i(integer boolean timestamp).include?(type) && !value.is_a?(String)
+ value
+ elsif type.instance_of?(Symbol)
+ parse_string(type, value)
+ elsif type.instance_of?(Array)
+ raise DeserializationError, "Expected array for #{type}" unless value.respond_to?(:map)
+ value.map {|element|
+ type.first.from_json(element)
+ }
+ elsif type.ancestors.include?(Entity)
+ type.from_json(value)
+ end
+ end
+
+ def from_json_sanity_validation(json_hash)
+ missing = %w(entity_type entity_data).map {|prop|
+ prop if json_hash[prop].nil?
+ }.compact.join(", ")
+ raise DeserializationError, "Required properties are missing in JSON object: #{missing}" unless missing.empty?
+ assert_parsability_of(json_hash["entity_type"])
+ end
+
+ # Raised when the format of the input JSON data doesn't match the parser's expectations
+ class DeserializationError < RuntimeError
+ end
+ end
+ end
+end
diff --git a/lib/diaspora_federation/parsers/relayable_json_parser.rb b/lib/diaspora_federation/parsers/relayable_json_parser.rb
new file mode 100644
index 0000000..192a572
--- /dev/null
+++ b/lib/diaspora_federation/parsers/relayable_json_parser.rb
@@ -0,0 +1,25 @@
+module DiasporaFederation
+ module Parsers
+ # This is a parser of JSON serialized object, that is normally used for parsing data of relayables.
+ # Assumed format differs from the usual entity by additional "property_order" property which is used to
+ # compute signatures deterministically.
+ # Input JSON for this parser is expected to match "/definitions/relayable" subschema of the JSON schema at
+ # https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json.
+ class RelayableJsonParser < JsonParser
+ # @see JsonParser#parse
+ # @see BaseParser#parse
+ # @return [Array[2]] comprehensive data for an entity instantiation
+ def parse(json_hash)
+ super.push(json_hash["property_order"])
+ end
+
+ private
+
+ def from_json_sanity_validation(json_hash)
+ super
+ return unless json_hash["property_order"].nil?
+ raise DeserializationError, "Required property is missing in JSON object: property_order"
+ end
+ end
+ end
+end
diff --git a/lib/diaspora_federation/parsers/relayable_xml_parser.rb b/lib/diaspora_federation/parsers/relayable_xml_parser.rb
new file mode 100644
index 0000000..272a4ca
--- /dev/null
+++ b/lib/diaspora_federation/parsers/relayable_xml_parser.rb
@@ -0,0 +1,22 @@
+module DiasporaFederation
+ module Parsers
+ # This is a parser of XML serialized object that is normally used for parsing data of relayables.
+ # Explanations about the XML data format can be found
+ # {https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html here}.
+ # Specific features of relayables are described
+ # {https://diaspora.github.io/diaspora_federation/federation/relayable.html here}.
+ #
+ # @see https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html XML Serialization
+ # documentation
+ # @see https://diaspora.github.io/diaspora_federation/federation/relayable.html Relayable documentation
+ class RelayableXmlParser < XmlParser
+ # @see XmlParser#parse
+ # @see BaseParser#parse
+ # @return [Array[2]] comprehensive data for an entity instantiation
+ def parse(*args)
+ hash = super[0]
+ [hash, hash.keys]
+ end
+ end
+ end
+end
diff --git a/lib/diaspora_federation/parsers/xml_parser.rb b/lib/diaspora_federation/parsers/xml_parser.rb
new file mode 100644
index 0000000..2e04418
--- /dev/null
+++ b/lib/diaspora_federation/parsers/xml_parser.rb
@@ -0,0 +1,84 @@
+module DiasporaFederation
+ module Parsers
+ # This is a parser of XML serialized object.
+ # Explanations about the XML data format can be found
+ # {https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html here}.
+ # @see https://diaspora.github.io/diaspora_federation/federation/xml_serialization.html XML Serialization
+ # documentation
+ class XmlParser < BaseParser
+ # @see BaseParser#parse
+ # @param [Nokogiri::XML::Element] root_node root XML node of the XML representation of the entity
+ # @return [Array[1]] comprehensive data for an entity instantiation
+ def parse(root_node)
+ from_xml_sanity_validation(root_node)
+
+ hash = root_node.element_children.map {|child|
+ xml_name = child.name
+ property = entity_type.find_property_for_xml_name(xml_name)
+ if property
+ type = class_properties[property]
+ value = parse_element_from_node(xml_name, type, root_node)
+ [property, value]
+ else
+ [xml_name, child.text]
+ end
+ }.to_h
+
+ [hash]
+ end
+
+ private
+
+ # @param [String] name property name to parse
+ # @param [Class, Symbol] type target type to parse
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
+ # @return [Object] parsed data
+ def parse_element_from_node(name, type, root_node)
+ if type.instance_of?(Symbol)
+ parse_string_from_node(name, type, root_node)
+ elsif type.instance_of?(Array)
+ parse_array_from_node(type.first, root_node)
+ elsif type.ancestors.include?(Entity)
+ parse_entity_from_node(type, root_node)
+ end
+ end
+
+ # Create simple entry in data hash
+ #
+ # @param [String] name xml tag to parse
+ # @param [Class, Symbol] type target type to parse
+ # @param [Nokogiri::XML::Element] root_node XML root_node to parse
+ # @return [String] data
+ def parse_string_from_node(name, type, root_node)
+ node = root_node.xpath(name.to_s)
+ node = root_node.xpath(xml_names[name].to_s) if node.empty?
+ parse_string(type, node.first.text) if node.any?
+ end
+
+ # Create an entry in the data hash for the nested entity
+ #
+ # @param [Class] type target type to parse
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
+ # @return [Entity] parsed child entity
+ def parse_entity_from_node(type, root_node)
+ node = root_node.xpath(type.entity_name)
+ type.from_xml(node.first) if node.any? && node.first.children.any?
+ end
+
+ # Collect all nested children of that type and create an array in the data hash
+ #
+ # @param [Class] type target type to parse
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
+ # @return [Array] array with parsed child entities
+ def parse_array_from_node(type, root_node)
+ node = root_node.xpath(type.entity_name)
+ node.select {|child| child.children.any? }.map {|child| type.from_xml(child) } unless node.empty?
+ end
+
+ def from_xml_sanity_validation(root_node)
+ raise ArgumentError, "only Nokogiri::XML::Element allowed" unless root_node.instance_of?(Nokogiri::XML::Element)
+ assert_parsability_of(root_node.name)
+ end
+ end
+ end
+end
diff --git a/lib/diaspora_federation/schemas/federation_entities.json b/lib/diaspora_federation/schemas/federation_entities.json
new file mode 100644
index 0000000..3df4433
--- /dev/null
+++ b/lib/diaspora_federation/schemas/federation_entities.json
@@ -0,0 +1,373 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "id": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities-0.2.0-dev.json",
+ "oneOf": [
+ {"$ref": "#/definitions/comment"},
+ {"$ref": "#/definitions/like"},
+ {"$ref": "#/definitions/participation"},
+ {"$ref": "#/definitions/poll_participation"},
+ {"$ref": "#/definitions/status_message"},
+ {"$ref": "#/definitions/reshare"},
+ {"$ref": "#/definitions/profile"},
+ {"$ref": "#/definitions/location"},
+ {"$ref": "#/definitions/photo"},
+ {"$ref": "#/definitions/poll"},
+ {"$ref": "#/definitions/poll_answer"}
+ ],
+
+ "definitions": {
+ "signature": {
+ "type": "string",
+ "minLength": 30
+ },
+
+ "guid": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 255
+ },
+
+ "relayable": {
+ "type": "object",
+ "description": "please don't use this object unless you're defining a new child relayable schema",
+ "properties": {
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "author": { "type": "string" },
+ "guid": { "$ref": "#/definitions/guid" },
+ "parent_guid": { "$ref": "#/definitions/guid" },
+ "author_signature": { "$ref": "#/definitions/signature" },
+ "parent_author_signature": { "$ref": "#/definitions/signature" }
+ },
+ "required": [
+ "author", "guid", "parent_guid"
+ ]
+ },
+ "property_order": {
+ "type": "array",
+ "items": { "type": "string" }
+ }
+ },
+ "required": [
+ "entity_data", "entity_type", "property_order"
+ ]
+ },
+
+ "comment": {
+ "allOf": [
+ {"$ref": "#/definitions/relayable"},
+ {
+ "type": "object",
+ "properties": {
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "text": { "type": "string" },
+ "created_at": { "type": "string" }
+ },
+ "required": ["text"]
+ },
+ "entity_type": {
+ "type": "string",
+ "pattern": "^comment$"
+ }
+ }
+ }
+ ]
+ },
+
+ "like": {
+ "allOf": [
+ {"$ref": "#/definitions/relayable"},
+ {
+ "type": "object",
+ "properties": {
+ "entity_type": {
+ "type": "string",
+ "pattern": "^like$"
+ },
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "positive": { "type": "boolean" },
+ "parent_type": { "enum": ["Post", "Comment"] }
+ },
+ "required": ["positive"]
+ }
+ }
+ }
+ ]
+ },
+
+ "participation": {
+ "allOf": [
+ {"$ref": "#/definitions/relayable"},
+ {
+ "type": "object",
+ "properties": {
+ "entity_type": {
+ "type": "string",
+ "pattern": "^participation$"
+ },
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "parent_type": {"enum": ["Post"]}
+ }
+ }
+ }
+ }
+ ]
+ },
+
+ "poll_participation": {
+ "allOf": [
+ {"$ref": "#/definitions/relayable"},
+ {
+ "type": "object",
+ "properties": {
+ "entity_type": {
+ "type": "string",
+ "pattern": "^poll_participation$"
+ },
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "poll_answer_guid": { "$ref": "#/definitions/guid" }
+ },
+ "required": ["poll_answer_guid"]
+ }
+ }
+ }
+ ]
+ },
+
+ "post": {
+ "type": "object",
+ "description": "please don't use this object unless you're defining a new child post schema",
+ "properties": {
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "guid": { "$ref": "#/definitions/guid" },
+ "public": { "type": "boolean" },
+ "created_at": { "type": "string" },
+ "provider_display_name" : { "type": "string" }
+ },
+ "required": [
+ "guid", "public", "created_at"
+ ]
+ },
+ "required": [
+ "entity_type", "entity_data"
+ ]
+ }
+ },
+
+ "status_message": {
+ "allOf": [
+ {"$ref": "#/definitions/post"},
+ {
+ "type": "object",
+ "properties": {
+ "entity_type": {
+ "type": "string",
+ "pattern": "^status_message$"
+ },
+
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "text": { "type": "string" },
+
+ "location": {
+ "oneOf": [
+ { "$ref": "#/definitions/location" },
+ { "type": "null" }
+ ]
+ },
+
+ "poll": {
+ "oneOf": [
+ { "$ref": "#/definitions/poll" },
+ { "type": "null" }
+ ]
+ },
+
+ "photos": {
+ "type": ["array", "null"],
+ "items": { "$ref": "#/definitions/photo" }
+ }
+ },
+
+ "required": ["text"]
+ }
+ }
+ }
+ ]
+ },
+
+ "reshare": {
+ "allOf": [
+ {"$ref": "#/definitions/post"},
+ {
+ "type": "object",
+ "properties": {
+ "entity_type": {
+ "type": "string",
+ "pattern": "^reshare$"
+ },
+
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "root_author": {"type": "string"},
+ "root_guid": {"$ref": "#/definitions/guid"}
+ },
+
+ "required": ["root_author", "root_guid"]
+ }
+ }
+ }
+ ]
+ },
+
+ "profile": {
+ "type": "object",
+ "properties": {
+ "entity_type": {
+ "type": "string",
+ "pattern": "^profile$"
+ },
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "first_name": { "type": ["string", "null"] },
+ "last_name": { "type": ["string", "null"] },
+ "gender": { "type": ["string", "null"] },
+ "bio": { "type": ["string", "null"] },
+ "birthday": { "type": ["string", "null"] },
+ "location": { "type": ["string", "null"] },
+ "image_url": { "type": ["string", "null"] },
+ "author": { "type": "string" }
+ },
+ "required": [
+ "author"
+ ]
+ }
+ },
+ "required" :["entity_data", "entity_type"]
+ },
+
+ "photo": {
+ "type": "object",
+ "properties": {
+ "entity_type": {
+ "type": "string",
+ "pattern": "^photo$"
+ },
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "author": { "type": "string" },
+ "guid": {"$ref": "#/definitions/guid"},
+ "public": {"type": "boolean"},
+ "created_at": {"type": "string"},
+ "remote_photo_path": {"type": "string"},
+ "remote_photo_name": {"type": "string"},
+ "text": {"type": ["null", "string"]},
+ "status_message_guid": {"$ref": "#/definitions/guid"},
+ "width": {"type": "number"},
+ "height": {"type": "number"}
+ },
+ "required": [
+ "author", "guid", "public", "created_at", "remote_photo_path", "remote_photo_name", "width", "height"
+ ]
+ }
+ }
+ },
+
+ "poll_answer": {
+ "type": "object",
+ "properties": {
+ "entity_type": {
+ "type": "string",
+ "pattern": "^poll_answer$"
+ },
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "guid": { "$ref": "#/definitions/guid" },
+ "answer": { "type": "string" }
+ },
+ "required": [
+ "answer",
+ "guid"
+ ]
+ }
+ }
+ },
+
+ "poll": {
+ "type": "object",
+ "properties": {
+ "entity_type": {
+ "type": "string",
+ "pattern": "^poll$"
+ },
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "guid": {
+ "$ref": "#/definitions/guid"
+ },
+ "question": {
+ "type": "string"
+ },
+ "poll_answers": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/poll_answer"
+ }
+ }
+ },
+ "required": [
+ "guid",
+ "question",
+ "poll_answers"
+ ]
+ }
+ }
+ },
+
+ "location": {
+ "type": "object",
+ "properties": {
+ "entity_type": {
+ "type": "string",
+ "pattern": "^location$"
+ },
+ "entity_data": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "lat": {
+ "type": "string"
+ },
+ "lng": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "address",
+ "lat",
+ "lng"
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/spec/entities.rb b/spec/entities.rb
index b42c531..e452fa9 100644
--- a/spec/entities.rb
+++ b/spec/entities.rb
@@ -46,6 +46,30 @@ module DiasporaFederation
class TestEntityWithTimestamp < DiasporaFederation::Entity
property :test, :timestamp
end
+
+ class TestComplexEntity < DiasporaFederation::Entity
+ property :test1, :string
+ property :test2, :boolean
+ property :test3, :string
+ property :test4, :integer
+ property :test5, :timestamp
+ entity :test6, TestEntity
+ entity :multi, [OtherEntity]
+ end
+
+ class SomeRelayable < DiasporaFederation::Entity
+ LEGACY_SIGNATURE_ORDER = %i(guid author property parent_guid).freeze
+
+ PARENT_TYPE = "Parent".freeze
+
+ include Entities::Relayable
+
+ property :property, :string
+
+ def parent_type
+ PARENT_TYPE
+ end
+ end
end
module Validators
diff --git a/spec/integration/comment_integration_spec.rb b/spec/integration/comment_integration_spec.rb
index 3d63665..2d26f2e 100644
--- a/spec/integration/comment_integration_spec.rb
+++ b/spec/integration/comment_integration_spec.rb
@@ -248,7 +248,7 @@ XML
expect(entity.author).to eq(author)
expect(entity.text).to eq(text)
- expect(entity.additional_xml_elements["new_data"]).to eq(new_data)
+ expect(entity.additional_data["new_data"]).to eq(new_data)
end
it "parses new xml with additional data" do
@@ -257,7 +257,7 @@ XML
expect(entity.author).to eq(author)
expect(entity.text).to eq(text)
- expect(entity.additional_xml_elements["new_data"]).to eq(new_data)
+ expect(entity.additional_data["new_data"]).to eq(new_data)
end
end
end
diff --git a/spec/lib/diaspora_federation/entities/comment_spec.rb b/spec/lib/diaspora_federation/entities/comment_spec.rb
index f454535..c59cfc9 100644
--- a/spec/lib/diaspora_federation/entities/comment_spec.rb
+++ b/spec/lib/diaspora_federation/entities/comment_spec.rb
@@ -24,12 +24,35 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "comment",
+ "entity_data": {
+ "author": "#{data[:author]}",
+ "guid": "#{data[:guid]}",
+ "parent_guid": "#{parent.guid}",
+ "author_signature": "#{data[:author_signature]}",
+ "parent_author_signature": "#{data[:parent_author_signature]}",
+ "text": "#{data[:text]}",
+ "created_at": "#{data[:created_at].iso8601}"
+ },
+ "property_order": [
+ "guid",
+ "parent_guid",
+ "text",
+ "author"
+ ]
+}
+JSON
+
let(:string) { "Comment:#{data[:guid]}:#{parent.guid}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity", [:created_at]
+ it_behaves_like "a JSON Entity"
+
it_behaves_like "a relayable Entity"
describe "#created_at" do
diff --git a/spec/lib/diaspora_federation/entities/like_spec.rb b/spec/lib/diaspora_federation/entities/like_spec.rb
index 8959091..800f0bc 100644
--- a/spec/lib/diaspora_federation/entities/like_spec.rb
+++ b/spec/lib/diaspora_federation/entities/like_spec.rb
@@ -24,12 +24,36 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "like",
+ "entity_data": {
+ "author": "#{data[:author]}",
+ "guid": "#{data[:guid]}",
+ "parent_guid": "#{parent.guid}",
+ "author_signature": "#{data[:author_signature]}",
+ "parent_author_signature": "#{data[:parent_author_signature]}",
+ "positive": #{data[:positive]},
+ "parent_type": "#{parent.entity_type}"
+ },
+ "property_order": [
+ "positive",
+ "guid",
+ "parent_type",
+ "parent_guid",
+ "author"
+ ]
+}
+JSON
+
let(:string) { "Like:#{data[:guid]}:Post:#{parent.guid}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
+ it_behaves_like "a JSON Entity"
+
it_behaves_like "a relayable Entity"
context "invalid XML" do
diff --git a/spec/lib/diaspora_federation/entities/location_spec.rb b/spec/lib/diaspora_federation/entities/location_spec.rb
index a3477cd..053d3e6 100644
--- a/spec/lib/diaspora_federation/entities/location_spec.rb
+++ b/spec/lib/diaspora_federation/entities/location_spec.rb
@@ -10,10 +10,23 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "location",
+ "entity_data": {
+ "address": "#{data[:address]}",
+ "lat": "#{data[:lat]}",
+ "lng": "#{data[:lng]}"
+ }
+}
+JSON
+
let(:string) { "Location" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
+
+ it_behaves_like "a JSON Entity"
end
end
diff --git a/spec/lib/diaspora_federation/entities/message_spec.rb b/spec/lib/diaspora_federation/entities/message_spec.rb
index 50ed5c8..a2df46d 100644
--- a/spec/lib/diaspora_federation/entities/message_spec.rb
+++ b/spec/lib/diaspora_federation/entities/message_spec.rb
@@ -76,7 +76,7 @@ XML
end
end
- describe ".populate_entity" do
+ describe ".from_xml" do
it "adds a nil parent" do
xml = Entities::Message.new(data).to_xml
parsed = Entities::Message.from_xml(xml)
diff --git a/spec/lib/diaspora_federation/entities/participation_spec.rb b/spec/lib/diaspora_federation/entities/participation_spec.rb
index 504183c..2ceb84e 100644
--- a/spec/lib/diaspora_federation/entities/participation_spec.rb
+++ b/spec/lib/diaspora_federation/entities/participation_spec.rb
@@ -23,12 +23,34 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "participation",
+ "entity_data": {
+ "author": "#{data[:author]}",
+ "guid": "#{data[:guid]}",
+ "parent_guid": "#{parent.guid}",
+ "author_signature": "#{data[:author_signature]}",
+ "parent_author_signature": "#{data[:parent_author_signature]}",
+ "parent_type": "#{parent.entity_type}"
+ },
+ "property_order": [
+ "guid",
+ "parent_type",
+ "parent_guid",
+ "author"
+ ]
+}
+JSON
+
let(:string) { "Participation:#{data[:guid]}:Post:#{parent.guid}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity", [:parent]
+ it_behaves_like "a JSON Entity"
+
it_behaves_like "a relayable Entity"
describe "#sender_valid?" do
diff --git a/spec/lib/diaspora_federation/entities/photo_spec.rb b/spec/lib/diaspora_federation/entities/photo_spec.rb
index a9db12e..0f8d71b 100644
--- a/spec/lib/diaspora_federation/entities/photo_spec.rb
+++ b/spec/lib/diaspora_federation/entities/photo_spec.rb
@@ -17,12 +17,32 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "photo",
+ "entity_data": {
+ "guid": "#{data[:guid]}",
+ "author": "#{data[:author]}",
+ "public": #{data[:public]},
+ "created_at": "#{data[:created_at].utc.iso8601}",
+ "remote_photo_path": "#{data[:remote_photo_path]}",
+ "remote_photo_name": "#{data[:remote_photo_name]}",
+ "text": "#{data[:text]}",
+ "status_message_guid": "#{data[:status_message_guid]}",
+ "height": #{data[:height]},
+ "width": #{data[:width]}
+ }
+}
+JSON
+
let(:string) { "Photo:#{data[:guid]}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
+ it_behaves_like "a JSON Entity"
+
context "default values" do
it "uses default values" do
minimal_xml = <<-XML
diff --git a/spec/lib/diaspora_federation/entities/poll_answer_spec.rb b/spec/lib/diaspora_federation/entities/poll_answer_spec.rb
index a9941f5..fd88740 100644
--- a/spec/lib/diaspora_federation/entities/poll_answer_spec.rb
+++ b/spec/lib/diaspora_federation/entities/poll_answer_spec.rb
@@ -9,10 +9,22 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "poll_answer",
+ "entity_data": {
+ "guid": "#{data[:guid]}",
+ "answer": "#{data[:answer]}"
+ }
+}
+JSON
+
let(:string) { "PollAnswer:#{data[:guid]}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
+
+ it_behaves_like "a JSON Entity"
end
end
diff --git a/spec/lib/diaspora_federation/entities/poll_participation_spec.rb b/spec/lib/diaspora_federation/entities/poll_participation_spec.rb
index 8ee541c..8d64df5 100644
--- a/spec/lib/diaspora_federation/entities/poll_participation_spec.rb
+++ b/spec/lib/diaspora_federation/entities/poll_participation_spec.rb
@@ -22,12 +22,34 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "poll_participation",
+ "entity_data": {
+ "author": "#{data[:author]}",
+ "guid": "#{data[:guid]}",
+ "parent_guid": "#{parent.guid}",
+ "author_signature": "#{data[:author_signature]}",
+ "parent_author_signature": "#{data[:parent_author_signature]}",
+ "poll_answer_guid": "#{data[:poll_answer_guid]}"
+ },
+ "property_order": [
+ "guid",
+ "parent_guid",
+ "author",
+ "poll_answer_guid"
+ ]
+}
+JSON
+
let(:string) { "PollParticipation:#{data[:guid]}:#{parent.guid}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
+ it_behaves_like "a JSON Entity"
+
it_behaves_like "a relayable Entity"
end
end
diff --git a/spec/lib/diaspora_federation/entities/poll_spec.rb b/spec/lib/diaspora_federation/entities/poll_spec.rb
index ac7fb62..7422d39 100644
--- a/spec/lib/diaspora_federation/entities/poll_spec.rb
+++ b/spec/lib/diaspora_federation/entities/poll_spec.rb
@@ -10,10 +10,25 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "poll",
+ "entity_data": {
+ "guid": "#{data[:guid]}",
+ "question": "#{data[:question]}",
+ "poll_answers": [
+#{data[:poll_answers].map {|a| JSON.pretty_generate(a.to_json).indent(6) }.join(",\n")}
+ ]
+ }
+}
+JSON
+
let(:string) { "Poll:#{data[:guid]}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
+
+ it_behaves_like "a JSON Entity"
end
end
diff --git a/spec/lib/diaspora_federation/entities/profile_spec.rb b/spec/lib/diaspora_federation/entities/profile_spec.rb
index 775b636..6aca890 100644
--- a/spec/lib/diaspora_federation/entities/profile_spec.rb
+++ b/spec/lib/diaspora_federation/entities/profile_spec.rb
@@ -20,12 +20,35 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "profile",
+ "entity_data": {
+ "author": "#{data[:author]}",
+ "first_name": "#{data[:first_name]}",
+ "last_name": "",
+ "image_url": "#{data[:image_url]}",
+ "image_url_medium": "#{data[:image_url]}",
+ "image_url_small": "#{data[:image_url]}",
+ "birthday": "#{data[:birthday]}",
+ "gender": "#{data[:gender]}",
+ "bio": "#{data[:bio]}",
+ "location": "#{data[:location]}",
+ "searchable": #{data[:searchable]},
+ "nsfw": #{data[:nsfw]},
+ "tag_string": "#{data[:tag_string]}"
+ }
+}
+JSON
+
let(:string) { "Profile:#{data[:author]}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
+ it_behaves_like "a JSON Entity"
+
context "default values" do
it "uses default values" do
minimal_xml = <<-XML
diff --git a/spec/lib/diaspora_federation/entities/relayable_spec.rb b/spec/lib/diaspora_federation/entities/relayable_spec.rb
index 5512f1b..9161fc7 100644
--- a/spec/lib/diaspora_federation/entities/relayable_spec.rb
+++ b/spec/lib/diaspora_federation/entities/relayable_spec.rb
@@ -11,23 +11,10 @@ module DiasporaFederation
let(:local_parent) { FactoryGirl.build(:related_entity, author: bob.diaspora_id) }
let(:remote_parent) { FactoryGirl.build(:related_entity, author: bob.diaspora_id, local: false) }
let(:hash) { {guid: guid, author: author, parent_guid: parent_guid, parent: local_parent, property: property} }
+ let(:hash_with_fake_signatures) { hash.merge!(author_signature: "aa", parent_author_signature: "bb") }
let(:legacy_signature_data) { "#{guid};#{author};#{property};#{parent_guid}" }
- class SomeRelayable < Entity
- LEGACY_SIGNATURE_ORDER = %i(guid author property parent_guid).freeze
-
- PARENT_TYPE = "Parent".freeze
-
- include Entities::Relayable
-
- property :property, :string
-
- def parent_type
- PARENT_TYPE
- end
- end
-
def sign_with_key(privkey, signature_data)
Base64.strict_encode64(privkey.sign(OpenSSL::Digest::SHA256.new, signature_data))
end
@@ -36,6 +23,15 @@ module DiasporaFederation
pubkey.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), signed_string)
end
+ describe "#initialize" do
+ it "filters signatures from order" do
+ xml_order = [:author, :guid, :parent_guid, :property, "new_property", :author_signature]
+
+ expect(Entities::SomeRelayable.new(hash, xml_order).xml_order)
+ .to eq([:author, :guid, :parent_guid, :property, "new_property"])
+ end
+ end
+
describe "#verify_signatures" do
it "doesn't raise anything if correct signatures with legacy-string were passed" do
hash[:author_signature] = sign_with_key(author_pkey, legacy_signature_data)
@@ -45,14 +41,14 @@ module DiasporaFederation
expect_callback(:fetch_public_key, author).and_return(author_pkey.public_key)
expect_callback(:fetch_public_key, remote_parent.author).and_return(parent_pkey.public_key)
- expect { SomeRelayable.new(hash).verify_signatures }.not_to raise_error
+ expect { Entities::SomeRelayable.new(hash).verify_signatures }.not_to raise_error
end
it "raises when no public key for author was fetched" do
expect_callback(:fetch_public_key, anything).and_return(nil)
expect {
- SomeRelayable.new(hash).verify_signatures
+ Entities::SomeRelayable.new(hash).verify_signatures
}.to raise_error Entities::Relayable::PublicKeyNotFound
end
@@ -62,7 +58,7 @@ module DiasporaFederation
expect_callback(:fetch_public_key, author).and_return(author_pkey.public_key)
expect {
- SomeRelayable.new(hash).verify_signatures
+ Entities::SomeRelayable.new(hash).verify_signatures
}.to raise_error Entities::Relayable::SignatureVerificationFailed
end
@@ -74,7 +70,7 @@ module DiasporaFederation
expect_callback(:fetch_public_key, remote_parent.author).and_return(nil)
expect {
- SomeRelayable.new(hash).verify_signatures
+ Entities::SomeRelayable.new(hash).verify_signatures
}.to raise_error Entities::Relayable::PublicKeyNotFound
end
@@ -87,7 +83,7 @@ module DiasporaFederation
expect_callback(:fetch_public_key, remote_parent.author).and_return(parent_pkey.public_key)
expect {
- SomeRelayable.new(hash).verify_signatures
+ Entities::SomeRelayable.new(hash).verify_signatures
}.to raise_error Entities::Relayable::SignatureVerificationFailed
end
@@ -98,7 +94,7 @@ module DiasporaFederation
expect_callback(:fetch_public_key, author).and_return(author_pkey.public_key)
- expect { SomeRelayable.new(hash).verify_signatures }.not_to raise_error
+ expect { Entities::SomeRelayable.new(hash).verify_signatures }.not_to raise_error
end
context "new signatures" do
@@ -113,7 +109,7 @@ module DiasporaFederation
expect_callback(:fetch_public_key, author).and_return(author_pkey.public_key)
expect_callback(:fetch_public_key, remote_parent.author).and_return(parent_pkey.public_key)
- expect { SomeRelayable.new(hash, xml_order).verify_signatures }.not_to raise_error
+ expect { Entities::SomeRelayable.new(hash, xml_order).verify_signatures }.not_to raise_error
end
it "doesn't raise anything if correct signatures with new property were passed" do
@@ -128,7 +124,7 @@ module DiasporaFederation
expect_callback(:fetch_public_key, remote_parent.author).and_return(parent_pkey.public_key)
expect {
- SomeRelayable.new(hash, xml_order, "new_property" => new_property).verify_signatures
+ Entities::SomeRelayable.new(hash, xml_order, "new_property" => new_property).verify_signatures
}.not_to raise_error
end
@@ -139,7 +135,7 @@ module DiasporaFederation
xml_order = [:author, :guid, :parent_guid, :property, "new_property"]
expect {
- SomeRelayable.new(hash, xml_order, "new_property" => new_property).verify_signatures
+ Entities::SomeRelayable.new(hash, xml_order, "new_property" => new_property).verify_signatures
}.to raise_error Entities::Relayable::SignatureVerificationFailed
end
end
@@ -159,42 +155,32 @@ module DiasporaFederation
XML
it "adds new unknown xml elements to the xml again" do
- hash.merge!(author_signature: "aa", parent_author_signature: "bb")
xml_order = [:author, :guid, :parent_guid, :property, "new_property"]
- xml = SomeRelayable.new(hash, xml_order, "new_property" => new_property).to_xml
+ xml = Entities::SomeRelayable.new(hash_with_fake_signatures, xml_order, "new_property" => new_property).to_xml
expect(xml.to_s.strip).to eq(expected_xml.strip)
end
it "converts strings in xml_order to symbol if needed" do
- hash.merge!(author_signature: "aa", parent_author_signature: "bb")
xml_order = %w(author guid parent_guid property new_property)
- xml = SomeRelayable.new(hash, xml_order, "new_property" => new_property).to_xml
+ xml = Entities::SomeRelayable.new(hash_with_fake_signatures, xml_order, "new_property" => new_property).to_xml
expect(xml.to_s.strip).to eq(expected_xml.strip)
end
it "adds missing properties from xml_order to xml" do
- hash.merge!(author_signature: "aa", parent_author_signature: "bb")
xml_order = [:author, :guid, :parent_guid, :property, "new_property"]
- xml = SomeRelayable.new(hash, xml_order).to_xml
+ xml = Entities::SomeRelayable.new(hash_with_fake_signatures, xml_order).to_xml
expect(xml.at_xpath("new_property").text).to be_empty
end
- it "filters signatures from order" do
- xml_order = [:author, :guid, :parent_guid, :property, "new_property", :author_signature]
-
- expect(SomeRelayable.new(hash, xml_order).xml_order)
- .to eq([:author, :guid, :parent_guid, :property, "new_property"])
- end
-
it "computes correct signatures for the entity" do
expect_callback(:fetch_private_key, author).and_return(author_pkey)
expect_callback(:fetch_private_key, local_parent.author).and_return(parent_pkey)
- xml = SomeRelayable.new(hash).to_xml
+ xml = Entities::SomeRelayable.new(hash).to_xml
author_signature = xml.at_xpath("author_signature").text
parent_author_signature = xml.at_xpath("parent_author_signature").text
@@ -210,7 +196,7 @@ XML
xml_order = [:author, :guid, :parent_guid, "new_property", :property]
signature_data_with_new_property = "#{author};#{guid};#{parent_guid};#{new_property};#{property}"
- xml = SomeRelayable.new(hash, xml_order, "new_property" => new_property).to_xml
+ xml = Entities::SomeRelayable.new(hash, xml_order, "new_property" => new_property).to_xml
author_signature = xml.at_xpath("author_signature").text
parent_author_signature = xml.at_xpath("parent_author_signature").text
@@ -220,9 +206,7 @@ XML
end
it "doesn't change signatures if they are already set" do
- hash.merge!(author_signature: "aa", parent_author_signature: "bb")
-
- xml = SomeRelayable.new(hash).to_xml
+ xml = Entities::SomeRelayable.new(hash_with_fake_signatures).to_xml
expect(xml.at_xpath("author_signature").text).to eq("aa")
expect(xml.at_xpath("parent_author_signature").text).to eq("bb")
@@ -232,7 +216,7 @@ XML
expect_callback(:fetch_private_key, author).and_return(nil)
expect {
- SomeRelayable.new(hash).to_xml
+ Entities::SomeRelayable.new(hash).to_xml
}.to raise_error Entities::Relayable::AuthorPrivateKeyNotFound
end
@@ -240,19 +224,16 @@ XML
expect_callback(:fetch_private_key, author).and_return(author_pkey)
expect_callback(:fetch_private_key, local_parent.author).and_return(nil)
- xml = SomeRelayable.new(hash).to_xml
+ xml = Entities::SomeRelayable.new(hash).to_xml
expect(xml.at_xpath("parent_author_signature").text).to eq("")
end
end
describe ".from_xml" do
- before do
- expect_callback(:fetch_related_entity, "Parent", parent_guid).and_return(remote_parent)
- end
-
context "parsing" do
before do
+ expect_callback(:fetch_related_entity, "Parent", parent_guid).and_return(remote_parent)
expect_callback(:fetch_public_key, author).and_return(author_pkey.public_key)
expect_callback(:fetch_public_key, remote_parent.author).and_return(parent_pkey.public_key)
end
@@ -271,109 +252,283 @@ XML
XML
it "doesn't drop unknown properties" do
- entity = SomeRelayable.from_xml(Nokogiri::XML::Document.parse(new_xml).root)
+ entity = Entities::SomeRelayable.from_xml(Nokogiri::XML::Document.parse(new_xml).root)
- expect(entity).to be_an_instance_of SomeRelayable
+ expect(entity).to be_an_instance_of Entities::SomeRelayable
expect(entity.property).to eq(property)
- expect(entity.additional_xml_elements).to eq(
+ expect(entity.additional_data).to eq(
"new_property" => new_property
)
end
it "hand over the order in the xml to the instance without signatures" do
- entity = SomeRelayable.from_xml(Nokogiri::XML::Document.parse(new_xml).root)
+ entity = Entities::SomeRelayable.from_xml(Nokogiri::XML::Document.parse(new_xml).root)
expect(entity.xml_order).to eq([:author, :guid, :parent_guid, "new_property", :property])
end
- it "creates Entity with empty 'additional_xml_elements' if the xml has only known properties" do
+ it "creates Entity with empty 'additional_data' if the xml has only known properties" do
hash[:author_signature] = sign_with_key(author_pkey, legacy_signature_data)
hash[:parent_author_signature] = sign_with_key(parent_pkey, legacy_signature_data)
- xml = SomeRelayable.new(hash).to_xml
+ xml = Entities::SomeRelayable.new(hash).to_xml
- entity = SomeRelayable.from_xml(xml)
+ entity = Entities::SomeRelayable.from_xml(xml)
- expect(entity).to be_an_instance_of SomeRelayable
+ expect(entity).to be_an_instance_of Entities::SomeRelayable
expect(entity.property).to eq(property)
- expect(entity.additional_xml_elements).to be_empty
+ expect(entity.additional_data).to be_empty
end
end
- context "relayable signature verification feature support" do
- it "calls signatures verification on relayable unpack" do
- hash.merge!(author_signature: "aa", parent_author_signature: "bb")
-
- xml = SomeRelayable.new(hash).to_xml
-
- expect_callback(:fetch_public_key, author).and_return(author_pkey.public_key)
-
- expect {
- SomeRelayable.from_xml(xml)
- }.to raise_error DiasporaFederation::Entities::Relayable::SignatureVerificationFailed
- end
- end
- end
-
- context "parse invalid XML" do
- it "raises a ValidationError if the parent_guid is missing" do
- broken_xml = <<-XML
+ context "parse invalid XML" do
+ it "raises a ValidationError if the parent_guid is missing" do
+ broken_xml = <<-XML
XML
- expect {
- SomeRelayable.from_xml(Nokogiri::XML::Document.parse(broken_xml).root)
- }.to raise_error Entity::ValidationError, "invalid DiasporaFederation::SomeRelayable! missing 'parent_guid'."
+ expect {
+ Entities::SomeRelayable.from_xml(Nokogiri::XML::Document.parse(broken_xml).root)
+ }.to raise_error Entity::ValidationError,
+ "invalid DiasporaFederation::Entities::SomeRelayable! missing 'parent_guid'."
+ end
end
end
- context "fetch parent" do
- before do
- expect_callback(:fetch_public_key, author).and_return(author_pkey.public_key)
- expect_callback(:fetch_public_key, remote_parent.author).and_return(parent_pkey.public_key)
+ describe "#to_json" do
+ let(:entity_class) { Entities::SomeRelayable }
+
+ it "contains the property order within the property_order property" do
+ property_order = %i(author guid parent_guid property)
+ json = entity_class.new(hash_with_fake_signatures, property_order).to_json.to_json
+
+ expect(json).to include_json(property_order: property_order.map(&:to_s))
+ end
+
+ it "uses legacy order for filling property_order when no xml_order supplied" do
+ entity = entity_class.new(hash_with_fake_signatures)
+ expect(
+ entity.to_json.to_json
+ ).to include_json(property_order: entity_class::LEGACY_SIGNATURE_ORDER.map(&:to_s))
+ end
+
+ it "adds new unknown elements to the json again" do
+ property_order = [:author, :guid, :parent_guid, :property, "new_property"]
+ json = Entities::SomeRelayable.new(hash_with_fake_signatures, property_order, "new_property" => new_property)
+ .to_json.to_json
+
+ expect(json).to include_json(
+ entity_data: {new_property: new_property},
+ property_order: {4 => "new_property"}
+ )
+ end
+
+ it "adds missing properties from property order to json" do
+ property_order = [:author, :guid, :parent_guid, :property, "new_property"]
+ json = Entities::SomeRelayable.new(hash_with_fake_signatures, property_order).to_json.to_json
+
+ expect(json).to include_json(
+ entity_data: {new_property: nil},
+ property_order: {4 => "new_property"}
+ )
+ end
+
+ it "computes correct signatures for the entity with new unknown elements" do
expect_callback(:fetch_private_key, author).and_return(author_pkey)
- expect_callback(:fetch_private_key, remote_parent.author).and_return(parent_pkey)
+ expect_callback(:fetch_private_key, local_parent.author).and_return(parent_pkey)
+
+ property_order = [:author, :guid, :parent_guid, "new_property", :property]
+ signature_data_with_new_property = "#{author};#{guid};#{parent_guid};#{new_property};#{property}"
+
+ json_hash = Entities::SomeRelayable.new(hash, property_order, "new_property" => new_property).to_json
+ author_signature = json_hash[:entity_data][:author_signature]
+ parent_author_signature = json_hash[:entity_data][:parent_author_signature]
+
+ expect(verify_signature(author_pkey, author_signature, signature_data_with_new_property)).to be_truthy
+ expect(verify_signature(parent_pkey, parent_author_signature, signature_data_with_new_property)).to be_truthy
end
- let(:xml) { SomeRelayable.new(hash).to_xml }
-
- it "fetches the parent from the backend" do
- expect_callback(:fetch_related_entity, "Parent", parent_guid).and_return(remote_parent)
- expect(Federation::Fetcher).not_to receive(:fetch_public)
-
- entity = SomeRelayable.from_xml(xml)
-
- expect(entity.parent).to eq(remote_parent)
+ it "doesn't change signatures if they are already set" do
+ json = Entities::SomeRelayable.new(hash_with_fake_signatures).to_json.to_json
+ expect(json).to include_json(entity_data: {author_signature: "aa"})
+ expect(json).to include_json(entity_data: {parent_author_signature: "bb"})
end
- it "fetches the parent from remote if not found on backend" do
- expect_callback(:fetch_related_entity, "Parent", parent_guid).and_return(nil, remote_parent)
- expect(Federation::Fetcher).to receive(:fetch_public).with(author, "Parent", parent_guid)
+ it "raises when author_signature not set and key isn't supplied" do
+ expect_callback(:fetch_private_key, author).and_return(nil)
- entity = SomeRelayable.from_xml(xml)
+ expect {
+ Entities::SomeRelayable.new(hash).to_json
+ }.to raise_error Entities::Relayable::AuthorPrivateKeyNotFound
+ end
- expect(entity.parent).to eq(remote_parent)
+ it "doesn't set parent_author_signature if key isn't supplied" do
+ expect_callback(:fetch_private_key, author).and_return(author_pkey)
+ expect_callback(:fetch_private_key, local_parent.author).and_return(nil)
+
+ json = Entities::SomeRelayable.new(hash).to_json.to_json
+ expect(json).to include_json(entity_data: {parent_author_signature: ""})
+ end
+ end
+
+ describe ".from_hash" do
+ let(:entity_class) { Entities::SomeRelayable }
+
+ context "parsing" do
+ before do
+ expect_callback(:fetch_related_entity, "Parent", parent_guid).and_return(remote_parent)
+ expect_callback(:fetch_public_key, author).and_return(author_pkey.public_key)
+ expect_callback(:fetch_public_key, remote_parent.author).and_return(parent_pkey.public_key)
+ end
+
+ context "when properties are sorted and there is an unknown property" do
+ let(:new_signature_data) { "#{author};#{guid};#{parent_guid};#{new_property};#{property}" }
+ let(:author_signature) { sign_with_key(author_pkey, new_signature_data) }
+ let(:parent_author_signature) { sign_with_key(parent_pkey, new_signature_data) }
+ let(:entity_data) {
+ {
+ :guid => guid,
+ :author => author,
+ :property => property,
+ :parent_guid => parent_guid,
+ "new_property" => new_property,
+ :author_signature => author_signature,
+ :parent_author_signature => parent_author_signature
+ }
+ }
+ let(:property_order) { %w(author guid parent_guid new_property property) }
+
+ it "parses entity properties from the input data" do
+ entity = Entities::SomeRelayable.from_hash(entity_data, property_order)
+ expect(entity).to be_an_instance_of Entities::SomeRelayable
+ expect(entity.author).to eq(author)
+ expect(entity.guid).to eq(guid)
+ expect(entity.parent_guid).to eq(parent_guid)
+ expect(entity.property).to eq(property)
+ expect(entity.author_signature).to eq(author_signature)
+ expect(entity.parent_author_signature).to eq(parent_author_signature)
+ end
+
+ it "makes unknown properties available via #additional_data" do
+ entity = Entities::SomeRelayable.from_hash(entity_data, property_order)
+ expect(entity.additional_data).to eq("new_property" => new_property)
+ end
+
+ it "hands over the order in the data to the instance without signatures" do
+ entity = Entities::SomeRelayable.from_hash(entity_data, property_order)
+ expect(entity.xml_order).to eq(%w(author guid parent_guid new_property property))
+ end
+
+ it "calls a constructor of the entity of the appropriate type" do
+ expect(Entities::SomeRelayable).to receive(:new).with(
+ {
+ author: author,
+ guid: guid,
+ parent_guid: parent_guid,
+ property: property,
+ author_signature: author_signature,
+ parent_author_signature: parent_author_signature,
+ parent: remote_parent
+ }.merge("new_property" => new_property),
+ %w(author guid parent_guid new_property property),
+ "new_property" => new_property
+ ).and_call_original
+ Entities::SomeRelayable.from_hash(entity_data, property_order)
+ end
+ end
+
+ it "creates Entity with empty 'additional_data' if it has only known properties" do
+ property_order = %w(guid author property parent_guid)
+
+ entity_data = {
+ guid: guid,
+ author: author,
+ property: property,
+ parent_guid: parent_guid,
+ author_signature: sign_with_key(author_pkey, legacy_signature_data),
+ parent_author_signature: sign_with_key(parent_pkey, legacy_signature_data)
+ }
+
+ entity = Entities::SomeRelayable.from_hash(entity_data, property_order)
+
+ expect(entity).to be_an_instance_of Entities::SomeRelayable
+ expect(entity.additional_data).to be_empty
+ end
+ end
+
+ context "relayable signature verification feature support" do
+ it "calls signatures verification on relayable unpack" do
+ property_order = %w(guid author property parent_guid)
+ entity_data = {
+ guid: guid,
+ author: author,
+ property: property,
+ parent_guid: parent_guid,
+ author_signature: "aa",
+ parent_author_signature: "bb"
+ }
+
+ expect_callback(:fetch_related_entity, "Parent", parent_guid).and_return(remote_parent)
+ expect_callback(:fetch_public_key, author).and_return(author_pkey.public_key)
+ expect {
+ Entities::SomeRelayable.from_hash(entity_data, property_order)
+ }.to raise_error DiasporaFederation::Entities::Relayable::SignatureVerificationFailed
+ end
+ end
+
+ context "fetch parent" do
+ before do
+ expect_callback(:fetch_public_key, author).and_return(author_pkey.public_key)
+ expect_callback(:fetch_public_key, remote_parent.author).and_return(parent_pkey.public_key)
+ expect_callback(:fetch_private_key, author).and_return(author_pkey)
+ expect_callback(:fetch_private_key, remote_parent.author).and_return(parent_pkey)
+ end
+
+ let(:entity) { Entities::SomeRelayable.new(hash) }
+ let(:data) {
+ entity.to_h.tap {|hash|
+ hash.delete(:parent)
+ }
+ }
+
+ it "fetches the parent from the backend" do
+ expect_callback(:fetch_related_entity, "Parent", parent_guid).and_return(remote_parent)
+ expect(Federation::Fetcher).not_to receive(:fetch_public)
+
+ new_entity = Entities::SomeRelayable.from_hash(data, entity.send(:signature_order))
+
+ expect(new_entity.parent).to eq(remote_parent)
+ end
+
+ it "fetches the parent from remote if not found on backend" do
+ expect_callback(:fetch_related_entity, "Parent", parent_guid).and_return(nil, remote_parent)
+ expect(Federation::Fetcher).to receive(:fetch_public).with(author, "Parent", parent_guid)
+
+ new_entity = Entities::SomeRelayable.from_hash(data, entity.send(:signature_order))
+
+ expect(new_entity.parent).to eq(remote_parent)
+ end
end
end
describe "#sender_valid?" do
it "allows author" do
- entity = SomeRelayable.new(hash)
+ entity = Entities::SomeRelayable.new(hash)
expect(entity.sender_valid?(author)).to be_truthy
end
it "allows parent author" do
- entity = SomeRelayable.new(hash)
+ entity = Entities::SomeRelayable.new(hash)
expect(entity.sender_valid?(local_parent.author)).to be_truthy
end
it "does not allow any random author" do
- entity = SomeRelayable.new(hash)
+ entity = Entities::SomeRelayable.new(hash)
invalid_author = FactoryGirl.generate(:diaspora_id)
expect(entity.sender_valid?(invalid_author)).to be_falsey
diff --git a/spec/lib/diaspora_federation/entities/reshare_spec.rb b/spec/lib/diaspora_federation/entities/reshare_spec.rb
index d1efba8..f0c9d96 100644
--- a/spec/lib/diaspora_federation/entities/reshare_spec.rb
+++ b/spec/lib/diaspora_federation/entities/reshare_spec.rb
@@ -15,12 +15,29 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "reshare",
+ "entity_data": {
+ "author": "#{data[:author]}",
+ "guid": "#{data[:guid]}",
+ "created_at": "#{data[:created_at].utc.iso8601}",
+ "provider_display_name": "#{data[:provider_display_name]}",
+ "root_author": "#{data[:root_author]}",
+ "root_guid": "#{data[:root_guid]}",
+ "public": #{data[:public]}
+ }
+}
+JSON
+
let(:string) { "Reshare:#{data[:guid]}:#{data[:root_guid]}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
+ it_behaves_like "a JSON Entity"
+
context "default values" do
it "uses default values" do
minimal_xml = <<-XML
diff --git a/spec/lib/diaspora_federation/entities/status_message_spec.rb b/spec/lib/diaspora_federation/entities/status_message_spec.rb
index 1f0ca4a..1df135d 100644
--- a/spec/lib/diaspora_federation/entities/status_message_spec.rb
+++ b/spec/lib/diaspora_federation/entities/status_message_spec.rb
@@ -54,12 +54,70 @@ module DiasporaFederation
XML
+ let(:json) { <<-JSON }
+{
+ "entity_type": "status_message",
+ "entity_data": {
+ "author": "#{data[:author]}",
+ "guid": "#{data[:guid]}",
+ "created_at": "#{data[:created_at].utc.iso8601}",
+ "provider_display_name": "#{data[:provider_display_name]}",
+ "text": "#{data[:text]}",
+ "photos": [
+ {
+ "entity_type": "photo",
+ "entity_data": {
+ "guid": "#{photo1.guid}",
+ "author": "#{photo1.author}",
+ "public": #{photo1.public},
+ "created_at": "#{photo1.created_at.utc.iso8601}",
+ "remote_photo_path": "#{photo1.remote_photo_path}",
+ "remote_photo_name": "#{photo1.remote_photo_name}",
+ "text": "#{photo1.text}",
+ "status_message_guid": "#{photo1.status_message_guid}",
+ "height": #{photo1.height},
+ "width": #{photo1.width}
+ }
+ },
+ {
+ "entity_type": "photo",
+ "entity_data": {
+ "guid": "#{photo2.guid}",
+ "author": "#{photo2.author}",
+ "public": #{photo2.public},
+ "created_at": "#{photo2.created_at.utc.iso8601}",
+ "remote_photo_path": "#{photo2.remote_photo_path}",
+ "remote_photo_name": "#{photo2.remote_photo_name}",
+ "text": "#{photo2.text}",
+ "status_message_guid": "#{photo2.status_message_guid}",
+ "height": #{photo2.height},
+ "width": #{photo2.width}
+ }
+ }
+ ],
+ "location": {
+ "entity_type": "location",
+ "entity_data": {
+ "address": "#{location.address}",
+ "lat": "#{location.lat}",
+ "lng": "#{location.lng}"
+ }
+ },
+ "poll": null,
+ "event": null,
+ "public": #{data[:public]}
+ }
+}
+JSON
+
let(:string) { "StatusMessage:#{data[:guid]}" }
it_behaves_like "an Entity subclass"
it_behaves_like "an XML Entity"
+ it_behaves_like "a JSON Entity"
+
context "default values" do
it "uses default values" do
minimal_xml = <<-XML
diff --git a/spec/lib/diaspora_federation/entity_spec.rb b/spec/lib/diaspora_federation/entity_spec.rb
index 88a9cd9..63d76bf 100644
--- a/spec/lib/diaspora_federation/entity_spec.rb
+++ b/spec/lib/diaspora_federation/entity_spec.rb
@@ -68,7 +68,11 @@ module DiasporaFederation
describe "#to_h" do
it "returns a hash of the internal data" do
entity = Entities::TestDefaultEntity.new(data)
- expect(entity.to_h).to eq(data.transform_values(&:to_s))
+ expect(entity.to_h).to eq(
+ data.map {|key, value|
+ [key, entity.class.class_props[key] == :string ? value.to_s : value]
+ }.to_h
+ )
end
end
@@ -104,34 +108,6 @@ module DiasporaFederation
let(:entity) { Entities::TestEntity.new(test: "asdf") }
let(:entity_xml) { entity.to_xml }
- context "sanity" do
- it "expects an Nokogiri::XML::Element as param" do
- expect {
- Entities::TestEntity.from_xml(entity_xml)
- }.not_to raise_error
- end
-
- it "raises and error when the param is not an Nokogiri::XML::Element" do
- ["asdf", 1234, true, :test, entity].each do |val|
- expect {
- Entity.from_xml(val)
- }.to raise_error ArgumentError, "only Nokogiri::XML::Element allowed"
- end
- end
-
- it "raises an error when the entity class doesn't match the root node" do
- xml = <<-XML
-
- asdf
-
-XML
-
- expect {
- Entity.from_xml(Nokogiri::XML::Document.parse(xml).root)
- }.to raise_error Entity::InvalidRootNode, "'unknown_entity' can't be parsed by DiasporaFederation::Entity"
- end
- end
-
context "returned object" do
subject { Entities::TestEntity.from_xml(entity_xml) }
@@ -145,126 +121,213 @@ XML
end
end
- context "parsing" do
- it "uses xml_name for parsing" do
- xml = <<-XML.strip
-
- asdf
- qwer
-
-XML
-
- entity = Entities::TestEntityWithXmlName.from_xml(Nokogiri::XML::Document.parse(xml).root)
-
- expect(entity).to be_an_instance_of Entities::TestEntityWithXmlName
- expect(entity.test).to eq("asdf")
- expect(entity.qwer).to eq("qwer")
- end
-
- it "allows name for parsing even when property has a xml_name" do
- xml = <<-XML.strip
-
- asdf
- qwer
-
-XML
-
- entity = Entities::TestEntityWithXmlName.from_xml(Nokogiri::XML::Document.parse(xml).root)
-
- expect(entity).to be_an_instance_of Entities::TestEntityWithXmlName
- expect(entity.test).to eq("asdf")
- expect(entity.qwer).to eq("qwer")
- end
-
- it "parses the string to the correct type" do
- xml = <<-XML.strip
+ it "calls .from_hash with the hash representation of provided XML" do
+ expect(Entities::TestDefaultEntity).to receive(:from_hash).with(
+ test1: "asdf",
+ test2: "qwer",
+ test3: true
+ )
+ Entities::TestDefaultEntity.from_xml(Nokogiri::XML::Document.parse(<<-XML).root)
asdf
qwer
true
XML
-
- entity = Entities::TestDefaultEntity.from_xml(Nokogiri::XML::Document.parse(xml).root)
-
- expect(entity).to be_an_instance_of Entities::TestDefaultEntity
- expect(entity.test1).to eq("asdf")
- expect(entity.test2).to eq("qwer")
- expect(entity.test3).to eq(true)
- end
-
- it "parses boolean fields with false value" do
- xml = <<-XML.strip
-
- false
-
-XML
-
- entity = Entities::TestEntityWithBoolean.from_xml(Nokogiri::XML::Document.parse(xml).root)
- expect(entity).to be_an_instance_of Entities::TestEntityWithBoolean
- expect(entity.test).to eq(false)
- end
-
- it "parses boolean fields with a randomly matching pattern as erroneous" do
- %w(ttFFFtt yesFFDSFSDy noDFDSFFDFn fXf LLyes).each do |weird_value|
- xml = <<-XML.strip
-
- #{weird_value}
-
-XML
-
- expect {
- Entities::TestEntityWithBoolean.from_xml(Nokogiri::XML::Document.parse(xml).root)
- }.to raise_error Entity::ValidationError, "missing required properties: test"
- end
- end
-
- it "parses integer fields with a randomly matching pattern as erroneous" do
- %w(1,2,3 foobar two).each do |weird_value|
- xml = <<-XML.strip
-
- #{weird_value}
-
-XML
-
- expect {
- Entities::TestEntityWithInteger.from_xml(Nokogiri::XML::Document.parse(xml).root)
- }.to raise_error Entity::ValidationError, "missing required properties: test"
- end
- end
-
- it "parses timestamp fields with a randomly matching pattern as erroneous" do
- %w(foobar yesterday now 1.2.foo).each do |weird_value|
- xml = <<-XML.strip
-
- #{weird_value}
-
-XML
-
- expect {
- Entities::TestEntityWithTimestamp.from_xml(Nokogiri::XML::Document.parse(xml).root)
- }.to raise_error Entity::ValidationError, "missing required properties: test"
- end
- end
end
- context "nested entities" do
- let(:child_entity1) { Entities::TestEntity.new(test: "bla") }
- let(:child_entity2) { Entities::OtherEntity.new(asdf: "blabla") }
- let(:nested_entity) {
- Entities::TestNestedEntity.new(asdf: "QWERT",
- test: child_entity1,
- multi: [child_entity2, child_entity2])
- }
- let(:nested_payload) { nested_entity.to_xml }
+ it "forms .from_hash arguments basing on parse return array" do
+ arguments = [{arg1: "value"}]
+ expect_any_instance_of(DiasporaFederation::Parsers::XmlParser).to receive(:parse).and_return(arguments)
+ expect(Entities::TestDefaultEntity).to receive(:from_hash).with(*arguments)
+ Entities::TestDefaultEntity.from_xml(Nokogiri::XML::Document.parse("").root)
+ end
- it "parses the xml with all the nested data" do
- entity = Entities::TestNestedEntity.from_xml(nested_payload)
- expect(entity.test.to_h).to eq(child_entity1.to_h)
- expect(entity.multi).to have(2).items
- expect(entity.multi.first.to_h).to eq(child_entity2.to_h)
- expect(entity.asdf).to eq("QWERT")
+ it "passes input parameter directly to .parse method of the parser" do
+ root = Nokogiri::XML::Document.parse("").root
+ expect_any_instance_of(DiasporaFederation::Parsers::XmlParser).to receive(:parse).with(root)
+ .and_return([{test1: "2", test2: "1"}])
+ Entities::TestDefaultEntity.from_xml(root)
+ end
+ end
+
+ describe "#to_json" do
+ let(:basic_props) {
+ {
+ test1: "123",
+ test2: false,
+ test3: "456",
+ test4: 789,
+ test5: Time.current.utc
+ }
+ }
+
+ let(:hash) {
+ basic_props.merge(
+ test6: {
+ test: "000"
+ },
+ multi: [
+ {asdf: "01"},
+ {asdf: "02"}
+ ]
+ )
+ }
+ let(:entity_class) { Entities::TestComplexEntity }
+
+ it "generates expected JSON data" do
+ json_output = entity_class.new(hash).to_json.to_json
+ basic_props[:test5] = basic_props[:test5].iso8601
+ expect(json_output).to include_json(
+ entity_type: "test_complex_entity",
+ entity_data: basic_props.merge(
+ test6: {
+ entity_type: "test_entity",
+ entity_data: {
+ test: "000"
+ }
+ },
+ multi: [
+ {
+ entity_type: "other_entity",
+ entity_data: {
+ asdf: "01"
+ }
+ },
+ {
+ entity_type: "other_entity",
+ entity_data: {
+ asdf: "02"
+ }
+ }
+ ]
+ )
+ )
+ end
+ end
+
+ describe ".from_json" do
+ it "parses entity properties from the input JSON data" do
+ now = Time.now.change(usec: 0).utc
+ entity_data = <<-JSON
+{
+ "entity_type": "test_complex_entity",
+ "entity_data": {
+ "test1": "abc",
+ "test2": false,
+ "test3": "def",
+ "test4": 123,
+ "test5": "#{now.iso8601}",
+ "test6": {
+ "entity_type": "test_entity",
+ "entity_data": {
+ "test": "nested"
+ }
+ },
+ "multi": [
+ {
+ "entity_type": "other_entity",
+ "entity_data": {
+ "asdf": "01"
+ }
+ },
+ {
+ "entity_type": "other_entity",
+ "entity_data": {
+ "asdf": "02"
+ }
+ }
+ ]
+ }
+}
+JSON
+
+ entity = Entities::TestComplexEntity.from_json(JSON.parse(entity_data))
+ expect(entity).to be_an_instance_of(Entities::TestComplexEntity)
+ expect(entity.test1).to eq("abc")
+ expect(entity.test2).to eq(false)
+ expect(entity.test3).to eq("def")
+ expect(entity.test4).to eq(123)
+ expect(entity.test5).to eq(now)
+ expect(entity.test6.test).to eq("nested")
+ expect(entity.multi[0].asdf).to eq("01")
+ expect(entity.multi[1].asdf).to eq("02")
+ end
+
+ it "calls .from_hash with the entity_data of json hash" do
+ json = {
+ "entity_type" => "test_entity",
+ "entity_data" => {
+ "test" => "value"
+ }
+ }
+ expect(Entities::TestEntity).to receive(:json_parser_class).and_call_original
+ expect_any_instance_of(Parsers::JsonParser).to receive(:parse).with(json).and_call_original
+ expect(Entities::TestEntity).to receive(:from_hash).with(test: "value")
+ Entities::TestEntity.from_json(json)
+ end
+
+ it "forms .from_hash arguments basing on parse return array" do
+ class EntityWithFromHashMethod < Entity
+ def self.from_hash(_arg1, _arg2, _arg3); end
end
+
+ expect(EntityWithFromHashMethod).to receive(:json_parser_class).and_call_original
+ expect_any_instance_of(Parsers::JsonParser).to receive(:parse).with("{}").and_return(%i(arg1 arg2 arg3))
+ expect(EntityWithFromHashMethod).to receive(:from_hash).with(:arg1, :arg2, :arg3)
+ EntityWithFromHashMethod.from_json("{}")
+ end
+ end
+
+ describe ".from_hash" do
+ it "parses entity properties from the input data" do
+ now = Time.now.change(usec: 0).utc
+ entity_data = {
+ test1: "abc",
+ test2: false,
+ test3: "def",
+ test4: 123,
+ test5: now,
+ test6: {
+ test: "nested"
+ },
+ multi: [
+ {asdf: "01"},
+ {asdf: "02"}
+ ]
+ }
+
+ entity = Entities::TestComplexEntity.from_hash(entity_data)
+ expect(entity).to be_an_instance_of(Entities::TestComplexEntity)
+ expect(entity.test1).to eq("abc")
+ expect(entity.test2).to eq(false)
+ expect(entity.test3).to eq("def")
+ expect(entity.test4).to eq(123)
+ expect(entity.test5).to eq(now)
+ expect(entity.test6.test).to eq("nested")
+ expect(entity.multi[0].asdf).to eq("01")
+ expect(entity.multi[1].asdf).to eq("02")
+ end
+
+ it "calls a constructor of the entity of the appropriate type" do
+ entity_data = {test1: "abc", test2: "123"}
+ expect(Entities::TestDefaultEntity).to receive(:new).with(test1: "abc", test2: "123")
+ Entities::TestDefaultEntity.from_hash(entity_data)
+ end
+
+ it "supports instantiation of nested entities using objects of the respective type" do
+ entity1 = Entities::TestEntity.new(test: "hello")
+ entity2 = Entities::OtherEntity.new(asdf: "01")
+ entity3 = Entities::OtherEntity.new(asdf: "02")
+ entity_data = {
+ asdf: "value",
+ test: entity1,
+ multi: [entity2, entity3]
+ }
+ entity = Entities::TestNestedEntity.from_hash(entity_data)
+ expect(entity.test).to eq(entity1)
+ expect(entity.multi[0]).to eq(entity2)
+ expect(entity.multi[1]).to eq(entity3)
end
end
diff --git a/spec/lib/diaspora_federation/parsers/base_parser_spec.rb b/spec/lib/diaspora_federation/parsers/base_parser_spec.rb
new file mode 100644
index 0000000..1fdf683
--- /dev/null
+++ b/spec/lib/diaspora_federation/parsers/base_parser_spec.rb
@@ -0,0 +1,11 @@
+module DiasporaFederation
+ describe Parsers::BaseParser do
+ describe ".parse" do
+ it "raises NotImplementedError error" do
+ expect {
+ Parsers::BaseParser.new(Entity).parse
+ }.to raise_error(NotImplementedError, "you must override this method when creating your own parser")
+ end
+ end
+ end
+end
diff --git a/spec/lib/diaspora_federation/parsers/json_parser_spec.rb b/spec/lib/diaspora_federation/parsers/json_parser_spec.rb
new file mode 100644
index 0000000..13c173c
--- /dev/null
+++ b/spec/lib/diaspora_federation/parsers/json_parser_spec.rb
@@ -0,0 +1,95 @@
+module DiasporaFederation
+ describe Parsers::JsonParser do
+ describe ".parse" do
+ let(:entity_class) { Entities::TestComplexEntity }
+ let(:json_parser) { Parsers::JsonParser.new(entity_class) }
+
+ it "raises error when the entity class doesn't match the entity_type property" do
+ expect {
+ json_parser.parse(JSON.parse(<<-JSON
+{
+ "entity_type": "unknown_entity",
+ "entity_data": {}
+}
+JSON
+ ))
+ }.to raise_error DiasporaFederation::Parsers::BaseParser::InvalidRootNode,
+ "'unknown_entity' can't be parsed by #{entity_class}"
+ end
+
+ include_examples ".parse parse error",
+ "Required properties are missing in JSON object: entity_type",
+ '{"entity_data": {}}'
+
+ include_examples ".parse parse error",
+ "Required properties are missing in JSON object: entity_data",
+ '{"entity_type": "test_complex_entity"}'
+
+ it "returns a hash for the correct JSON input" do
+ now = Time.now.change(usec: 0).utc
+ json = <<-JSON
+{
+ "entity_type": "test_complex_entity",
+ "entity_data": {
+ "test1": "abc",
+ "test2": false,
+ "test3": "def",
+ "test4": 123,
+ "test5": "#{now.iso8601}",
+ "test6": {
+ "entity_type": "test_entity",
+ "entity_data": {
+ "test": "nested"
+ }
+ },
+ "multi": [
+ {
+ "entity_type": "other_entity",
+ "entity_data": {
+ "asdf": "01"
+ }
+ },
+ {
+ "entity_type": "other_entity",
+ "entity_data": {
+ "asdf": "02"
+ }
+ }
+ ]
+ }
+}
+JSON
+ hash = json_parser.parse(JSON.parse(json)).first
+ expect(hash).to be_a(Hash)
+ expect(hash[:test1]).to eq("abc")
+ expect(hash[:test2]).to eq(false)
+ expect(hash[:test3]).to eq("def")
+ expect(hash[:test4]).to eq(123)
+ expect(hash[:test5]).to eq(now)
+ expect(hash[:test6]).to be_a(Entities::TestEntity)
+ expect(hash[:test6].test).to eq("nested")
+ expect(hash[:multi]).to be_an(Array)
+ expect(hash[:multi][0]).to be_an(Entities::OtherEntity)
+ expect(hash[:multi][0].asdf).to eq("01")
+ expect(hash[:multi][1].asdf).to eq("02")
+ end
+
+ it "doesn't drop extra properties" do
+ json = <<-JSON.strip
+{
+ "entity_type": "test_default_entity",
+ "entity_data": {
+ "test1": "abc",
+ "test2": false,
+ "test3": "def",
+ "test_new": "new_value"
+ }
+}
+JSON
+
+ parsed = Parsers::JsonParser.new(Entities::TestDefaultEntity).parse(JSON.parse(json))
+ expect(parsed[0]["test_new"]).to eq("new_value")
+ end
+ end
+ end
+end
diff --git a/spec/lib/diaspora_federation/parsers/relayable_json_parser_spec.rb b/spec/lib/diaspora_federation/parsers/relayable_json_parser_spec.rb
new file mode 100644
index 0000000..2c75d5e
--- /dev/null
+++ b/spec/lib/diaspora_federation/parsers/relayable_json_parser_spec.rb
@@ -0,0 +1,31 @@
+module DiasporaFederation
+ describe Parsers::RelayableJsonParser do
+ describe ".parse" do
+ let(:entity_class) { Entities::SomeRelayable }
+ let(:json_parser) { Parsers::RelayableJsonParser.new(entity_class) }
+ include_examples ".parse parse error",
+ "Required property is missing in JSON object: property_order",
+ '{"entity_type": "some_relayable", "entity_data": {}}'
+
+ it "returns property order as a second argument" do
+ json = JSON.parse <<-JSON
+{
+ "entity_type": "some_relayable",
+ "property_order": ["property", "guid", "author"],
+ "entity_data": {
+ "author": "id@example.tld",
+ "guid": "im a guid",
+ "property": "value"
+ }
+}
+JSON
+ parsed_data = json_parser.parse(json)
+ expect(parsed_data[0]).to be_a(Hash)
+ expect(parsed_data[0][:guid]).to eq("im a guid")
+ expect(parsed_data[0][:property]).to eq("value")
+ expect(parsed_data[0][:author]).to eq("id@example.tld")
+ expect(parsed_data[1]).to eq(%w(property guid author))
+ end
+ end
+ end
+end
diff --git a/spec/lib/diaspora_federation/parsers/relayable_xml_parser_spec.rb b/spec/lib/diaspora_federation/parsers/relayable_xml_parser_spec.rb
new file mode 100644
index 0000000..e40b979
--- /dev/null
+++ b/spec/lib/diaspora_federation/parsers/relayable_xml_parser_spec.rb
@@ -0,0 +1,24 @@
+module DiasporaFederation
+ describe Parsers::RelayableXmlParser do
+ describe ".parse" do
+ let(:entity_class) { Entities::SomeRelayable }
+ let(:xml_parser) { Parsers::RelayableXmlParser.new(entity_class) }
+ it "passes order of the XML elements as a second argument in the returned list" do
+ xml_object = Nokogiri::XML::Document.parse(<<-XML).root
+
+ im a guid
+ value
+ id@example.tld
+
+XML
+
+ parsed_data = xml_parser.parse(xml_object)
+ expect(parsed_data[0]).to be_a(Hash)
+ expect(parsed_data[0][:guid]).to eq("im a guid")
+ expect(parsed_data[0][:property]).to eq("value")
+ expect(parsed_data[0][:author]).to eq("id@example.tld")
+ expect(parsed_data[1]).to eq(%i(guid property author))
+ end
+ end
+ end
+end
diff --git a/spec/lib/diaspora_federation/parsers/xml_parser_spec.rb b/spec/lib/diaspora_federation/parsers/xml_parser_spec.rb
new file mode 100644
index 0000000..010b35c
--- /dev/null
+++ b/spec/lib/diaspora_federation/parsers/xml_parser_spec.rb
@@ -0,0 +1,169 @@
+module DiasporaFederation
+ describe Parsers::XmlParser do
+ describe ".parse" do
+ let(:entity_class) { Entities::TestComplexEntity }
+ let(:xml_parser) { Parsers::XmlParser.new(entity_class) }
+
+ it "expects an Nokogiri::XML::Element as param" do
+ expect {
+ Entities::TestEntity.from_xml(Entities::TestEntity.new(test: "asdf").to_xml)
+ }.not_to raise_error
+ end
+
+ it "raises an error when the entity class doesn't match the root node" do
+ xml = <<-XML
+
+ asdf
+
+XML
+
+ expect {
+ xml_parser.parse(Nokogiri::XML::Document.parse(xml).root)
+ }.to raise_error Parsers::BaseParser::InvalidRootNode,
+ "'unknown_entity' can't be parsed by DiasporaFederation::Entities::TestComplexEntity"
+ end
+
+ it "raises an error when the param is not an Nokogiri::XML::Element" do
+ ["asdf", 1234, true, :test].each do |val|
+ expect {
+ xml_parser.parse(val)
+ }.to raise_error ArgumentError, "only Nokogiri::XML::Element allowed"
+ end
+ end
+
+ it "uses xml_name for parsing" do
+ xml = <<-XML.strip
+
+ asdf
+ qwer
+
+XML
+
+ parsed = Parsers::XmlParser.new(Entities::TestEntityWithXmlName).parse(Nokogiri::XML::Document.parse(xml).root)
+
+ expect(parsed[0][:test]).to eq("asdf")
+ expect(parsed[0][:qwer]).to eq("qwer")
+ end
+
+ it "allows name for parsing even when property has a xml_name" do
+ xml = <<-XML.strip
+
+ asdf
+ qwer
+
+XML
+
+ parsed = Parsers::XmlParser.new(Entities::TestEntityWithXmlName).parse(Nokogiri::XML::Document.parse(xml).root)
+
+ expect(parsed[0][:test]).to eq("asdf")
+ expect(parsed[0][:qwer]).to eq("qwer")
+ end
+
+ it "parses the string to the correct type" do
+ xml = <<-XML.strip
+
+ asdf
+ qwer
+ true
+
+XML
+
+ parsed = Parsers::XmlParser.new(Entities::TestDefaultEntity).parse(Nokogiri::XML::Document.parse(xml).root)
+
+ expect(parsed[0][:test1]).to eq("asdf")
+ expect(parsed[0][:test2]).to eq("qwer")
+ expect(parsed[0][:test3]).to eq(true)
+ end
+
+ it "parses boolean fields with false value" do
+ xml = <<-XML.strip
+
+ false
+
+XML
+
+ parsed = Parsers::XmlParser.new(Entities::TestEntityWithBoolean).parse(Nokogiri::XML::Document.parse(xml).root)
+ expect(parsed[0][:test]).to eq(false)
+ end
+
+ it "parses boolean fields with a randomly matching pattern as nil" do
+ %w(ttFFFtt yesFFDSFSDy noDFDSFFDFn fXf LLyes).each do |weird_value|
+ xml = <<-XML.strip
+
+ #{weird_value}
+
+XML
+
+ parsed = Parsers::XmlParser.new(Entities::TestEntityWithBoolean).parse(
+ Nokogiri::XML::Document.parse(xml).root
+ )
+ expect(parsed[0][:test]).to be_nil
+ end
+ end
+
+ it "parses integer fields with a randomly matching pattern as nil" do
+ %w(1,2,3 foobar two).each do |weird_value|
+ xml = <<-XML.strip
+
+ #{weird_value}
+
+XML
+
+ parsed = Parsers::XmlParser.new(Entities::TestEntityWithInteger).parse(
+ Nokogiri::XML::Document.parse(xml).root
+ )
+ expect(parsed[0][:test]).to be_nil
+ end
+ end
+
+ it "parses timestamp fields with a randomly matching pattern as nil" do
+ %w(foobar yesterday now 1.2.foo).each do |weird_value|
+ xml = <<-XML.strip
+
+ #{weird_value}
+
+XML
+
+ parsed = Parsers::XmlParser.new(Entities::TestEntityWithTimestamp).parse(
+ Nokogiri::XML::Document.parse(xml).root
+ )
+ expect(parsed[0][:test]).to be_nil
+ end
+ end
+
+ context "nested entities" do
+ let(:child_entity1) { Entities::TestEntity.new(test: "bla") }
+ let(:child_entity2) { Entities::OtherEntity.new(asdf: "blabla") }
+ let(:nested_entity) {
+ Entities::TestNestedEntity.new(asdf: "QWERT",
+ test: child_entity1,
+ multi: [child_entity2, child_entity2])
+ }
+ let(:nested_payload) { nested_entity.to_xml }
+
+ it "parses the xml with all the nested data" do
+ parsed = Parsers::XmlParser.new(Entities::TestNestedEntity).parse(nested_payload)
+
+ expect(parsed[0][:test].to_h).to eq(child_entity1.to_h)
+ expect(parsed[0][:multi]).to have(2).items
+ expect(parsed[0][:multi].first.to_h).to eq(child_entity2.to_h)
+ expect(parsed[0][:asdf]).to eq("QWERT")
+ end
+ end
+
+ it "doesn't drop extra properties" do
+ xml = <<-XML.strip
+
+ asdf
+ qwer
+ true
+ new_value
+
+XML
+
+ parsed = Parsers::XmlParser.new(Entities::TestDefaultEntity).parse(Nokogiri::XML::Document.parse(xml).root)
+ expect(parsed[0]["test_new"]).to eq("new_value")
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index fdb096a..9f07f48 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -17,6 +17,7 @@ require File.join(File.dirname(__FILE__), "..", "test", "dummy", "config", "envi
require "rspec/rails"
require "webmock/rspec"
+require "rspec/json_expectations"
# load factory girl factories
require "factories"
@@ -52,6 +53,9 @@ support_files.each {|f| require f }
require fixture_builder_file
RSpec.configure do |config|
+ config.include JSON::SchemaMatchers
+ config.json_schemas[:entity_schema] = "lib/diaspora_federation/schemas/federation_entities.json"
+
config.example_status_persistence_file_path = "spec/rspec-persistance.txt"
config.infer_spec_type_from_file_location!
diff --git a/spec/support/shared_entity_specs.rb b/spec/support/shared_entity_specs.rb
index 6dd85c4..9f5c9f5 100644
--- a/spec/support/shared_entity_specs.rb
+++ b/spec/support/shared_entity_specs.rb
@@ -1,3 +1,17 @@
+def entity_hash_from(hash)
+ hash.transform_values {|value|
+ if [String, TrueClass, FalseClass, Integer, NilClass].any? {|c| value.is_a? c }
+ value
+ elsif value.is_a? Time
+ value.iso8601
+ elsif value.instance_of?(Array)
+ value.map(&:to_h)
+ else
+ value.to_h
+ end
+ }
+end
+
shared_examples "an Entity subclass" do
it "should be an Entity" do
expect(described_class).to be < DiasporaFederation::Entity
@@ -26,21 +40,7 @@ shared_examples "an Entity subclass" do
describe "#to_h" do
it "should return a hash with nested data" do
- expected_data = data.transform_values {|value|
- if [String, TrueClass, FalseClass, Integer].any? {|c| value.is_a? c }
- value.to_s
- elsif value.nil?
- nil
- elsif value.is_a? Time
- value.iso8601
- elsif value.instance_of?(Array)
- value.map(&:to_h)
- else
- value.to_h
- end
- }
-
- expect(instance.to_h).to eq(expected_data)
+ expect(instance.to_h).to eq(entity_hash_from(data))
end
end
@@ -133,3 +133,58 @@ shared_examples "a retraction" do
end
end
end
+
+shared_examples "a JSON Entity" do
+ describe "#to_json" do
+ it "#to_json output matches JSON schema" do
+ json = described_class.new(data).to_json
+ expect(json.to_json).to match_json_schema(:entity_schema)
+ end
+
+ let(:to_json_output) { described_class.new(data).to_json.to_json }
+
+ it "contains described_class property matching the entity class (underscored)" do
+ expect(to_json_output).to include_json(entity_type: described_class.entity_name)
+ end
+
+ it "contains JSON properties for each of the entity properties with the entity_data property" do
+ entity_data = entity_hash_from(data)
+ entity_data.delete(:parent)
+ nested_elements = entity_data.select {|_key, value| value.is_a?(Array) || value.is_a?(Hash) }
+ entity_data.reject! {|_key, value| value.is_a?(Array) || value.is_a?(Hash) }
+
+ expect(to_json_output).to include_json(entity_data: entity_data)
+ nested_elements.each {|key, value|
+ type = described_class.class_props[key]
+ if value.is_a?(Array)
+ data = value.map {|element|
+ {
+ entity_type: type.first.entity_name,
+ entity_data: element
+ }
+ }
+ expect(to_json_output).to include_json(entity_data: {key => data})
+ else
+ expect(to_json_output).to include_json(
+ entity_data: {
+ key => {
+ entity_type: type.entity_name,
+ entity_data: value
+ }
+ }
+ )
+ end
+ }
+ end
+
+ it "produces correct JSON" do
+ entity_json = JSON.pretty_generate(described_class.new(data).to_json)
+ expect(entity_json).to eq(json.strip)
+ end
+ end
+
+ it ".from_json(entity_json).to_json should match entity.to_json" do
+ entity_json = described_class.new(data).to_json.to_json
+ expect(described_class.from_json(JSON.parse(entity_json)).to_json.to_json).to eq(entity_json)
+ end
+end
diff --git a/spec/support/shared_parser_specs.rb b/spec/support/shared_parser_specs.rb
new file mode 100644
index 0000000..f5cb26c
--- /dev/null
+++ b/spec/support/shared_parser_specs.rb
@@ -0,0 +1,7 @@
+shared_examples ".parse parse error" do |reason, json|
+ it "raises error when #{reason}" do
+ expect {
+ json_parser.parse(JSON.parse(json))
+ }.to raise_error DiasporaFederation::Parsers::JsonParser::DeserializationError, reason
+ end
+end