from datetime import datetime from unittest.mock import patch, Mock, DEFAULT import json import pytest #from federation.entities.activitypub.entities import ( # models.Follow, models.Accept, models.Person, models.Note, models.Note, # models.Delete, models.Announce) import federation.entities.activitypub.models as models from federation.entities.activitypub.mappers import message_to_objects, get_outbound_entity from federation.entities.base import Accept, Follow, Profile, Post, Comment, Image, Share, Retraction from federation.tests.fixtures.payloads import ( ACTIVITYPUB_FOLLOW, ACTIVITYPUB_PROFILE, ACTIVITYPUB_PROFILE_INVALID, ACTIVITYPUB_UNDO_FOLLOW, ACTIVITYPUB_POST, ACTIVITYPUB_COMMENT, ACTIVITYPUB_RETRACTION, ACTIVITYPUB_SHARE, ACTIVITYPUB_RETRACTION_SHARE, ACTIVITYPUB_POST_IMAGES, ACTIVITYPUB_POST_WITH_SOURCE_MARKDOWN, ACTIVITYPUB_POST_WITH_TAGS, ACTIVITYPUB_POST_WITH_SOURCE_BBCODE, ACTIVITYPUB_POST_WITH_MENTIONS, ACTIVITYPUB_PROFILE_WITH_DIASPORA_GUID, ACTIVITYPUB_REMOTE_PROFILE, ACTIVITYPUB_COLLECTION) from federation.types import UserType, ReceiverVariant class TestActivitypubEntityMappersReceive: @patch.object(models.Follow, "post_receive", autospec=True) def test_message_to_objects__calls_post_receive_hook(self, mock_post_receive): message_to_objects(ACTIVITYPUB_FOLLOW, "https://example.com/actor") assert mock_post_receive.called def test_message_to_objects__announce(self): entities = message_to_objects(ACTIVITYPUB_SHARE, "https://mastodon.social/users/jaywink") assert len(entities) == 1 entity = entities[0] assert isinstance(entity, models.Announce) assert entity.actor_id == "https://mastodon.social/users/jaywink" assert entity.target_id == "https://mastodon.social/users/Gargron/statuses/102559779793316012" assert entity.id == "https://mastodon.social/users/jaywink/statuses/102560701449465612/activity" assert entity.public is True assert entity.entity_type == "Post" assert entity.raw_content == "" def test_message_to_objects__follow(self): entities = message_to_objects(ACTIVITYPUB_FOLLOW, "https://example.com/actor") assert len(entities) == 1 entity = entities[0] assert isinstance(entity, models.Follow) assert entity.actor_id == "https://example.com/actor" assert entity.target_id == "https://example.org/actor" assert entity.following is True def test_message_to_objects__unfollow(self): entities = message_to_objects(ACTIVITYPUB_UNDO_FOLLOW, "https://example.com/actor") assert len(entities) == 1 entity = entities[0] assert isinstance(entity, models.Follow) assert entity.actor_id == "https://example.com/actor" assert entity.target_id == "https://example.org/actor" assert entity.following is False @pytest.mark.skip def test_message_to_objects_mentions_are_extracted(self): entities = message_to_objects( DIASPORA_POST_SIMPLE_WITH_MENTION, "alice@alice.diaspora.example.org" ) assert len(entities) == 1 post = entities[0] assert post._mentions == {'jaywink@jasonrobinson.me'} def test_message_to_objects_simple_post(self): entities = message_to_objects(ACTIVITYPUB_POST, "https://diaspodon.fr/users/jaywink") assert len(entities) == 1 post = entities[0] assert isinstance(post, models.Note) assert isinstance(post, Post) assert post.raw_content == '

' \ '@jaywink boom

' assert post.rendered_content == '

' \ '@jaywink boom

' assert post.id == "https://diaspodon.fr/users/jaywink/statuses/102356911717767237" assert post.actor_id == "https://diaspodon.fr/users/jaywink" assert post.public is True assert post._media_type == "text/html" assert getattr(post, "target_id", None) is None def test_message_to_objects_simple_post__with_tags(self): entities = message_to_objects(ACTIVITYPUB_POST_WITH_TAGS, "https://diaspodon.fr/users/jaywink") assert len(entities) == 1 post = entities[0] assert isinstance(post, models.Note) assert isinstance(post, Post) assert post.raw_content == '

boom #test

' # TODO: fix this test @pytest.mark.skip def test_message_to_objects_simple_post__with_mentions(self): entities = message_to_objects(ACTIVITYPUB_POST_WITH_MENTIONS, "https://mastodon.social/users/jaywink") assert len(entities) == 1 post = entities[0] assert isinstance(post, models.Note) assert isinstance(post, Post) assert len(post._mentions) == 1 assert list(post._mentions)[0] == "https://dev3.jasonrobinson.me/u/jaywink/" def test_message_to_objects_simple_post__with_source__bbcode(self): entities = message_to_objects(ACTIVITYPUB_POST_WITH_SOURCE_BBCODE, "https://diaspodon.fr/users/jaywink") assert len(entities) == 1 post = entities[0] assert isinstance(post, models.Note) assert isinstance(post, Post) assert post.rendered_content == '

' \ '@jaywink boom

' assert post.raw_content == '

' \ '@jaywink boom

' def test_message_to_objects_simple_post__with_source__markdown(self): entities = message_to_objects(ACTIVITYPUB_POST_WITH_SOURCE_MARKDOWN, "https://diaspodon.fr/users/jaywink") assert len(entities) == 1 post = entities[0] assert isinstance(post, models.Note) assert isinstance(post, Post) assert post.rendered_content == '

@jaywink boom

' assert post.raw_content == "@jaywink boom" assert post.id == "https://diaspodon.fr/users/jaywink/statuses/102356911717767237" assert post.actor_id == "https://diaspodon.fr/users/jaywink" assert post.public is True assert post._media_type == "text/markdown" assert getattr(post, "target_id", None) is None def test_message_to_objects_post_with_photos(self): entities = message_to_objects(ACTIVITYPUB_POST_IMAGES, "https://mastodon.social/users/jaywink") assert len(entities) == 1 post = entities[0] assert isinstance(post, models.Note) # TODO: test video and audio attachment assert len(post._children) == 2 photo = post._children[0] assert isinstance(photo, Image) assert photo.url == "https://files.mastodon.social/media_attachments/files/017/642/079/original/" \ "f51b0aee0ee1f2e1.jpg" assert photo.name == "" assert photo.raw_content == "" assert photo.height == 0 assert photo.width == 0 assert photo.guid == "" assert photo.handle == "" def test_message_to_objects_comment(self): entities = message_to_objects(ACTIVITYPUB_COMMENT, "https://diaspodon.fr/users/jaywink") assert len(entities) == 1 comment = entities[0] assert isinstance(comment, models.Note) assert isinstance(comment, Comment) assert comment.raw_content == '

' \ '@jaywink boom

' assert comment.id == "https://diaspodon.fr/users/jaywink/statuses/102356911717767237" assert comment.actor_id == "https://diaspodon.fr/users/jaywink" assert comment.target_id == "https://dev.jasonrobinson.me/content/653bad70-41b3-42c9-89cb-c4ee587e68e4/" @pytest.mark.skip def test_message_to_objects_like(self, mock_validate): entities = message_to_objects( DIASPORA_POST_LIKE, "alice@alice.diaspora.example.org", sender_key_fetcher=Mock() ) assert len(entities) == 1 like = entities[0] assert isinstance(like, DiasporaLike) assert isinstance(like, Reaction) assert like.target_guid == "((parent_guidparent_guidparent_guidparent_guid))" assert like.guid == "((guidguidguidguidguidguid))" assert like.handle == "alice@alice.diaspora.example.org" assert like.participation == "reaction" assert like.reaction == "like" assert like.signature == "((signature))" assert like._xml_tags == [ "parent_type", "guid", "parent_guid", "positive", "author", ] mock_validate.assert_called_once_with() def test_message_to_objects_profile(self): entities = message_to_objects(ACTIVITYPUB_PROFILE, "http://example.com/1234") assert len(entities) == 1 profile = entities[0] assert profile.id == "https://diaspodon.fr/users/jaywink" assert profile.inboxes == { "private": "https://diaspodon.fr/users/jaywink/inbox", "public": "https://diaspodon.fr/inbox", } assert profile.handle == None assert profile.name == "Jason Robinson" assert profile.image_urls == { "large": "https://diaspodon.fr/system/accounts/avatars/000/033/155/original/pnc__picked_media_be51984c-4" "3e9-4266-9b9a-b74a61ae4167.jpg?1538505110", "medium": "https://diaspodon.fr/system/accounts/avatars/000/033/155/original/pnc__picked_media_be51984c-4" "3e9-4266-9b9a-b74a61ae4167.jpg?1538505110", "small": "https://diaspodon.fr/system/accounts/avatars/000/033/155/original/pnc__picked_media_be51984c-4" "3e9-4266-9b9a-b74a61ae4167.jpg?1538505110", } assert profile.gender == "" assert profile.raw_content == "

Temp account while implementing AP for Socialhome.

" \ "https://jasonrobinson." \ "me / https://socialhome.network / https://feneas.org

" assert profile.location == "" assert profile.public is True assert profile.nsfw is False assert profile.tag_list == [] def test_message_to_objects_profile__diaspora_guid_extracted(self): entities = message_to_objects( ACTIVITYPUB_PROFILE_WITH_DIASPORA_GUID, "https://friendica.feneas.org/profile/feneas", ) assert len(entities) == 1 profile = entities[0] assert profile.id == "https://friendica.feneas.org/profile/feneas" assert profile.guid == "76158462365bd347844d248732383358" #@patch('federation.tests.django.utils.get_profile', return_value=None) @patch('federation.entities.activitypub.models.get_profile', return_value=None) @patch('federation.utils.activitypub.fetch_document') def test_message_to_objects_receivers_are_saved(self, mock_fetch, mock_func): def side_effect(*args, **kwargs): payloads = {'https://diaspodon.fr/users/jaywink': json.dumps(ACTIVITYPUB_PROFILE), 'https://fosstodon.org/users/astdenis': json.dumps(ACTIVITYPUB_REMOTE_PROFILE), 'https://diaspodon.fr/users/jaywink/followers': json.dumps(ACTIVITYPUB_COLLECTION), } if args[0] in payloads.keys(): return payloads[args[0]], 200, None else: return None, None, 'Nothing here' mock_fetch.side_effect = side_effect # noinspection PyTypeChecker entities = message_to_objects( ACTIVITYPUB_POST, "https://diaspodon.fr/users/jaywink", ) entity = entities[0] assert set(entity._receivers) == { UserType( id='https://diaspodon.fr/users/jaywink', receiver_variant=ReceiverVariant.FOLLOWERS, ), UserType( id='https://fosstodon.org/users/astdenis', receiver_variant=ReceiverVariant.ACTOR, ) } def test_message_to_objects_retraction(self): entities = message_to_objects(ACTIVITYPUB_RETRACTION, "https://friendica.feneas.org/profile/jaywink") assert len(entities) == 1 entity = entities[0] assert isinstance(entity, Retraction) assert entity.actor_id == "https://friendica.feneas.org/profile/jaywink" assert entity.target_id == "https://friendica.feneas.org/objects/76158462-165d-3386-aa23-ba2090614385" assert entity.entity_type == "Object" def test_message_to_objects_retraction__share(self): entities = message_to_objects(ACTIVITYPUB_RETRACTION_SHARE, "https://mastodon.social/users/jaywink") assert len(entities) == 1 entity = entities[0] assert isinstance(entity, Retraction) assert entity.actor_id == "https://mastodon.social/users/jaywink" assert entity.target_id == "https://mastodon.social/users/jaywink/statuses/102571932479036987/activity" assert entity.entity_type == "Object" @pytest.mark.skip 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.activitypub.mappers.logger.error") def test_invalid_entity_logs_an_error(self, mock_logger): entities = message_to_objects(ACTIVITYPUB_PROFILE_INVALID, "http://example.com/1234") assert len(entities) == 0 assert mock_logger.called def test_adds_source_protocol_to_entity(self): entities = message_to_objects(ACTIVITYPUB_PROFILE, "http://example.com/1234") assert entities[0]._source_protocol == "activitypub" @pytest.mark.skip # since calamus turns the whole payload into objects, the source payload is not kept def test_source_object(self): entities = message_to_objects(ACTIVITYPUB_PROFILE, "http://example.com/1234") entity = entities[0] assert entity._source_object == ACTIVITYPUB_PROFILE @pytest.mark.skip def test_element_to_objects_calls_retrieve_remote_profile(self, mock_retrieve, mock_validate): message_to_objects(DIASPORA_POST_COMMENT, "alice@alice.diaspora.example.org") mock_retrieve.assert_called_once_with("alice@alice.diaspora.example.org") @pytest.mark.skip def test_element_to_objects_verifies_handles_are_the_same(self, mock_check): message_to_objects(DIASPORA_POST_SIMPLE, "bob@example.org") mock_check.assert_called_once_with("bob@example.org", "alice@alice.diaspora.example.org") @pytest.mark.skip def test_element_to_objects_returns_no_entity_if_handles_are_different(self): entities = message_to_objects(DIASPORA_POST_SIMPLE, "bob@example.org") assert not entities class TestGetOutboundEntity: def test_already_fine_entities_are_returned_as_is(self, private_key): entity = models.Accept() entity.validate = Mock() assert get_outbound_entity(entity, private_key) == entity entity = models.Follow() entity.validate = Mock() assert get_outbound_entity(entity, private_key) == entity entity = models.Person() entity.validate = Mock() assert get_outbound_entity(entity, private_key) == entity @patch.object(models.Accept, "validate", new=Mock()) def test_accept_is_converted_to_activitypubaccept(self, private_key): entity = Accept() assert isinstance(get_outbound_entity(entity, private_key), models.Accept) @patch.object(models.Follow, "validate", new=Mock()) def test_follow_is_converted_to_activitypubfollow(self, private_key): entity = Follow() assert isinstance(get_outbound_entity(entity, private_key), models.Follow) @patch.object(models.Person, "validate", new=Mock()) def test_profile_is_converted_to_activitypubprofile(self, private_key): entity = Profile() assert isinstance(get_outbound_entity(entity, private_key), models.Person) def test_entity_is_validated__fail(self, private_key): entity = Share( actor_id="https://localhost.local/foo", id="https://localhost.local/bar", created_at=datetime.now(), ) with pytest.raises(ValueError): get_outbound_entity(entity, private_key) def test_entity_is_validated__success(self, private_key): entity = Share( actor_id="https://localhost.local/foo", id="https://localhost.local/bar", created_at=datetime.now(), target_id="https://localhost.local/bar", ) get_outbound_entity(entity, private_key)