From bf348e9544fbed71e984c22b51afef0655cd6984 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Mon, 18 Jul 2016 23:26:25 +0300 Subject: [PATCH] Convert outbound entities to correct protocol types When sending an entity, first convert it to the correct entity using the protocol entities. If a suitable entity is not found, raise an error. Closes #27 --- CHANGELOG.md | 1 + federation/controllers.py | 5 ++- federation/entities/diaspora/entities.py | 16 ++++--- federation/entities/diaspora/mappers.py | 32 ++++++++++++- federation/entities/diaspora/utils.py | 16 +++++++ .../tests/entities/diaspora/test_mappers.py | 45 ++++++++++++++++++- .../tests/entities/diaspora/test_utils.py | 12 +++++ federation/tests/test_controllers.py | 12 ++++- 8 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 federation/tests/entities/diaspora/test_utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e254ab9..bbb2e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## Changed - Unlock most of the direct dependencies to a certain version range. Unlock all of test requirements to any version. +- Entities passed to `federation.controllers.handle_create_payload` are now converted from the base entity types (Post, Comment, Reaction, etc) to Diaspora entity types (DiasporaPost, DiasporaComment, DiasporaLike, etc). This ensures actual payload generation has the correct methods available (for example `to_xml`) whatever entity is passed in. ### Fixes - Fix fetching sender handle from Diaspora protocol private messages. As it is not contained in the header, it needs to be read from the message content itself. diff --git a/federation/controllers.py b/federation/controllers.py index 55523c9..444091d 100644 --- a/federation/controllers.py +++ b/federation/controllers.py @@ -1,5 +1,6 @@ import importlib +from federation.entities.diaspora.mappers import get_outbound_entity from federation.exceptions import NoSuitableProtocolFoundError from federation.protocols.diaspora.protocol import Protocol @@ -51,6 +52,8 @@ def handle_create_payload(from_user, to_user, entity): `from_user` must have `private_key` and `handle` attributes. `to_user` must have `key` attribute. """ + # Just use Diaspora protocol for now protocol = Protocol() - data = protocol.build_send(from_user=from_user, to_user=to_user, entity=entity) + outbound_entity = get_outbound_entity(entity) + data = protocol.build_send(from_user=from_user, to_user=to_user, entity=outbound_entity) return data diff --git a/federation/entities/diaspora/entities.py b/federation/entities/diaspora/entities.py index 959cca1..e6c6eed 100644 --- a/federation/entities/diaspora/entities.py +++ b/federation/entities/diaspora/entities.py @@ -2,10 +2,16 @@ from lxml import etree from federation.entities.base import Comment, Post, Reaction, Relationship -from federation.entities.diaspora.utils import format_dt, struct_to_xml +from federation.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes -class DiasporaComment(Comment): +class DiasporaEntityMixin(object): + @classmethod + def from_base(cls, entity): + return cls(**get_base_attributes(entity)) + + +class DiasporaComment(DiasporaEntityMixin, Comment): """Diaspora comment.""" author_signature = "" @@ -21,7 +27,7 @@ class DiasporaComment(Comment): return element -class DiasporaPost(Post): +class DiasporaPost(DiasporaEntityMixin, Post): """Diaspora post, ie status message.""" def to_xml(self): """Convert to XML message.""" @@ -36,7 +42,7 @@ class DiasporaPost(Post): return element -class DiasporaLike(Reaction): +class DiasporaLike(DiasporaEntityMixin, Reaction): """Diaspora like.""" author_signature = "" reaction = "like" @@ -55,7 +61,7 @@ class DiasporaLike(Reaction): return element -class DiasporaRequest(Relationship): +class DiasporaRequest(DiasporaEntityMixin, Relationship): """Diaspora relationship request.""" relationship = "sharing" diff --git a/federation/entities/diaspora/mappers.py b/federation/entities/diaspora/mappers.py index 915def8..eea2b59 100644 --- a/federation/entities/diaspora/mappers.py +++ b/federation/entities/diaspora/mappers.py @@ -3,7 +3,7 @@ from datetime import datetime from lxml import etree -from federation.entities.base import Image, Relationship +from federation.entities.base import Image, Relationship, Post, Reaction, Comment from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest MAPPINGS = { @@ -69,3 +69,33 @@ def transform_attributes(attrs): else: transformed[key] = value return transformed + + +def get_outbound_entity(entity): + """Get the correct outbound entity for this protocol. + + We might have to look at entity values to decide the correct outbound entity. + If we cannot find one, we should raise as conversion cannot be guaranteed to the given protocol. + + Args: + entity - any of the base entity types from federation.entities.base + + Returns: + An instance of the correct protocol specific entity. + """ + cls = entity.__class__ + if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike]: + # Already fine + return entity + elif cls == Post: + return DiasporaPost.from_base(entity) + elif cls == Comment: + return DiasporaComment.from_base(entity) + elif cls == Reaction: + if entity.reaction == "like": + return DiasporaLike.from_base(entity) + elif cls == Relationship: + if entity.relationship in ["sharing", "following"]: + # Unfortunately we must send out in both cases since in Diaspora they are the same thing + return DiasporaRequest.from_base(entity) + raise ValueError("Don't know how to convert this base entity to Diaspora protocol entities.") diff --git a/federation/entities/diaspora/utils.py b/federation/entities/diaspora/utils.py index 073b4f4..28f1571 100644 --- a/federation/entities/diaspora/utils.py +++ b/federation/entities/diaspora/utils.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import inspect + from dateutil.tz import tzlocal, tzutc from lxml import etree @@ -32,3 +34,17 @@ def struct_to_xml(node, struct): for obj in struct: for k, v in obj.items(): etree.SubElement(node, k).text = v + + +def get_base_attributes(entity): + """Build a dict of attributes of an entity. + + Returns attributes and their values, ignoring any properties, functions and anything that starts + with an underscore. + """ + attributes = {} + cls = entity.__class__ + for attr, _ in inspect.getmembers(cls, lambda o: not isinstance(o, property) and not inspect.isroutine(o)): + if not attr.startswith("_"): + attributes[attr] = getattr(entity, attr) + return attributes diff --git a/federation/tests/entities/diaspora/test_mappers.py b/federation/tests/entities/diaspora/test_mappers.py index 6f23d32..5a0ddf0 100644 --- a/federation/tests/entities/diaspora/test_mappers.py +++ b/federation/tests/entities/diaspora/test_mappers.py @@ -1,15 +1,16 @@ # -*- coding: utf-8 -*- from datetime import datetime +import pytest + from federation.entities.base import Comment, Post, Reaction, Relationship from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest -from federation.entities.diaspora.mappers import message_to_objects +from federation.entities.diaspora.mappers import message_to_objects, get_outbound_entity from federation.tests.fixtures.payloads import DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE, \ DIASPORA_REQUEST class TestDiasporaEntityMappersReceive(object): - def test_message_to_objects_simple_post(self): entities = message_to_objects(DIASPORA_POST_SIMPLE) assert len(entities) == 1 @@ -61,3 +62,43 @@ class TestDiasporaEntityMappersReceive(object): assert following.target_handle == "alice@alice.diaspora.example.org" assert sharing.relationship == "sharing" assert following.relationship == "following" + + +class TestGetOutboundEntity(object): + def test_already_fine_entities_are_returned_as_is(self): + entity = DiasporaPost() + assert get_outbound_entity(entity) == entity + entity = DiasporaLike() + assert get_outbound_entity(entity) == entity + entity = DiasporaComment() + assert get_outbound_entity(entity) == entity + entity = DiasporaRequest() + assert get_outbound_entity(entity) == entity + + def test_post_is_converted_to_diasporapost(self): + entity = Post() + assert isinstance(get_outbound_entity(entity), DiasporaPost) + + def test_comment_is_converted_to_diasporacomment(self): + entity = Comment() + assert isinstance(get_outbound_entity(entity), DiasporaComment) + + def test_reaction_of_like_is_converted_to_diasporaplike(self): + entity = Reaction(reaction="like") + assert isinstance(get_outbound_entity(entity), DiasporaLike) + + def test_relationship_of_sharing_or_following_is_converted_to_diasporarequest(self): + entity = Relationship(relationship="sharing") + assert isinstance(get_outbound_entity(entity), DiasporaRequest) + entity = Relationship(relationship="following") + assert isinstance(get_outbound_entity(entity), DiasporaRequest) + + def test_other_reaction_raises(self): + entity = Reaction(reaction="foo") + with pytest.raises(ValueError): + get_outbound_entity(entity) + + def test_other_relation_raises(self): + entity = Relationship(relationship="foo") + with pytest.raises(ValueError): + get_outbound_entity(entity) diff --git a/federation/tests/entities/diaspora/test_utils.py b/federation/tests/entities/diaspora/test_utils.py new file mode 100644 index 0000000..81db5b8 --- /dev/null +++ b/federation/tests/entities/diaspora/test_utils.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from federation.entities.base import Post +from federation.entities.diaspora.utils import get_base_attributes + + +class TestGetBaseAttributes(object): + def test_get_base_attributes_returns_only_intended_attributes(self): + entity = Post() + attrs = get_base_attributes(entity).keys() + assert set(attrs) == { + 'created_at', 'guid', 'handle', 'location', 'photos', 'provider_display_name', 'public', 'raw_content' + } diff --git a/federation/tests/test_controllers.py b/federation/tests/test_controllers.py index 4101b8e..cb850cd 100644 --- a/federation/tests/test_controllers.py +++ b/federation/tests/test_controllers.py @@ -11,7 +11,6 @@ from federation.tests.fixtures.payloads import UNENCRYPTED_DIASPORA_PAYLOAD class TestHandleReceiveProtocolIdentification(object): - def test_handle_receive_routes_to_identified_protocol(self): payload = UNENCRYPTED_DIASPORA_PAYLOAD with patch.object( @@ -31,7 +30,6 @@ class TestHandleReceiveProtocolIdentification(object): class TestHandleCreatePayloadBuildsAPayload(object): - def test_handle_create_payload_builds_an_xml(self): from_user = Mock(private_key=RSA.generate(2048), handle="foobar@domain.tld") to_user = Mock(key=RSA.generate(2048).publickey()) @@ -42,3 +40,13 @@ class TestHandleCreatePayloadBuildsAPayload(object): assert len(parts) == 2 assert parts[0] == "xml" assert len(parts[1]) > 0 + + @patch("federation.controllers.get_outbound_entity") + def test_handle_create_payload_calls_get_outbound_entity(self, mock_get_outbound_entity): + mock_get_outbound_entity.return_value = DiasporaPost() + from_user = Mock(private_key=RSA.generate(2048), handle="foobar@domain.tld") + to_user = Mock(key=RSA.generate(2048).publickey()) + entity = DiasporaPost() + handle_create_payload(from_user, to_user, entity) + assert mock_get_outbound_entity.called +