# 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 "validation" 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�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) asdf qwer true 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("").root) end it "passes input parameter directly to .parse method of the parser" do root = Nokogiri::XML("").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 FDSA 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