Merge pull request #52 from cmrd-senya/to_json
Entity#to_json/.from_json methods
This commit is contained in:
commit
423465c32f
45 changed files with 1977 additions and 421 deletions
2
Gemfile
2
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
|
||||
|
||||
|
|
|
|||
12
Gemfile.lock
12
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
|
||||
|
|
|
|||
19
diaspora_federation-json_schema.gemspec
Normal file
19
diaspora_federation-json_schema.gemspec
Normal 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ module DiasporaFederation
|
|||
def to_xml
|
||||
nil
|
||||
end
|
||||
|
||||
def to_json
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
13
lib/diaspora_federation/parsers.rb
Normal file
13
lib/diaspora_federation/parsers.rb
Normal 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"
|
||||
61
lib/diaspora_federation/parsers/base_parser.rb
Normal file
61
lib/diaspora_federation/parsers/base_parser.rb
Normal 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
|
||||
60
lib/diaspora_federation/parsers/json_parser.rb
Normal file
60
lib/diaspora_federation/parsers/json_parser.rb
Normal 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
|
||||
25
lib/diaspora_federation/parsers/relayable_json_parser.rb
Normal file
25
lib/diaspora_federation/parsers/relayable_json_parser.rb
Normal 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
|
||||
22
lib/diaspora_federation/parsers/relayable_xml_parser.rb
Normal file
22
lib/diaspora_federation/parsers/relayable_xml_parser.rb
Normal 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
|
||||
84
lib/diaspora_federation/parsers/xml_parser.rb
Normal file
84
lib/diaspora_federation/parsers/xml_parser.rb
Normal 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
|
||||
373
lib/diaspora_federation/schemas/federation_entities.json
Normal file
373
lib/diaspora_federation/schemas/federation_entities.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
11
spec/lib/diaspora_federation/parsers/base_parser_spec.rb
Normal file
11
spec/lib/diaspora_federation/parsers/base_parser_spec.rb
Normal 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
|
||||
95
spec/lib/diaspora_federation/parsers/json_parser_spec.rb
Normal file
95
spec/lib/diaspora_federation/parsers/json_parser_spec.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
169
spec/lib/diaspora_federation/parsers/xml_parser_spec.rb
Normal file
169
spec/lib/diaspora_federation/parsers/xml_parser_spec.rb
Normal 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
|
||||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
7
spec/support/shared_parser_specs.rb
Normal file
7
spec/support/shared_parser_specs.rb
Normal 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
|
||||
Loading…
Reference in a new issue