Merge pull request #100 from jaywink/share-entity

Added base entity Share
merge-requests/130/head
Jason Robinson 2017-08-22 12:21:54 +03:00 zatwierdzone przez GitHub
commit bcc779e006
11 zmienionych plików z 255 dodań i 86 usunięć

Wyświetl plik

@ -2,6 +2,11 @@
## [unreleased] ## [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 ### 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. * 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.

Wyświetl plik

@ -24,6 +24,7 @@ The feature set supported by this release is approximately the following:
* Retraction * Retraction
* StatusMessage * StatusMessage
* Contact * 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. 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.

Wyświetl plik

@ -17,6 +17,7 @@ Entity types are as follows below.
.. autoclass:: federation.entities.base.Reaction .. autoclass:: federation.entities.base.Reaction
.. autoclass:: federation.entities.base.Relationship .. autoclass:: federation.entities.base.Relationship
.. autoclass:: federation.entities.base.Retraction .. autoclass:: federation.entities.base.Retraction
.. autoclass:: federation.entities.base.Share
Protocol entities Protocol entities
................. .................

Wyświetl plik

@ -5,7 +5,7 @@ from dirty_validators.basic import Email
__all__ = ( __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") 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): class HandleMixin(BaseEntity):
handle = "" handle = ""
@ -178,7 +196,43 @@ class OptionalRawContentMixin(RawContentMixin):
self._required.remove("raw_content") 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.""" """Reflects a single image, possibly linked to another object."""
remote_path = "" remote_path = ""
remote_name = "" remote_name = ""
@ -192,32 +246,13 @@ class Image(GUIDMixin, HandleMixin, PublicMixin, OptionalRawContentMixin, Create
self._required += ["remote_path", "remote_name"] 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.""" """Reflects a post, status message, etc, which will be composed from the message or to the message."""
provider_display_name = ""
location = "" location = ""
_allowed_children = (Image,) _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): class Comment(RawContentMixin, GUIDMixin, ParticipationMixin, CreatedAtMixin, HandleMixin):
"""Represents a comment, linked to another object.""" """Represents a comment, linked to another object."""
participation = "comment" 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.""" """Represents a relationship between two handles."""
target_handle = ""
relationship = "" relationship = ""
_relationship_valid_values = ["sharing", "following", "ignoring", "blocking"] _relationship_valid_values = ["sharing", "following", "ignoring", "blocking"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._required += ["relationship", "target_handle"] self._required += ["relationship"]
def validate_target_handle(self):
validator = Email()
if not validator.is_valid(self.target_handle):
raise ValueError("Target handle is not valid")
def validate_relationship(self): def validate_relationship(self):
"""Ensure relationship is of a certain type.""" """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.""" """Represents a handle following or unfollowing another handle."""
target_handle = ""
following = True following = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._required += ["target_handle", "following"] self._required += ["following"]
def validate_target_handle(self):
validator = Email()
if not validator.is_valid(self.target_handle):
raise ValueError("Target handle is not valid")
class Profile(CreatedAtMixin, HandleMixin, OptionalRawContentMixin, PublicMixin, GUIDMixin): class Profile(CreatedAtMixin, HandleMixin, OptionalRawContentMixin, PublicMixin, GUIDMixin):
@ -313,15 +336,19 @@ class Profile(CreatedAtMixin, HandleMixin, OptionalRawContentMixin, PublicMixin,
raise ValueError("Email is not valid") raise ValueError("Email is not valid")
class Retraction(CreatedAtMixin, HandleMixin, TargetGUIDMixin): class Retraction(CreatedAtMixin, HandleMixin, TargetGUIDMixin, EntityTypeMixin):
"""Represents a retraction of content by author.""" """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): class Share(CreatedAtMixin, HandleMixin, TargetGUIDMixin, GUIDMixin, EntityTypeMixin, OptionalRawContentMixin,
"""Ensure type is some entity we know of.""" PublicMixin, ProviderDisplayNameMixin, TargetHandleMixin):
if self.entity_type not in __all__: """Represents a share of another entity.
raise ValueError("Entity type %s not recognized." % self.entity_type)
``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"

Wyświetl plik

@ -1,6 +1,7 @@
from lxml import etree 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.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes, add_element_to_doc
from federation.exceptions import SignatureVerificationError from federation.exceptions import SignatureVerificationError
from federation.protocols.diaspora.signatures import verify_relayable_signature, create_relayable_signature from federation.protocols.diaspora.signatures import verify_relayable_signature, create_relayable_signature
@ -211,3 +212,22 @@ class DiasporaRetraction(DiasporaEntityMixin, Retraction):
index = values.index(value) index = values.index(value)
return list(DiasporaRetraction.mapped.keys())[index] return list(DiasporaRetraction.mapped.keys())[index]
return value 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

Wyświetl plik

@ -3,10 +3,10 @@ from datetime import datetime
from lxml import etree 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 ( from federation.entities.diaspora.entities import (
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction, DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction,
DiasporaRelayableMixin, DiasporaContact) DiasporaRelayableMixin, DiasporaContact, DiasporaReshare)
from federation.protocols.diaspora.signatures import get_element_child_info from federation.protocols.diaspora.signatures import get_element_child_info
from federation.utils.diaspora import retrieve_and_parse_profile from federation.utils.diaspora import retrieve_and_parse_profile
@ -21,11 +21,12 @@ MAPPINGS = {
"profile": DiasporaProfile, "profile": DiasporaProfile,
"retraction": DiasporaRetraction, "retraction": DiasporaRetraction,
"contact": DiasporaContact, "contact": DiasporaContact,
"reshare": DiasporaReshare,
} }
TAGS = [ TAGS = [
# Order is important. Any top level tags should be before possibly child 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 = ( BOOLEAN_KEYS = (
@ -44,6 +45,7 @@ INTEGER_KEYS = (
"width", "width",
) )
def xml_children_as_dict(node): def xml_children_as_dict(node):
"""Turn the children of node <xml> into a dict, keyed by tag name. """Turn the children of node <xml> into a dict, keyed by tag name.
@ -167,9 +169,9 @@ def transform_attributes(attrs, cls):
transformed["raw_content"] = value transformed["raw_content"] = value
elif key in ["diaspora_handle", "sender_handle", "author"]: elif key in ["diaspora_handle", "sender_handle", "author"]:
transformed["handle"] = value transformed["handle"] = value
elif key in ["recipient_handle", "recipient"]: elif key in ["recipient_handle", "recipient", "root_author", "root_diaspora_id"]:
transformed["target_handle"] = value transformed["target_handle"] = value
elif key == "parent_guid": elif key in ["parent_guid", "post_guid", "root_guid"]:
transformed["target_guid"] = value transformed["target_guid"] = value
elif key == "first_name": elif key == "first_name":
transformed["name"] = value transformed["name"] = value
@ -203,8 +205,6 @@ def transform_attributes(attrs, cls):
transformed["linked_type"] = "Post" transformed["linked_type"] = "Post"
elif key == "author_signature": elif key == "author_signature":
transformed["signature"] = value transformed["signature"] = value
elif key == "post_guid":
transformed["target_guid"] = value
elif key in BOOLEAN_KEYS: elif key in BOOLEAN_KEYS:
transformed[key] = True if value == "true" else False transformed[key] = True if value == "true" else False
elif key in DATETIME_KEYS: elif key in DATETIME_KEYS:
@ -239,7 +239,7 @@ def get_outbound_entity(entity, private_key):
outbound = None outbound = None
cls = entity.__class__ cls = entity.__class__
if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction, if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction,
DiasporaContact]: DiasporaContact, DiasporaReshare]:
# Already fine # Already fine
outbound = entity outbound = entity
elif cls == Post: elif cls == Post:
@ -259,6 +259,8 @@ def get_outbound_entity(entity, private_key):
outbound = DiasporaProfile.from_base(entity) outbound = DiasporaProfile.from_base(entity)
elif cls == Retraction: elif cls == Retraction:
outbound = DiasporaRetraction.from_base(entity) outbound = DiasporaRetraction.from_base(entity)
elif cls == Share:
outbound = DiasporaReshare.from_base(entity)
if not outbound: if not outbound:
raise ValueError("Don't know how to convert this base entity to Diaspora protocol entities.") raise ValueError("Don't know how to convert this base entity to Diaspora protocol entities.")
if isinstance(outbound, DiasporaRelayableMixin) and not outbound.signature: if isinstance(outbound, DiasporaRelayableMixin) and not outbound.signature:

Wyświetl plik

@ -6,9 +6,10 @@ from lxml import etree
from federation.entities.base import Profile from federation.entities.base import Profile
from federation.entities.diaspora.entities import ( from federation.entities.diaspora.entities import (
DiasporaComment, DiasporaPost, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction, DiasporaComment, DiasporaPost, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction,
DiasporaContact) DiasporaContact, DiasporaReshare)
from federation.entities.diaspora.mappers import message_to_objects from federation.entities.diaspora.mappers import message_to_objects
from federation.exceptions import SignatureVerificationError 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.keys import get_dummy_private_key
from federation.tests.fixtures.payloads import DIASPORA_POST_COMMENT from federation.tests.fixtures.payloads import DIASPORA_POST_COMMENT
@ -93,6 +94,21 @@ class TestEntitiesConvertToXML:
b"<following>true</following><sharing>true</sharing></contact>" b"<following>true</following><sharing>true</sharing></contact>"
assert etree.tostring(result) == converted 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 = "<reshare><author>%s</author><guid>%s</guid><created_at></created_at><root_author>%s" \
"</root_author><root_guid>%s</root_guid><provider_display_name>%s</provider_display_name>" \
"<public>%s</public><raw_content>%s</raw_content><entity_type>%s</entity_type></reshare>" % (
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: class TestDiasporaProfileFillExtraAttributes:
def test_raises_if_no_handle(self): def test_raises_if_no_handle(self):

Wyświetl plik

@ -6,17 +6,18 @@ import pytest
from federation.entities.base import ( from federation.entities.base import (
Comment, Post, Reaction, Relationship, Profile, Retraction, Image, Comment, Post, Reaction, Relationship, Profile, Retraction, Image,
Follow) Follow, Share)
from federation.entities.diaspora.entities import ( from federation.entities.diaspora.entities import (
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest,
DiasporaProfile, DiasporaRetraction, DiasporaContact) DiasporaProfile, DiasporaRetraction, DiasporaContact, DiasporaReshare)
from federation.entities.diaspora.mappers import ( from federation.entities.diaspora.mappers import (
message_to_objects, get_outbound_entity, check_sender_and_entity_handle_match) message_to_objects, get_outbound_entity, check_sender_and_entity_handle_match)
from federation.tests.fixtures.payloads import ( from federation.tests.fixtures.payloads import (
DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE, DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE,
DIASPORA_REQUEST, DIASPORA_PROFILE, DIASPORA_POST_INVALID, DIASPORA_RETRACTION, DIASPORA_REQUEST, DIASPORA_PROFILE, DIASPORA_POST_INVALID, DIASPORA_RETRACTION,
DIASPORA_POST_WITH_PHOTOS, DIASPORA_POST_LEGACY_TIMESTAMP, DIASPORA_POST_LEGACY, DIASPORA_CONTACT, 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): def mock_fill(attributes):
@ -189,6 +190,40 @@ class TestDiasporaEntityMappersReceive:
assert entity.target_handle == "bob@example.org" assert entity.target_handle == "bob@example.org"
assert entity.following is True 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") @patch("federation.entities.diaspora.mappers.logger.error")
def test_invalid_entity_logs_an_error(self, mock_logger): def test_invalid_entity_logs_an_error(self, mock_logger):
entities = message_to_objects(DIASPORA_POST_INVALID, "alice@alice.diaspora.example.org") 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 assert get_outbound_entity(entity, private_key) == entity
entity = DiasporaContact() entity = DiasporaContact()
assert get_outbound_entity(entity, private_key) == entity 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): def test_post_is_converted_to_diasporapost(self, private_key):
entity = Post() entity = Post()
@ -283,6 +320,10 @@ class TestGetOutboundEntity:
entity = Follow() entity = Follow()
assert isinstance(get_outbound_entity(entity, private_key), DiasporaContact) 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): def test_signs_relayable_if_no_signature(self, private_key):
entity = DiasporaComment() entity = DiasporaComment()
outbound = get_outbound_entity(entity, private_key) outbound = get_outbound_entity(entity, private_key)

Wyświetl plik

@ -4,11 +4,11 @@ import pytest
from federation.entities.base import ( from federation.entities.base import (
BaseEntity, Relationship, Profile, RawContentMixin, GUIDMixin, HandleMixin, PublicMixin, Image, Retraction, BaseEntity, Relationship, Profile, RawContentMixin, GUIDMixin, HandleMixin, PublicMixin, Image, Retraction,
Follow) Follow, TargetHandleMixin)
from federation.tests.factories.entities import TaggedPostFactory, PostFactory from federation.tests.factories.entities import TaggedPostFactory, PostFactory, ShareFactory
class TestPostEntityTags(): class TestPostEntityTags:
def test_post_entity_returns_list_of_tags(self): def test_post_entity_returns_list_of_tags(self):
post = TaggedPostFactory() post = TaggedPostFactory()
assert post.tags == {"tagone", "tagtwo", "tagthree", "upper", "snakecase"} assert post.tags == {"tagone", "tagtwo", "tagthree", "upper", "snakecase"}
@ -18,7 +18,7 @@ class TestPostEntityTags():
assert post.tags == set() assert post.tags == set()
class TestBaseEntityCallsValidateMethods(): class TestBaseEntityCallsValidateMethods:
def test_entity_calls_attribute_validate_method(self): def test_entity_calls_attribute_validate_method(self):
post = PostFactory() post = PostFactory()
post.validate_location = Mock() post.validate_location = Mock()
@ -48,28 +48,41 @@ class TestBaseEntityCallsValidateMethods():
post._validate_children() post._validate_children()
class TestGUIDMixinValidate(): class TestGUIDMixinValidate:
def test_validate_guid_raises_on_low_length(self): def test_validate_guid_raises_on_low_length(self):
guid = GUIDMixin(guid="x"*15) guid = GUIDMixin(guid="x"*15)
with pytest.raises(ValueError): with pytest.raises(ValueError):
guid.validate() guid.validate()
guid = GUIDMixin(guid="x" * 16)
guid.validate()
class TestHandleMixinValidate(): class TestHandleMixinValidate:
def test_validate_handle_raises_on_invalid_format(self): def test_validate_handle_raises_on_invalid_format(self):
handle = HandleMixin(handle="foobar") handle = HandleMixin(handle="foobar")
with pytest.raises(ValueError): with pytest.raises(ValueError):
handle.validate() 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): def test_validate_public_raises_on_low_length(self):
public = PublicMixin(public="foobar") public = PublicMixin(public="foobar")
with pytest.raises(ValueError): with pytest.raises(ValueError):
public.validate() public.validate()
class TestEntityRequiredAttributes(): class TestEntityRequiredAttributes:
def test_entity_checks_for_required_attributes(self): def test_entity_checks_for_required_attributes(self):
entity = BaseEntity() entity = BaseEntity()
entity._required = ["foobar"] entity._required = ["foobar"]
@ -85,7 +98,7 @@ class TestEntityRequiredAttributes():
entity.validate() entity.validate()
class TestRelationshipEntity(): class TestRelationshipEntity:
def test_instance_creation(self): def test_instance_creation(self):
entity = Relationship(handle="bob@example.com", target_handle="alice@example.com", relationship="following") entity = Relationship(handle="bob@example.com", target_handle="alice@example.com", relationship="following")
assert entity assert entity
@ -95,13 +108,8 @@ class TestRelationshipEntity():
entity = Relationship(handle="bob@example.com", target_handle="alice@example.com", relationship="hating") entity = Relationship(handle="bob@example.com", target_handle="alice@example.com", relationship="hating")
entity.validate() 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): def test_instance_creation(self):
entity = Profile(handle="bob@example.com", raw_content="foobar") entity = Profile(handle="bob@example.com", raw_content="foobar")
assert entity assert entity
@ -117,7 +125,7 @@ class TestProfileEntity():
entity.validate() entity.validate()
class TestImageEntity(): class TestImageEntity:
def test_instance_creation(self): def test_instance_creation(self):
entity = Image( entity = Image(
guid="x"*16, handle="foo@example.com", public=False, remote_path="foobar", remote_name="barfoo" guid="x"*16, handle="foo@example.com", public=False, remote_path="foobar", remote_name="barfoo"
@ -137,7 +145,7 @@ class TestImageEntity():
entity.validate() entity.validate()
class TestRetractionEntity(): class TestRetractionEntity:
def test_instance_creation(self): def test_instance_creation(self):
entity = Retraction( entity = Retraction(
handle="foo@example.com", target_guid="x"*16, entity_type="Post" handle="foo@example.com", target_guid="x"*16, entity_type="Post"
@ -162,16 +170,15 @@ class TestRetractionEntity():
entity.validate() entity.validate()
class TestFollowEntity(): class TestFollowEntity:
def test_instance_creation(self): def test_instance_creation(self):
entity = Follow( entity = Follow(
handle="foo@example.com", target_handle="bar@example.org", following=True handle="foo@example.com", target_handle="bar@example.org", following=True
) )
entity.validate() entity.validate()
def test_required_validates(self):
entity = Follow( class TestShareEntity:
handle="foo@example.com", following=True def test_instance_creation(self):
) entity = ShareFactory()
with pytest.raises(ValueError): entity.validate()
entity.validate()

Wyświetl plik

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
from random import shuffle from random import shuffle
import factory import factory
from factory import fuzzy 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 from federation.entities.diaspora.entities import DiasporaPost
@ -47,3 +46,15 @@ class ProfileFactory(GUIDMixinFactory, HandleMixinFactory, RawContentMixinFactor
name = fuzzy.FuzzyText(length=30) name = fuzzy.FuzzyText(length=30)
public_key = fuzzy.FuzzyText(length=300) 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")

Wyświetl plik

@ -231,3 +231,41 @@ DIASPORA_CONTACT = """
<sharing>true</sharing> <sharing>true</sharing>
</contact> </contact>
""" """
DIASPORA_RESHARE = """
<reshare>
<author>alice@example.org</author>
<guid>a0b53e5029f6013487753131731751e9</guid>
<created_at>2016-07-12T00:36:42Z</created_at>
<provider_display_name/>
<root_author>bob@example.com</root_author>
<root_guid>a0b53bc029f6013487753131731751e9</root_guid>
<public>true</public>
</reshare>
"""
DIASPORA_RESHARE_LEGACY = """
<reshare>
<diaspora_handle>alice@example.org</diaspora_handle>
<guid>a0b53e5029f6013487753131731751e9</guid>
<created_at>2016-07-12T00:36:42Z</created_at>
<provider_display_name/>
<root_diaspora_id>bob@example.com</root_diaspora_id>
<root_guid>a0b53bc029f6013487753131731751e9</root_guid>
<public>true</public>
</reshare>
"""
DIASPORA_RESHARE_WITH_EXTRA_PROPERTIES = """
<reshare>
<author>alice@example.org</author>
<guid>a0b53e5029f6013487753131731751e9</guid>
<created_at>2016-07-12T00:36:42Z</created_at>
<provider_display_name/>
<root_author>bob@example.com</root_author>
<root_guid>a0b53bc029f6013487753131731751e9</root_guid>
<public>true</public>
<raw_content>Important note here</raw_content>
<entity_type>Comment</entity_type>
</reshare>
"""