Entity JSON serialization/deserialization feature

This commit is contained in:
cmrd Senya 2017-03-23 13:48:15 +02:00
parent e1ad855cd8
commit c58d076c96
No known key found for this signature in database
GPG key ID: 5FCC5BA680E67BFE
45 changed files with 1977 additions and 421 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,6 +28,10 @@ module DiasporaFederation
def to_xml
nil
end
def to_json
nil
end
end
end
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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:<br>
# 1) Properties of the hash are representation of the entity's class properties<br>
# 2) Keys of the hash must be of Symbol type<br>
# 3) Possible values of the hash properties depend on the types of the entity's class properties<br>
# 4) Basic properties, such as booleans, strings, integers and timestamps are represented by values of respective
# formats<br>
# 5) Nested hashes and arrays of hashes are allowed to represent nested entities. Nested hashes follow the same
# format as the parent hash.<br>
# 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<Entity>] 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,12 +24,35 @@ module DiasporaFederation
</comment>
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

View file

@ -24,12 +24,36 @@ module DiasporaFederation
</like>
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

View file

@ -10,10 +10,23 @@ module DiasporaFederation
</location>
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

View file

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

View file

@ -23,12 +23,34 @@ module DiasporaFederation
</participation>
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

View file

@ -17,12 +17,32 @@ module DiasporaFederation
</photo>
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

View file

@ -9,10 +9,22 @@ module DiasporaFederation
</poll_answer>
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

View file

@ -22,12 +22,34 @@ module DiasporaFederation
</poll_participation>
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

View file

@ -10,10 +10,25 @@ module DiasporaFederation
</poll>
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

View file

@ -20,12 +20,35 @@ module DiasporaFederation
</profile>
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

View file

@ -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
<some_relayable>
<author_signature/>
<parent_author_signature/>
</some_relayable>
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

View file

@ -15,12 +15,29 @@ module DiasporaFederation
</reshare>
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

View file

@ -54,12 +54,70 @@ module DiasporaFederation
</status_message>
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

View file

@ -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
<unknown_entity>
<test>asdf</test>
</unknown_entity>
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
<test_entity_with_xml_name>
<test>asdf</test>
<asdf>qwer</asdf>
</test_entity_with_xml_name>
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
<test_entity_with_xml_name>
<test>asdf</test>
<qwer>qwer</qwer>
</test_entity_with_xml_name>
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)
<test_default_entity>
<test1>asdf</test1>
<test2>qwer</qwer2>
<test3>true</qwer3>
</test_default_entity>
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
<test_entity_with_boolean>
<test>false</test>
</test_entity_with_boolean>
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
<test_entity_with_boolean>
<test>#{weird_value}</test>
</test_entity_with_boolean>
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
<test_entity_with_integer>
<test>#{weird_value}</test>
</test_entity_with_integer>
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
<test_entity_with_timestamp>
<test>#{weird_value}</test>
</test_entity_with_timestamp>
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("<dummy/>").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("<dummy/>").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

View file

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

View file

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

View file

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

View file

@ -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
<some_relayable>
<guid>im a guid</guid>
<property>value</property>
<author>id@example.tld</author>
</some_relayable>
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

View file

@ -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
<unknown_entity>
<test>asdf</test>
</unknown_entity>
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
<test_entity_with_xml_name>
<test>asdf</test>
<asdf>qwer</asdf>
</test_entity_with_xml_name>
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
<test_entity_with_xml_name>
<test>asdf</test>
<qwer>qwer</qwer>
</test_entity_with_xml_name>
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
<test_default_entity>
<test1>asdf</test1>
<test2>qwer</qwer2>
<test3>true</qwer3>
</test_default_entity>
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
<test_entity_with_boolean>
<test>false</test>
</test_entity_with_boolean>
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
<test_entity_with_boolean>
<test>#{weird_value}</test>
</test_entity_with_boolean>
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
<test_entity_with_integer>
<test>#{weird_value}</test>
</test_entity_with_integer>
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
<test_entity_with_timestamp>
<test>#{weird_value}</test>
</test_entity_with_timestamp>
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
<test_default_entity>
<test1>asdf</test1>
<test2>qwer</test2>
<test3>true</test3>
<test_new>new_value</test_new>
</test_default_entity>
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

View file

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

View file

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

View file

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