532 lines
18 KiB
Ruby
532 lines
18 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
module DiasporaFederation
|
||
describe Entity do
|
||
let(:data) { {test1: "asdf", test2: 1234, test3: false, test4: false} }
|
||
let(:guid) { Fabricate.sequence(:guid) }
|
||
|
||
it "should extend Entity" do
|
||
expect(Entities::TestDefaultEntity).to be < Entity
|
||
end
|
||
|
||
context "creation" do
|
||
it "freezes the instance after initialization" do
|
||
entity = Entities::TestDefaultEntity.new(data)
|
||
expect(entity).to be_frozen
|
||
end
|
||
|
||
context "required properties" do
|
||
it "checks for required properties" do
|
||
expect {
|
||
Entities::TestDefaultEntity.new({})
|
||
}.to raise_error Entity::ValidationError, "TestDefaultEntity: Missing required properties: test1, test2"
|
||
end
|
||
|
||
it "adds the guid to the error message if available" do
|
||
expect {
|
||
Entities::TestDefaultEntity.new(guid: guid)
|
||
}.to raise_error Entity::ValidationError,
|
||
"TestDefaultEntity:#{guid}: Missing required properties: test1, test2"
|
||
end
|
||
|
||
it "adds the author to the error message if available" do
|
||
expect {
|
||
Entities::TestDefaultEntity.new(author: alice.diaspora_id)
|
||
}.to raise_error Entity::ValidationError,
|
||
"TestDefaultEntity from #{alice.diaspora_id}: Missing required properties: test1, test2"
|
||
end
|
||
end
|
||
|
||
context "defaults" do
|
||
it "sets the defaults" do
|
||
entity = Entities::TestDefaultEntity.new(test1: "1", test2: "2")
|
||
expect(entity.test3).to be_truthy
|
||
end
|
||
|
||
it "handles callable defaults" do
|
||
entity = Entities::TestDefaultEntity.new(test1: "1", test2: "2")
|
||
expect(entity.test4).to be_truthy
|
||
end
|
||
|
||
it "uses provided values over defaults" do
|
||
entity = Entities::TestDefaultEntity.new(data)
|
||
expect(entity.test3).to be_falsey
|
||
expect(entity.test4).to be_falsey
|
||
end
|
||
end
|
||
|
||
it "sets nil if string is empty" do
|
||
data[:test1] = ""
|
||
entity = Entities::TestDefaultEntity.new(data)
|
||
expect(entity.test1).to be_nil
|
||
end
|
||
|
||
context "when validating" do
|
||
let(:invalid_data) { {test1: "as;df", test2: nil, test3: "no boolean"} }
|
||
|
||
it "validates the entity and raise an error with failed properties if not valid" do
|
||
expect {
|
||
Entities::TestDefaultEntity.new(invalid_data)
|
||
}.to raise_error Entity::ValidationError,
|
||
/Failed validation for TestDefaultEntity for properties:.*test1.*\|.*test2.*\|.*test3/
|
||
end
|
||
|
||
it "contains the failed rule" do
|
||
expect {
|
||
Entities::TestDefaultEntity.new(invalid_data)
|
||
}.to raise_error Entity::ValidationError, /property: test2, value: nil, rule: not_nil, with params: \{\}/
|
||
end
|
||
|
||
it "contains the params of the failed rule" do
|
||
expect {
|
||
Entities::TestDefaultEntity.new(invalid_data)
|
||
}.to raise_error Entity::ValidationError, /rule: regular_expression, with params: \{:regex=>.*\}/
|
||
end
|
||
|
||
it "adds the guid to the error message if available" do
|
||
expect {
|
||
Entities::TestEntityWithAuthorAndGuid.new(test: "invalid", guid: guid, author: alice.diaspora_id)
|
||
}.to raise_error Entity::ValidationError,
|
||
/Failed validation for TestEntityWithAuthorAndGuid:#{guid} from .* for properties:.*/
|
||
end
|
||
|
||
it "handles missing guid" do
|
||
expect {
|
||
Entities::TestEntityWithAuthorAndGuid.new(test: "invalid", guid: nil, author: alice.diaspora_id)
|
||
}.to raise_error Entity::ValidationError,
|
||
/Failed validation for TestEntityWithAuthorAndGuid: from .* for properties:.*/
|
||
end
|
||
|
||
it "adds the author to the error message if available" do
|
||
expect {
|
||
Entities::TestEntityWithAuthorAndGuid.new(test: "invalid", guid: guid, author: alice.diaspora_id)
|
||
}.to raise_error Entity::ValidationError,
|
||
/Failed validation for .* from #{alice.diaspora_id} for properties:.*/
|
||
end
|
||
|
||
it "handles missing author" do
|
||
expect {
|
||
Entities::TestEntityWithAuthorAndGuid.new(test: "invalid", guid: guid, author: nil)
|
||
}.to raise_error Entity::ValidationError,
|
||
/Failed validation for .* from for properties:.*/
|
||
end
|
||
end
|
||
end
|
||
|
||
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.map {|key, value|
|
||
[key, entity.class.class_props[key] == :string ? value.to_s : value]
|
||
}.to_h
|
||
)
|
||
end
|
||
end
|
||
|
||
describe "#to_xml" do
|
||
it "returns an Nokogiri::XML::Element" do
|
||
entity = Entities::TestDefaultEntity.new(data)
|
||
expect(entity.to_xml).to be_an_instance_of Nokogiri::XML::Element
|
||
end
|
||
|
||
it "has the root node named after the class (underscored)" do
|
||
entity = Entities::TestDefaultEntity.new(data)
|
||
expect(entity.to_xml.name).to eq("test_default_entity")
|
||
end
|
||
|
||
it "contains nodes for each of the properties" do
|
||
entity = Entities::TestDefaultEntity.new(data)
|
||
xml_children = entity.to_xml.children
|
||
expect(xml_children).to have_exactly(4).items
|
||
xml_children.each do |node|
|
||
expect(%w[test1 test2 test3 test4]).to include(node.name)
|
||
end
|
||
end
|
||
|
||
context "optional properties" do
|
||
it "contains nodes for optional properties when not nil" do
|
||
entity = Entities::TestOptionalEntity.new(test1: "aa", test2: "bb")
|
||
xml_children = entity.to_xml.children
|
||
expect(xml_children).to have_exactly(2).items
|
||
xml_children.each do |node|
|
||
expect(%w[test1 test2]).to include(node.name)
|
||
end
|
||
end
|
||
|
||
it "contains no nodes for optional nil properties" do
|
||
entity = Entities::TestOptionalEntity.new(test2: "bb")
|
||
xml_children = entity.to_xml.children
|
||
expect(xml_children).to have_exactly(1).items
|
||
xml_children.each do |node|
|
||
expect(%w[test2]).to include(node.name)
|
||
end
|
||
end
|
||
|
||
it "contains nodes for non optional properties when nil" do
|
||
entity = Entities::TestOptionalEntity.new(test1: "aa", test2: nil)
|
||
xml_children = entity.to_xml.children
|
||
expect(xml_children).to have_exactly(2).items
|
||
xml_children.each do |node|
|
||
expect(%w[test1 test2]).to include(node.name)
|
||
end
|
||
end
|
||
end
|
||
|
||
it "replaces invalid XML characters" do
|
||
entity = Entities::TestEntity.new(test: "asdfasdf asdf💩asdf\nasdf")
|
||
xml = entity.to_xml.to_xml
|
||
parsed = Entities::TestEntity.from_xml(Nokogiri::XML(xml).root).test
|
||
expect(parsed).to eq("asdf<EFBFBD>asdf asdf💩asdf\nasdf")
|
||
end
|
||
end
|
||
|
||
describe ".from_xml" do
|
||
let(:entity) { Entities::TestEntity.new(test: "asdf") }
|
||
let(:entity_xml) { entity.to_xml }
|
||
|
||
context "returned object" do
|
||
subject { Entities::TestEntity.from_xml(entity_xml) }
|
||
|
||
it "#to_h should match entity.to_h" do
|
||
expect(subject.to_h).to eq(entity.to_h)
|
||
end
|
||
|
||
it "returns an entity instance of the original class" do
|
||
expect(subject).to be_an_instance_of Entities::TestEntity
|
||
expect(subject.test).to eq("asdf")
|
||
end
|
||
end
|
||
|
||
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(<<~XML).root)
|
||
<test_default_entity>
|
||
<test1>asdf</test1>
|
||
<test2>qwer</qwer2>
|
||
<test3>true</qwer3>
|
||
</test_default_entity>
|
||
XML
|
||
end
|
||
|
||
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("<dummy/>").root)
|
||
end
|
||
|
||
it "passes input parameter directly to .parse method of the parser" do
|
||
root = Nokogiri::XML("<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.now.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 = change_time(Time.now.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 = change_time(Time.now.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
|
||
|
||
describe ".entity_name" do
|
||
it "strips the module and returns the name underscored" do
|
||
expect(Entities::TestDefaultEntity.entity_name).to eq("test_default_entity")
|
||
expect(Entities::TestNestedEntity.entity_name).to eq("test_nested_entity")
|
||
expect(Entities::OtherEntity.entity_name).to eq("other_entity")
|
||
end
|
||
|
||
it "works with a single word" do
|
||
expect(Entities::Entity.entity_name).to eq("entity")
|
||
end
|
||
end
|
||
|
||
describe ".entity_class" do
|
||
it "should parse a single word" do
|
||
expect(Entity.entity_class("entity")).to eq(Entities::Entity)
|
||
end
|
||
|
||
it "should parse with underscore" do
|
||
expect(Entity.entity_class("test_entity")).to eq(Entities::TestEntity)
|
||
end
|
||
|
||
it "should not change the input string" do
|
||
entity_name = "test_entity"
|
||
Entity.entity_class(entity_name)
|
||
expect(entity_name).to eq("test_entity")
|
||
end
|
||
|
||
it "raises an error when the entity name contains special characters" do
|
||
expect {
|
||
Entity.entity_class("te.st-enti/ty")
|
||
}.to raise_error Entity::InvalidEntityName, "'te.st-enti/ty' is invalid"
|
||
end
|
||
|
||
it "raises an error when the entity name contains upper case letters" do
|
||
expect {
|
||
Entity.entity_class("TestEntity")
|
||
}.to raise_error Entity::InvalidEntityName, "'TestEntity' is invalid"
|
||
end
|
||
|
||
it "raises an error when the entity name contains numbers" do
|
||
expect {
|
||
Entity.entity_class("te5t_ent1ty_w1th_number5")
|
||
}.to raise_error Entity::InvalidEntityName, "'te5t_ent1ty_w1th_number5' is invalid"
|
||
end
|
||
|
||
it "raises an error when the entity is unknown" do
|
||
expect {
|
||
Entity.entity_class("unknown_entity")
|
||
}.to raise_error Entity::UnknownEntity, "'UnknownEntity' not found"
|
||
end
|
||
end
|
||
|
||
context "nested entities" do
|
||
let(:nested_data) {
|
||
{
|
||
asdf: "FDSA",
|
||
test: Entities::TestEntity.new(test: "test"),
|
||
multi: [Entities::OtherEntity.new(asdf: "asdf"), Entities::OtherEntity.new(asdf: "asdf")]
|
||
}
|
||
}
|
||
let(:nested_hash) {
|
||
{
|
||
asdf: nested_data[:asdf],
|
||
test: nested_data[:test].to_h,
|
||
multi: nested_data[:multi].map(&:to_h)
|
||
}
|
||
}
|
||
|
||
it "gets returned as Hash by #to_h" do
|
||
entity = Entities::TestNestedEntity.new(nested_data)
|
||
|
||
expect(entity.to_h).to eq(nested_hash)
|
||
end
|
||
|
||
it "gets xml-ified by #to_xml" do
|
||
entity = Entities::TestNestedEntity.new(nested_data)
|
||
xml = entity.to_xml
|
||
expect(xml.children).to have_exactly(4).items
|
||
xml.children.each do |node|
|
||
expect(%w[asdf test_entity other_entity]).to include(node.name)
|
||
end
|
||
expect(xml.xpath("test_entity")).to have_exactly(1).items
|
||
expect(xml.xpath("other_entity")).to have_exactly(2).items
|
||
end
|
||
|
||
it "is not added to xml if #to_xml returns nil" do
|
||
entity = Entities::TestEntityWithRelatedEntity.new(test: "test", parent: Fabricate(:related_entity))
|
||
xml = entity.to_xml
|
||
expect(xml.children).to have_exactly(1).items
|
||
xml.children.first.name = "test"
|
||
end
|
||
|
||
it "instantiates nested entities if provided as hash" do
|
||
entity = Entities::TestNestedEntity.new(nested_hash)
|
||
|
||
expect(entity.test).to be_instance_of(Entities::TestEntity)
|
||
expect(entity.test.test).to eq("test")
|
||
|
||
expect(entity.multi).to be_instance_of(Array)
|
||
expect(entity.multi).to have_exactly(2).items
|
||
expect(entity.multi.first).to be_instance_of(Entities::OtherEntity)
|
||
expect(entity.multi.first.asdf).to eq("asdf")
|
||
end
|
||
|
||
it "handles empty xml-element for nested entities" do
|
||
xml = <<~XML
|
||
<test_nested_entity>
|
||
<asdf>FDSA</asdf>
|
||
<test_entity/>
|
||
<other_entity/>
|
||
</test_nested_entity>
|
||
XML
|
||
|
||
entity = Entities::TestNestedEntity.from_xml(Nokogiri::XML(xml).root)
|
||
|
||
expect(entity.asdf).to eq("FDSA")
|
||
expect(entity.test).to be_nil
|
||
expect(entity.multi).to be_empty
|
||
end
|
||
end
|
||
end
|
||
end
|