From fb2f9d091d7c2de90345374b88e49df6c98debce Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Thu, 17 Aug 2017 21:33:18 +0300 Subject: [PATCH] Added base entity Share Maps to a `DiasporaReshare` for the Diaspora protocol. The `Share` entity supports all the properties that a Diaspora reshare does. Additionally two other properties are supported: `raw_content` and `entity_type`. The former can be used for a "quoted share" case where the sharer adds their own note to the share. The latter can be used to reference the type of object that was shared, to help the receiver, if it is not sharing a `Post` entity. The value must be a base entity class name. Closes #94 --- CHANGELOG.md | 5 + docs/protocols.rst | 1 + docs/usage.rst | 1 + federation/entities/base.py | 121 +++++++++++------- federation/entities/diaspora/entities.py | 22 +++- federation/entities/diaspora/mappers.py | 18 +-- .../tests/entities/diaspora/test_entities.py | 18 ++- .../tests/entities/diaspora/test_mappers.py | 47 ++++++- federation/tests/entities/test_base.py | 55 ++++---- federation/tests/factories/entities.py | 15 ++- federation/tests/fixtures/payloads.py | 38 ++++++ 11 files changed, 255 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c5f19..19755b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [unreleased] +### Added +* Added base entity `Share` which maps to a `DiasporaReshare` for the Diaspora protocol. ([related issue](https://github.com/jaywink/federation/issues/94)) + + The `Share` entity supports all the properties that a Diaspora reshare does. Additionally two other properties are supported: `raw_content` and `entity_type`. The former can be used for a "quoted share" case where the sharer adds their own note to the share. The latter can be used to reference the type of object that was shared, to help the receiver, if it is not sharing a `Post` entity. The value must be a base entity class name. + ### Fixed * Converting base entity `Profile` to `DiasporaProfile` for outbound sending missed two attributes, `image_urls` and `tag_list`. Those are now included so that the values transfer into the built payload. diff --git a/docs/protocols.rst b/docs/protocols.rst index 443cff1..a12f202 100644 --- a/docs/protocols.rst +++ b/docs/protocols.rst @@ -24,6 +24,7 @@ The feature set supported by this release is approximately the following: * Retraction * StatusMessage * Contact + * Reshare Implementation unfortunately currently requires knowledge of how Diaspora discovery works as the implementer has to implement all the necessary views correctly (even though this library provides document generators). However, the magic envelope, signature and entity building is all abstracted inside the library. diff --git a/docs/usage.rst b/docs/usage.rst index e69a918..5a84ebd 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -17,6 +17,7 @@ Entity types are as follows below. .. autoclass:: federation.entities.base.Reaction .. autoclass:: federation.entities.base.Relationship .. autoclass:: federation.entities.base.Retraction +.. autoclass:: federation.entities.base.Share Protocol entities ................. diff --git a/federation/entities/base.py b/federation/entities/base.py index 989516a..b8f8ba5 100644 --- a/federation/entities/base.py +++ b/federation/entities/base.py @@ -5,7 +5,7 @@ from dirty_validators.basic import Email __all__ = ( - "Post", "Image", "Comment", "Reaction", "Relationship", "Profile", "Retraction", "Follow", + "Post", "Image", "Comment", "Reaction", "Relationship", "Profile", "Retraction", "Follow", "Share," ) @@ -125,6 +125,24 @@ class TargetGUIDMixin(BaseEntity): raise ValueError("Target GUID must be at least 16 characters") +class ParticipationMixin(TargetGUIDMixin): + """Reflects a participation to something.""" + participation = "" + + _participation_valid_values = ["reaction", "subscription", "comment"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._required += ["participation"] + + def validate_participation(self): + """Ensure participation is of a certain type.""" + if self.participation not in self._participation_valid_values: + raise ValueError("participation should be one of: {valid}".format( + valid=", ".join(self._participation_valid_values) + )) + + class HandleMixin(BaseEntity): handle = "" @@ -178,7 +196,43 @@ class OptionalRawContentMixin(RawContentMixin): self._required.remove("raw_content") -class Image(GUIDMixin, HandleMixin, PublicMixin, OptionalRawContentMixin, CreatedAtMixin, BaseEntity): +class EntityTypeMixin(BaseEntity): + """Provides a field for entity type. + + Validates it is one of our entities. + """ + entity_type = "" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._required += ["entity_type"] + + def validate_entity_type(self): + """Ensure type is some entity we know of.""" + if self.entity_type not in __all__: + raise ValueError("Entity type %s not recognized." % self.entity_type) + + +class ProviderDisplayNameMixin(BaseEntity): + """Provides a field for provider display name.""" + provider_display_name = "" + + +class TargetHandleMixin(BaseEntity): + """Provides a target handle field.""" + target_handle = "" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._required += ["target_handle"] + + def validate_target_handle(self): + validator = Email() + if not validator.is_valid(self.target_handle): + raise ValueError("Target handle is not valid") + + +class Image(GUIDMixin, HandleMixin, PublicMixin, OptionalRawContentMixin, CreatedAtMixin): """Reflects a single image, possibly linked to another object.""" remote_path = "" remote_name = "" @@ -192,32 +246,13 @@ class Image(GUIDMixin, HandleMixin, PublicMixin, OptionalRawContentMixin, Create self._required += ["remote_path", "remote_name"] -class Post(RawContentMixin, GUIDMixin, HandleMixin, PublicMixin, CreatedAtMixin, BaseEntity): +class Post(RawContentMixin, GUIDMixin, HandleMixin, PublicMixin, CreatedAtMixin, ProviderDisplayNameMixin): """Reflects a post, status message, etc, which will be composed from the message or to the message.""" - provider_display_name = "" location = "" _allowed_children = (Image,) -class ParticipationMixin(TargetGUIDMixin): - """Reflects a participation to something.""" - participation = "" - - _participation_valid_values = ["reaction", "subscription", "comment"] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._required += ["participation"] - - def validate_participation(self): - """Ensure participation is of a certain type.""" - if self.participation not in self._participation_valid_values: - raise ValueError("participation should be one of: {valid}".format( - valid=", ".join(self._participation_valid_values) - )) - - class Comment(RawContentMixin, GUIDMixin, ParticipationMixin, CreatedAtMixin, HandleMixin): """Represents a comment, linked to another object.""" participation = "comment" @@ -247,21 +282,15 @@ class Reaction(GUIDMixin, ParticipationMixin, CreatedAtMixin, HandleMixin): )) -class Relationship(CreatedAtMixin, HandleMixin): +class Relationship(CreatedAtMixin, HandleMixin, TargetHandleMixin): """Represents a relationship between two handles.""" - target_handle = "" relationship = "" _relationship_valid_values = ["sharing", "following", "ignoring", "blocking"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._required += ["relationship", "target_handle"] - - def validate_target_handle(self): - validator = Email() - if not validator.is_valid(self.target_handle): - raise ValueError("Target handle is not valid") + self._required += ["relationship"] def validate_relationship(self): """Ensure relationship is of a certain type.""" @@ -271,19 +300,13 @@ class Relationship(CreatedAtMixin, HandleMixin): )) -class Follow(CreatedAtMixin, HandleMixin): +class Follow(CreatedAtMixin, HandleMixin, TargetHandleMixin): """Represents a handle following or unfollowing another handle.""" - target_handle = "" following = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._required += ["target_handle", "following"] - - def validate_target_handle(self): - validator = Email() - if not validator.is_valid(self.target_handle): - raise ValueError("Target handle is not valid") + self._required += ["following"] class Profile(CreatedAtMixin, HandleMixin, OptionalRawContentMixin, PublicMixin, GUIDMixin): @@ -313,15 +336,19 @@ class Profile(CreatedAtMixin, HandleMixin, OptionalRawContentMixin, PublicMixin, raise ValueError("Email is not valid") -class Retraction(CreatedAtMixin, HandleMixin, TargetGUIDMixin): +class Retraction(CreatedAtMixin, HandleMixin, TargetGUIDMixin, EntityTypeMixin): """Represents a retraction of content by author.""" - entity_type = "" + pass - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._required += ["entity_type"] - def validate_entity_type(self): - """Ensure type is some entity we know of.""" - if self.entity_type not in __all__: - raise ValueError("Entity type %s not recognized." % self.entity_type) +class Share(CreatedAtMixin, HandleMixin, TargetGUIDMixin, GUIDMixin, EntityTypeMixin, OptionalRawContentMixin, + PublicMixin, ProviderDisplayNameMixin, TargetHandleMixin): + """Represents a share of another entity. + + ``entity_type`` defaults to "Post" but can be any base entity class name. It should be the class name of the + entity that was shared. + + The optional ``raw_content`` can be used for a "quoted share" case where the sharer adds their own note to the + share. + """ + entity_type = "Post" diff --git a/federation/entities/diaspora/entities.py b/federation/entities/diaspora/entities.py index 5e23523..d7392aa 100644 --- a/federation/entities/diaspora/entities.py +++ b/federation/entities/diaspora/entities.py @@ -1,6 +1,7 @@ from lxml import etree -from federation.entities.base import Comment, Post, Reaction, Relationship, Profile, Retraction, BaseEntity, Follow +from federation.entities.base import ( + Comment, Post, Reaction, Relationship, Profile, Retraction, BaseEntity, Follow, Share) from federation.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes, add_element_to_doc from federation.exceptions import SignatureVerificationError from federation.protocols.diaspora.signatures import verify_relayable_signature, create_relayable_signature @@ -211,3 +212,22 @@ class DiasporaRetraction(DiasporaEntityMixin, Retraction): index = values.index(value) return list(DiasporaRetraction.mapped.keys())[index] return value + + +class DiasporaReshare(DiasporaEntityMixin, Share): + """Diaspora Reshare.""" + def to_xml(self): + element = etree.Element("reshare") + struct_to_xml(element, [ + {"author": self.handle}, + {"guid": self.guid}, + {"created_at": format_dt(self.created_at)}, + {"root_author": self.target_handle}, + {"root_guid": self.target_guid}, + {"provider_display_name": self.provider_display_name}, + {"public": "true" if self.public else "false"}, + # Some of our own not in Diaspora protocol + {"raw_content": self.raw_content}, + {"entity_type": self.entity_type}, + ]) + return element diff --git a/federation/entities/diaspora/mappers.py b/federation/entities/diaspora/mappers.py index bb8a174..5acd205 100644 --- a/federation/entities/diaspora/mappers.py +++ b/federation/entities/diaspora/mappers.py @@ -3,10 +3,10 @@ from datetime import datetime from lxml import etree -from federation.entities.base import Image, Relationship, Post, Reaction, Comment, Profile, Retraction, Follow +from federation.entities.base import Image, Relationship, Post, Reaction, Comment, Profile, Retraction, Follow, Share from federation.entities.diaspora.entities import ( DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction, - DiasporaRelayableMixin, DiasporaContact) + DiasporaRelayableMixin, DiasporaContact, DiasporaReshare) from federation.protocols.diaspora.signatures import get_element_child_info from federation.utils.diaspora import retrieve_and_parse_profile @@ -21,11 +21,12 @@ MAPPINGS = { "profile": DiasporaProfile, "retraction": DiasporaRetraction, "contact": DiasporaContact, + "reshare": DiasporaReshare, } TAGS = [ # Order is important. Any top level tags should be before possibly child tags - "status_message", "comment", "like", "request", "profile", "retraction", "photo", "contact", + "reshare", "status_message", "comment", "like", "request", "profile", "retraction", "photo", "contact", ] BOOLEAN_KEYS = ( @@ -44,6 +45,7 @@ INTEGER_KEYS = ( "width", ) + def xml_children_as_dict(node): """Turn the children of node into a dict, keyed by tag name. @@ -167,9 +169,9 @@ def transform_attributes(attrs, cls): transformed["raw_content"] = value elif key in ["diaspora_handle", "sender_handle", "author"]: transformed["handle"] = value - elif key in ["recipient_handle", "recipient"]: + elif key in ["recipient_handle", "recipient", "root_author", "root_diaspora_id"]: transformed["target_handle"] = value - elif key == "parent_guid": + elif key in ["parent_guid", "post_guid", "root_guid"]: transformed["target_guid"] = value elif key == "first_name": transformed["name"] = value @@ -203,8 +205,6 @@ def transform_attributes(attrs, cls): transformed["linked_type"] = "Post" elif key == "author_signature": transformed["signature"] = value - elif key == "post_guid": - transformed["target_guid"] = value elif key in BOOLEAN_KEYS: transformed[key] = True if value == "true" else False elif key in DATETIME_KEYS: @@ -239,7 +239,7 @@ def get_outbound_entity(entity, private_key): outbound = None cls = entity.__class__ if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction, - DiasporaContact]: + DiasporaContact, DiasporaReshare]: # Already fine outbound = entity elif cls == Post: @@ -259,6 +259,8 @@ def get_outbound_entity(entity, private_key): outbound = DiasporaProfile.from_base(entity) elif cls == Retraction: outbound = DiasporaRetraction.from_base(entity) + elif cls == Share: + outbound = DiasporaReshare.from_base(entity) if not outbound: raise ValueError("Don't know how to convert this base entity to Diaspora protocol entities.") if isinstance(outbound, DiasporaRelayableMixin) and not outbound.signature: diff --git a/federation/tests/entities/diaspora/test_entities.py b/federation/tests/entities/diaspora/test_entities.py index 2f8671d..dea34c1 100644 --- a/federation/tests/entities/diaspora/test_entities.py +++ b/federation/tests/entities/diaspora/test_entities.py @@ -6,9 +6,10 @@ from lxml import etree from federation.entities.base import Profile from federation.entities.diaspora.entities import ( DiasporaComment, DiasporaPost, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction, - DiasporaContact) + DiasporaContact, DiasporaReshare) from federation.entities.diaspora.mappers import message_to_objects from federation.exceptions import SignatureVerificationError +from federation.tests.factories.entities import ShareFactory from federation.tests.fixtures.keys import get_dummy_private_key from federation.tests.fixtures.payloads import DIASPORA_POST_COMMENT @@ -93,6 +94,21 @@ class TestEntitiesConvertToXML: b"truetrue" assert etree.tostring(result) == converted + def test_reshare_to_xml(self): + base_entity = ShareFactory() + entity = DiasporaReshare.from_base(base_entity) + result = entity.to_xml() + assert result.tag == "reshare" + result.find("created_at").text = "" # timestamp makes testing painful + converted = "%s%s%s" \ + "%s%s" \ + "%s%s%s" % ( + entity.handle, entity.guid, entity.target_handle, entity.target_guid, + entity.provider_display_name, "true" if entity.public else "false", entity.raw_content, + entity.entity_type, + ) + assert etree.tostring(result).decode("utf-8") == converted + class TestDiasporaProfileFillExtraAttributes: def test_raises_if_no_handle(self): diff --git a/federation/tests/entities/diaspora/test_mappers.py b/federation/tests/entities/diaspora/test_mappers.py index 9313596..1adeac0 100644 --- a/federation/tests/entities/diaspora/test_mappers.py +++ b/federation/tests/entities/diaspora/test_mappers.py @@ -6,17 +6,18 @@ import pytest from federation.entities.base import ( Comment, Post, Reaction, Relationship, Profile, Retraction, Image, - Follow) + Follow, Share) from federation.entities.diaspora.entities import ( DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, - DiasporaProfile, DiasporaRetraction, DiasporaContact) + DiasporaProfile, DiasporaRetraction, DiasporaContact, DiasporaReshare) from federation.entities.diaspora.mappers import ( message_to_objects, get_outbound_entity, check_sender_and_entity_handle_match) from federation.tests.fixtures.payloads import ( DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE, DIASPORA_REQUEST, DIASPORA_PROFILE, DIASPORA_POST_INVALID, DIASPORA_RETRACTION, DIASPORA_POST_WITH_PHOTOS, DIASPORA_POST_LEGACY_TIMESTAMP, DIASPORA_POST_LEGACY, DIASPORA_CONTACT, - DIASPORA_LEGACY_REQUEST_RETRACTION, DIASPORA_POST_WITH_PHOTOS_2, DIASPORA_PROFILE_EMPTY_TAGS) + DIASPORA_LEGACY_REQUEST_RETRACTION, DIASPORA_POST_WITH_PHOTOS_2, DIASPORA_PROFILE_EMPTY_TAGS, DIASPORA_RESHARE, + DIASPORA_RESHARE_WITH_EXTRA_PROPERTIES, DIASPORA_RESHARE_LEGACY) def mock_fill(attributes): @@ -189,6 +190,40 @@ class TestDiasporaEntityMappersReceive: assert entity.target_handle == "bob@example.org" assert entity.following is True + def test_message_to_objects_reshare(self): + entities = message_to_objects(DIASPORA_RESHARE, "alice@example.org") + assert len(entities) == 1 + entity = entities[0] + assert isinstance(entity, DiasporaReshare) + assert entity.handle == "alice@example.org" + assert entity.guid == "a0b53e5029f6013487753131731751e9" + assert entity.provider_display_name == "" + assert entity.target_handle == "bob@example.com" + assert entity.target_guid == "a0b53bc029f6013487753131731751e9" + assert entity.public is True + assert entity.entity_type == "Post" + + def test_message_to_objects_reshare_legacy(self): + entities = message_to_objects(DIASPORA_RESHARE_LEGACY, "alice@example.org") + assert len(entities) == 1 + entity = entities[0] + assert isinstance(entity, DiasporaReshare) + assert entity.handle == "alice@example.org" + assert entity.guid == "a0b53e5029f6013487753131731751e9" + assert entity.provider_display_name == "" + assert entity.target_handle == "bob@example.com" + assert entity.target_guid == "a0b53bc029f6013487753131731751e9" + assert entity.public is True + assert entity.entity_type == "Post" + + def test_message_to_objects_reshare_extra_properties(self): + entities = message_to_objects(DIASPORA_RESHARE_WITH_EXTRA_PROPERTIES, "alice@example.org") + assert len(entities) == 1 + entity = entities[0] + assert isinstance(entity, DiasporaReshare) + assert entity.raw_content == "Important note here" + assert entity.entity_type == "Comment" + @patch("federation.entities.diaspora.mappers.logger.error") def test_invalid_entity_logs_an_error(self, mock_logger): entities = message_to_objects(DIASPORA_POST_INVALID, "alice@alice.diaspora.example.org") @@ -242,6 +277,8 @@ class TestGetOutboundEntity: assert get_outbound_entity(entity, private_key) == entity entity = DiasporaContact() assert get_outbound_entity(entity, private_key) == entity + entity = DiasporaReshare() + assert get_outbound_entity(entity, private_key) == entity def test_post_is_converted_to_diasporapost(self, private_key): entity = Post() @@ -283,6 +320,10 @@ class TestGetOutboundEntity: entity = Follow() assert isinstance(get_outbound_entity(entity, private_key), DiasporaContact) + def test_share_is_converted_to_diasporareshare(self, private_key): + entity = Share() + assert isinstance(get_outbound_entity(entity, private_key), DiasporaReshare) + def test_signs_relayable_if_no_signature(self, private_key): entity = DiasporaComment() outbound = get_outbound_entity(entity, private_key) diff --git a/federation/tests/entities/test_base.py b/federation/tests/entities/test_base.py index 5eaa10a..89985e3 100644 --- a/federation/tests/entities/test_base.py +++ b/federation/tests/entities/test_base.py @@ -4,11 +4,11 @@ import pytest from federation.entities.base import ( BaseEntity, Relationship, Profile, RawContentMixin, GUIDMixin, HandleMixin, PublicMixin, Image, Retraction, - Follow) -from federation.tests.factories.entities import TaggedPostFactory, PostFactory + Follow, TargetHandleMixin) +from federation.tests.factories.entities import TaggedPostFactory, PostFactory, ShareFactory -class TestPostEntityTags(): +class TestPostEntityTags: def test_post_entity_returns_list_of_tags(self): post = TaggedPostFactory() assert post.tags == {"tagone", "tagtwo", "tagthree", "upper", "snakecase"} @@ -18,7 +18,7 @@ class TestPostEntityTags(): assert post.tags == set() -class TestBaseEntityCallsValidateMethods(): +class TestBaseEntityCallsValidateMethods: def test_entity_calls_attribute_validate_method(self): post = PostFactory() post.validate_location = Mock() @@ -48,28 +48,41 @@ class TestBaseEntityCallsValidateMethods(): post._validate_children() -class TestGUIDMixinValidate(): +class TestGUIDMixinValidate: def test_validate_guid_raises_on_low_length(self): guid = GUIDMixin(guid="x"*15) with pytest.raises(ValueError): guid.validate() + guid = GUIDMixin(guid="x" * 16) + guid.validate() -class TestHandleMixinValidate(): +class TestHandleMixinValidate: def test_validate_handle_raises_on_invalid_format(self): handle = HandleMixin(handle="foobar") with pytest.raises(ValueError): handle.validate() + handle = HandleMixin(handle="foobar@example.com") + handle.validate() -class TestPublicMixinValidate(): +class TestTargetHandleMixinValidate: + def test_validate_target_handle_raises_on_invalid_format(self): + handle = TargetHandleMixin(target_handle="foobar") + with pytest.raises(ValueError): + handle.validate() + handle = TargetHandleMixin(target_handle="foobar@example.com") + handle.validate() + + +class TestPublicMixinValidate: def test_validate_public_raises_on_low_length(self): public = PublicMixin(public="foobar") with pytest.raises(ValueError): public.validate() -class TestEntityRequiredAttributes(): +class TestEntityRequiredAttributes: def test_entity_checks_for_required_attributes(self): entity = BaseEntity() entity._required = ["foobar"] @@ -85,7 +98,7 @@ class TestEntityRequiredAttributes(): entity.validate() -class TestRelationshipEntity(): +class TestRelationshipEntity: def test_instance_creation(self): entity = Relationship(handle="bob@example.com", target_handle="alice@example.com", relationship="following") assert entity @@ -95,13 +108,8 @@ class TestRelationshipEntity(): entity = Relationship(handle="bob@example.com", target_handle="alice@example.com", relationship="hating") entity.validate() - def test_instance_creation_validates_target_handle_value(self): - with pytest.raises(ValueError): - entity = Relationship(handle="bob@example.com", target_handle="fefle.com", relationship="following") - entity.validate() - -class TestProfileEntity(): +class TestProfileEntity: def test_instance_creation(self): entity = Profile(handle="bob@example.com", raw_content="foobar") assert entity @@ -117,7 +125,7 @@ class TestProfileEntity(): entity.validate() -class TestImageEntity(): +class TestImageEntity: def test_instance_creation(self): entity = Image( guid="x"*16, handle="foo@example.com", public=False, remote_path="foobar", remote_name="barfoo" @@ -137,7 +145,7 @@ class TestImageEntity(): entity.validate() -class TestRetractionEntity(): +class TestRetractionEntity: def test_instance_creation(self): entity = Retraction( handle="foo@example.com", target_guid="x"*16, entity_type="Post" @@ -162,16 +170,15 @@ class TestRetractionEntity(): entity.validate() -class TestFollowEntity(): +class TestFollowEntity: def test_instance_creation(self): entity = Follow( handle="foo@example.com", target_handle="bar@example.org", following=True ) entity.validate() - def test_required_validates(self): - entity = Follow( - handle="foo@example.com", following=True - ) - with pytest.raises(ValueError): - entity.validate() + +class TestShareEntity: + def test_instance_creation(self): + entity = ShareFactory() + entity.validate() diff --git a/federation/tests/factories/entities.py b/federation/tests/factories/entities.py index 7e9fdf0..3f4ab26 100644 --- a/federation/tests/factories/entities.py +++ b/federation/tests/factories/entities.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- from random import shuffle import factory from factory import fuzzy -from federation.entities.base import Post, Profile +from federation.entities.base import Post, Profile, Share from federation.entities.diaspora.entities import DiasporaPost @@ -47,3 +46,15 @@ class ProfileFactory(GUIDMixinFactory, HandleMixinFactory, RawContentMixinFactor name = fuzzy.FuzzyText(length=30) public_key = fuzzy.FuzzyText(length=300) + + +class ShareFactory(GUIDMixinFactory, HandleMixinFactory): + class Meta: + model = Share + + target_guid = factory.Faker("uuid4") + entity_type = "Post" + raw_content = "" + public = factory.Faker("pybool") + provider_display_name = "" + target_handle = factory.Faker("safe_email") diff --git a/federation/tests/fixtures/payloads.py b/federation/tests/fixtures/payloads.py index 973247b..5b5ae85 100644 --- a/federation/tests/fixtures/payloads.py +++ b/federation/tests/fixtures/payloads.py @@ -231,3 +231,41 @@ DIASPORA_CONTACT = """ true """ + +DIASPORA_RESHARE = """ + + alice@example.org + a0b53e5029f6013487753131731751e9 + 2016-07-12T00:36:42Z + + bob@example.com + a0b53bc029f6013487753131731751e9 + true + +""" + +DIASPORA_RESHARE_LEGACY = """ + + alice@example.org + a0b53e5029f6013487753131731751e9 + 2016-07-12T00:36:42Z + + bob@example.com + a0b53bc029f6013487753131731751e9 + true + +""" + +DIASPORA_RESHARE_WITH_EXTRA_PROPERTIES = """ + + alice@example.org + a0b53e5029f6013487753131731751e9 + 2016-07-12T00:36:42Z + + bob@example.com + a0b53bc029f6013487753131731751e9 + true + Important note here + Comment + +"""