diff --git a/federation/entities/activitypub/mappers.py b/federation/entities/activitypub/mappers.py index 9b7cdc4..cec6db5 100644 --- a/federation/entities/activitypub/mappers.py +++ b/federation/entities/activitypub/mappers.py @@ -28,7 +28,13 @@ def get_outbound_entity(entity: BaseEntity, private_key): return entity outbound = None cls = entity.__class__ - if cls == Accept: + if cls in [ + models.Accept, models.Follow, models.Person, models.Note, + models.Delete, models.Tombstone, models.Announce, + ] and isinstance(entity, BaseEntity): + # Already fine + outbound = entity + elif cls == Accept: outbound = models.Accept.from_base(entity) elif cls == Follow: outbound = models.Follow.from_base(entity) diff --git a/federation/entities/activitypub/models.py b/federation/entities/activitypub/models.py index 7c3aa46..6df3ad8 100644 --- a/federation/entities/activitypub/models.py +++ b/federation/entities/activitypub/models.py @@ -206,7 +206,6 @@ class MixedField(fields.Nested): return super()._serialize(value, attr, obj, **kwargs) def _deserialize(self, value, attr, data, **kwargs): - print(attr, value, type(value)) # this is just so the ACTIVITYPUB_POST_OBJECT_IMAGES test payload passes if len(value) == 0: return value @@ -293,7 +292,6 @@ class Object(BaseEntity, metaclass=JsonLDAnnotation): def to_as2(self): obj = self.activity if isinstance(self.activity, Activity) else self - print('to_as2', obj, getattr(obj, 'tag_objects', None)) return jsonld.compact(obj.dump(), CONTEXT) @classmethod @@ -399,10 +397,8 @@ class Home(metaclass=JsonLDAnnotation): class NormalizedList(fields.List): def _deserialize(self,value, attr, data, **kwargs): - print('List', attr, value) value = normalize_value(value) ret = super()._deserialize(value,attr,data,**kwargs) - print('List after', ret) return ret @@ -555,9 +551,9 @@ class Emoji(Object): class Person(Object, base.Profile): id = fields.Id() inbox = IRI(ldp.inbox) - outbox = IRI(as2.outbox, dump_derived={'fmt': '{id}/outbox/', 'fields': ['id']}) - following = IRI(as2.following, dump_derived={'fmt': '{id}/following/', 'fields': ['id']}) - followers = IRI(as2.followers, dump_derived={'fmt': '{id}/followers/', 'fields': ['id']}) + outbox = IRI(as2.outbox) + following = IRI(as2.following) + followers = IRI(as2.followers) username = fields.String(as2.preferredUsername) endpoints = CompactedDict(as2.endpoints) shared_inbox = IRI(as2.sharedInbox) # misskey adds this @@ -570,7 +566,7 @@ class Person(Object, base.Profile): devices = IRI(toot.devices) public_key_dict = CompactedDict(sec.publicKey) guid = fields.String(diaspora.guid) - handle = fields.String(diaspora.handle) + handle = fields.String(diaspora.handle, default="") raw_content = fields.String(as2.summary, default="") # None fails in extract_mentions has_address = MixedField(vcard.hasAddress, nested='HomeSchema') has_instant_message = fields.List(vcard.hasInstantMessage, cls_or_instance=fields.String) @@ -580,6 +576,7 @@ class Person(Object, base.Profile): copied_to = IRI(toot.copiedTo) capabilities = CompactedDict(litepub.capabilities) suspended = fields.Boolean(toot.suspended) + public = True _inboxes = None _public_key = None _image_urls = None @@ -598,7 +595,21 @@ class Person(Object, base.Profile): self._allowed_children += (PropertyValue, IdentityProof) def to_as2(self): - self.id = self.id.rstrip('/') # TODO: sort out the trailing / business + #self.id = self.id.rstrip('/') # TODO: sort out the trailing / business + self.followers = f'{with_slash(self.id)}followers/' + self.following = f'{with_slash(self.id)}following/' + self.outbox = f'{with_slash(self.id)}outbox/' + + if hasattr(self, 'times'): + if self.times.get('updated',0) > self.times.get('created',0): + self.updated = self.times.get('updated') + if self.times.get('edited'): + self.activity = Update( + activity_id=f'{self.id}#profile-{uuid.uuid4()}', + actor_id=self.id, + created_at=self.times.get('updated'), + object_=self, + ) return super().to_as2() @property @@ -617,10 +628,11 @@ class Person(Object, base.Profile): @inboxes.setter def inboxes(self, value): - self._inboxes = value - if isinstance(value, dict): - self.inbox = value.get('private', None) - self.endpoints = {'sharedInbox': value.get('public', None)} + if value != {'private':None, 'public':None}: + self._inboxes = value + if isinstance(value, dict): + self.inbox = value.get('private', None) + self.endpoints = {'sharedInbox': value.get('public', None)} @property def public_key(self): @@ -634,8 +646,9 @@ class Person(Object, base.Profile): @public_key.setter def public_key(self, value): self._public_key = value - id_ = self.id.rstrip('/') - self.public_key_dict = {'id': id_+'#main-key', 'owner': id_, 'publicKeyPem': value} + #id_ = self.id.rstrip('/') + #self.public_key_dict = {'id': id_+'#main-key', 'owner': id_, 'publicKeyPem': value} + self.public_key_dict = {'id': self.id+'#main-key', 'owner': self.id, 'publicKeyPem': value} @property def image_urls(self): @@ -652,18 +665,15 @@ class Person(Object, base.Profile): @image_urls.setter def image_urls(self, value): - self._image_urls = value - if value.get('large'): - try: - profile_icon = base.Image(url=value.get('large')) - if profile_icon.media_type: - self.icon = [Image.from_base(profile_icon)] - except Exception as ex: - logger.warning("models.Person - failed to set profile icon: %s", ex) - - def to_base(self): - set_public(self) - return self + if value != {'large':'', 'medium':'', 'small':''}: + self._image_urls = value + if value.get('large'): + try: + profile_icon = base.Image(url=value.get('large')) + if profile_icon.media_type: + self.icon = [Image.from_base(profile_icon)] + except Exception as ex: + logger.warning("models.Person - failed to set profile icon: %s", ex) class Meta: rdf_type = as2.Person @@ -988,11 +998,10 @@ class Follow(Activity, base.Follow): def to_as2(self): if not self.following: self.activity = Undo( - activity_id = self.activity_id, + activity_id = self.activity_id if self.activity_id else f"{self.actor_id}#follow-{uuid.uuid4()}", actor_id = self.actor_id, object_ = self ) - self.activity_id = f"{self.actor_id}#follow-{uuid.uuid4()}" return super().to_as2() @@ -1067,10 +1076,8 @@ class Announce(Activity, base.Share): self.activity = self.activity( activity_id = self.activity_id if self.activity_id else f"{self.actor_id}#share-{uuid.uuid4()}", actor_id = self.actor_id, - created_at = self.created_at, object_ = self ) - self.id = f"{self.target_id}" return super().to_as2() @@ -1097,7 +1104,7 @@ class Tombstone(Object, base.Retraction): def to_as2(self): if not isinstance(self.activity, type): return None self.activity = self.activity( - activity_id = self.activity_id, + activity_id = self.activity_id if self.activity_id else f"{self.actor_id}#delete-{uuid.uuid4()}", actor_id = self.actor_id, created_at = self.created_at, object_ = self, @@ -1200,9 +1207,9 @@ def extract_receivers(entity): """ receivers = [] profile = None - # don't care about receivers for payloads without an actor_id - with rc.enabled(cache_name='fed_cache', backend=backend): - if getattr(entity, 'actor_id'): + # don't care about receivers for payloads without an actor_id + if getattr(entity, 'actor_id'): + with rc.enabled(cache_name='fed_cache', backend=backend): profile = retrieve_and_parse_profile(entity.actor_id) if not profile: return receivers @@ -1262,11 +1269,10 @@ def element_to_objects(element: Union[Dict, Object]) -> List: # json-ld handling with calamus # Skips unimplemented payloads - # TODO: remove unused code entity = model_to_objects(element) if not isinstance(element, Object) else element - #if entity: entity = entity.to_base() if entity and hasattr(entity, 'to_base'): entity = entity.to_base() + if isinstance(entity, BaseEntity): try: extract_and_validate(entity) except ValueError as ex: diff --git a/federation/outbound.py b/federation/outbound.py index 613d3fb..ec78374 100644 --- a/federation/outbound.py +++ b/federation/outbound.py @@ -358,6 +358,7 @@ def handle_send( # Do actual sending for payload in payloads: for url in payload["urls"]: + # Comment this out for testing #try: # pprint(json.loads(payload["payload"])) #except: diff --git a/federation/tests/conftest.py b/federation/tests/conftest.py index df45a23..d30095d 100644 --- a/federation/tests/conftest.py +++ b/federation/tests/conftest.py @@ -15,7 +15,7 @@ def disable_network_calls(monkeypatch): """Disable network calls.""" monkeypatch.setattr("requests.post", Mock()) - class MockResponse(str): + class MockGetResponse(str): status_code = 200 text = "" @@ -29,8 +29,17 @@ def disable_network_calls(monkeypatch): return saved_get(*args, **kwargs) return DEFAULT - monkeypatch.setattr("requests.get", Mock(return_value=MockResponse, side_effect=side_effect)) + monkeypatch.setattr("requests.get", Mock(return_value=MockGetResponse, side_effect=side_effect)) + class MockHeadResponse(dict): + status_code = 200 + headers = {'Content-Type':'image/jpeg'} + + @staticmethod + def raise_for_status(): + pass + + monkeypatch.setattr("requests.head", Mock(return_value=MockHeadResponse)) @pytest.fixture def private_key(): diff --git a/federation/tests/entities/activitypub/test_entities.py b/federation/tests/entities/activitypub/test_entities.py index 546c7ca..26e6ba3 100644 --- a/federation/tests/entities/activitypub/test_entities.py +++ b/federation/tests/entities/activitypub/test_entities.py @@ -376,10 +376,12 @@ class TestEntitiesConvertToAS2: 'id': 'http://127.0.0.1:8000/post/123456/#delete', 'actor': 'http://127.0.0.1:8000/profile/123456/', 'object': { + 'actor': 'http://127.0.0.1:8000/profile/123456/', 'id': 'http://127.0.0.1:8000/post/123456/activity', + 'object': 'http://127.0.0.1:8000/post/123456', 'type': 'Announce', + 'published': '2019-04-27T00:00:00', }, - 'published': '2019-04-27T00:00:00', } diff --git a/federation/tests/entities/activitypub/test_mappers.py b/federation/tests/entities/activitypub/test_mappers.py index 31305a1..3912d43 100644 --- a/federation/tests/entities/activitypub/test_mappers.py +++ b/federation/tests/entities/activitypub/test_mappers.py @@ -1,6 +1,7 @@ from datetime import datetime -from unittest.mock import patch, Mock +from unittest.mock import patch, Mock, DEFAULT +import json import pytest #from federation.entities.activitypub.entities import ( @@ -8,12 +9,13 @@ import pytest # 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 +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_POST_WITH_SOURCE_BBCODE, ACTIVITYPUB_POST_WITH_MENTIONS, ACTIVITYPUB_PROFILE_WITH_DIASPORA_GUID, + ACTIVITYPUB_REMOTE_PROFILE, ACTIVITYPUB_COLLECTION) from federation.types import UserType, ReceiverVariant @@ -217,7 +219,20 @@ class TestActivitypubEntityMappersReceive: assert profile.id == "https://friendica.feneas.org/profile/feneas" assert profile.guid == "76158462365bd347844d248732383358" - def test_message_to_objects_receivers_are_saved(self): + @patch('federation.utils.activitypub.fetch_document') + def test_message_to_objects_receivers_are_saved(self, mock_fetch): + 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 DEFAULT + + mock_fetch.side_effect = side_effect + # noinspection PyTypeChecker entities = message_to_objects( ACTIVITYPUB_POST, @@ -230,7 +245,7 @@ class TestActivitypubEntityMappersReceive: id='https://diaspodon.fr/users/jaywink', receiver_variant=ReceiverVariant.FOLLOWERS, ), UserType( - id='https://dev.jasonrobinson.me/p/d4574854-a5d7-42be-bfac-f70c16fcaa97/', + id='https://fosstodon.org/users/astdenis', receiver_variant=ReceiverVariant.ACTOR, ) } @@ -239,7 +254,7 @@ class TestActivitypubEntityMappersReceive: entities = message_to_objects(ACTIVITYPUB_RETRACTION, "https://friendica.feneas.org/profile/jaywink") assert len(entities) == 1 entity = entities[0] - assert isinstance(entity, models.Delete) + 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" @@ -248,7 +263,7 @@ class TestActivitypubEntityMappersReceive: entities = message_to_objects(ACTIVITYPUB_RETRACTION_SHARE, "https://mastodon.social/users/jaywink") assert len(entities) == 1 entity = entities[0] - assert isinstance(entity, models.Announce) + 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" diff --git a/federation/tests/entities/diaspora/test_utils.py b/federation/tests/entities/diaspora/test_utils.py index 3d71f64..1e8f7ae 100644 --- a/federation/tests/entities/diaspora/test_utils.py +++ b/federation/tests/entities/diaspora/test_utils.py @@ -19,7 +19,7 @@ class TestGetBaseAttributes: assert set(attrs) == { "created_at", "location", "provider_display_name", "public", "raw_content", "signature", "base_url", "actor_id", "id", "handle", "guid", "activity", "activity_id", - "url", "mxid", + "url", "mxid", "times", } entity = Profile() attrs = get_base_attributes(entity).keys() @@ -27,7 +27,7 @@ class TestGetBaseAttributes: "created_at", "name", "email", "gender", "raw_content", "location", "public", "nsfw", "public_key", "image_urls", "tag_list", "signature", "url", "atom_url", "base_url", "id", "actor_id", "handle", "handle", "guid", "activity", "activity_id", "username", - "inboxes", "mxid", + "inboxes", "mxid", "times", } diff --git a/federation/tests/fixtures/entities.py b/federation/tests/fixtures/entities.py index 810356a..9528109 100644 --- a/federation/tests/fixtures/entities.py +++ b/federation/tests/fixtures/entities.py @@ -180,7 +180,7 @@ https://jasonrobinson.me/media/uploads/2019/07/16/daa24d89-cedf-4fc7-bad8-74a902 def activitypubprofile(mock_fetch): with freeze_time("2022-09-06"): return models.Person( - id="https://example.com/bob/", raw_content="foobar", name="Bob Bobertson", public=True, + id="https://example.com/bob", raw_content="foobar", name="Bob Bobertson", public=True, tag_list=["socialfederation", "federation"], image_urls={ "large": "urllarge", "medium": "urlmedium", "small": "urlsmall" }, inboxes={ @@ -195,7 +195,7 @@ def activitypubprofile(mock_fetch): def activitypubprofile_diaspora_guid(mock_fetch): with freeze_time("2022-09-06"): return models.Person( - id="https://example.com/bob/", raw_content="foobar", name="Bob Bobertson", public=True, + id="https://example.com/bob", raw_content="foobar", name="Bob Bobertson", public=True, tag_list=["socialfederation", "federation"], image_urls={ "large": "urllarge", "medium": "urlmedium", "small": "urlsmall" }, inboxes={ @@ -222,7 +222,8 @@ def activitypubretraction(): def activitypubretraction_announce(): with freeze_time("2019-04-27"): obj = Retraction( - target_id="http://127.0.0.1:8000/post/123456/activity", + id="http://127.0.0.1:8000/post/123456/activity", + target_id="http://127.0.0.1:8000/post/123456", activity_id="http://127.0.0.1:8000/post/123456/#delete", actor_id="http://127.0.0.1:8000/profile/123456/", entity_type="Share", diff --git a/federation/tests/fixtures/payloads/activitypub.py b/federation/tests/fixtures/payloads/activitypub.py index 7a1d9d3..7c807c3 100644 --- a/federation/tests/fixtures/payloads/activitypub.py +++ b/federation/tests/fixtures/payloads/activitypub.py @@ -128,6 +128,85 @@ ACTIVITYPUB_PROFILE = { } } +ACTIVITYPUB_REMOTE_PROFILE = { + "@context": ["https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {"Curve25519Key": "toot:Curve25519Key", + "Device": "toot:Device", + "Ed25519Key": "toot:Ed25519Key", + "Ed25519Signature": "toot:Ed25519Signature", + "EncryptedMessage": "toot:EncryptedMessage", + "PropertyValue": "schema:PropertyValue", + "alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"}, + "cipherText": "toot:cipherText", + "claim": {"@id": "toot:claim", "@type": "@id"}, + "deviceId": "toot:deviceId", + "devices": {"@id": "toot:devices", "@type": "@id"}, + "discoverable": "toot:discoverable", + "featured": {"@id": "toot:featured", "@type": "@id"}, + "featuredTags": {"@id": "toot:featuredTags", "@type": "@id"}, + "fingerprintKey": {"@id": "toot:fingerprintKey", "@type": "@id"}, + "focalPoint": {"@container": "@list", "@id": "toot:focalPoint"}, + "identityKey": {"@id": "toot:identityKey", "@type": "@id"}, + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "messageFranking": "toot:messageFranking", + "messageType": "toot:messageType", + "movedTo": {"@id": "as:movedTo", "@type": "@id"}, + "publicKeyBase64": "toot:publicKeyBase64", + "schema": "http://schema.org#", + "suspended": "toot:suspended", + "toot": "http://joinmastodon.org/ns#", + "value": "schema:value"}], + "attachment": [{"name": "OS", "type": "PropertyValue", "value": "Manjaro"}, + {"name": "Self Hosting", + "type": "PropertyValue", + "value": "Matrix HS, Nextcloud"}], + "devices": "https://fosstodon.org/users/astdenis/collections/devices", + "discoverable": True, + "endpoints": {"sharedInbox": "https://fosstodon.org/inbox"}, + "featured": "https://fosstodon.org/users/astdenis/collections/featured", + "featuredTags": "https://fosstodon.org/users/astdenis/collections/tags", + "followers": "https://fosstodon.org/users/astdenis/followers", + "following": "https://fosstodon.org/users/astdenis/following", + "icon": {"mediaType": "image/jpeg", + "type": "Image", + "url": "https://cdn.fosstodon.org/accounts/avatars/000/252/976/original/09b7067cde009950.jpg"}, + "id": "https://fosstodon.org/users/astdenis", + "image": {"mediaType": "image/jpeg", + "type": "Image", + "url": "https://cdn.fosstodon.org/accounts/headers/000/252/976/original/555a1ac1819e4e7f.jpg"}, + "inbox": "https://fosstodon.org/users/astdenis/inbox", + "manuallyApprovesFollowers": False, + "name": "Alain", + "outbox": "https://fosstodon.org/users/astdenis/outbox", + "preferredUsername": "astdenis", + "publicKey": {"id": "https://fosstodon.org/users/astdenis#main-key", + "owner": "https://fosstodon.org/users/astdenis", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuaoIq/b+aUNqGAJNYF76\n" + "WY8tk49Vb1udyb7X+oseBXYtOwCDGfbZMalnFfqur1bAzogkKzuyjCeA3BfVs6R3\n" + "Cll897kUveMNHVc24pslhOx5ZzwpNT8e4q97dNaeHWLSLH5H+4JJGbeoD23G5SaY\n" + "9ZKt5iP+qRUlO/kSsUPwqsX9i2qSEqzwDiSvyRYhvvx4O588cUaaY9rAliLgtc/P\n" + "4EID3v6Edexe2QosUaghwGbb8zZWsYq0O4Umn2QMN4LzmQ0FjP+lq1TFX8FkGDZH\n" + "lnP+AMEQMyuac9Yb12t4RwvdsAIk4MXhAKvutMJm/X1GVQIyrsLEmvAO3rgk8dMr\n" + "6QIDAQAB\n" + "-----END PUBLIC KEY-----\n"}, + "published": "2020-07-25T00:00:00Z", + "summary": "
Linux user and sysadmin since 1994, retired from the HPC field " + "since 2019.
Utilisateur et sysadmin Linux depuis 1994, " + "retraité du domaine du CHP depuis 2019.
", + "tag": [], + "type": "Person", + "url": "https://fosstodon.org/@astdenis" +} + +ACTIVITYPUB_COLLECTION = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://diaspodon.fr/users/jaywink/followers", + "totalItems": 231, + "type": "OrderedCollection" +} + ACTIVITYPUB_PROFILE_INVALID = { "@context": [ "https://www.w3.org/ns/activitystreams", @@ -313,7 +392,7 @@ ACTIVITYPUB_POST = { 'published': '2019-06-29T21:08:45Z', 'to': 'https://www.w3.org/ns/activitystreams#Public', 'cc': ['https://diaspodon.fr/users/jaywink/followers', - 'https://dev.jasonrobinson.me/p/d4574854-a5d7-42be-bfac-f70c16fcaa97/'], + 'https://fosstodon.org/users/astdenis'], 'object': {'id': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237', 'type': 'Note', 'summary': None, @@ -323,7 +402,7 @@ ACTIVITYPUB_POST = { 'attributedTo': 'https://diaspodon.fr/users/jaywink', 'to': 'https://www.w3.org/ns/activitystreams#Public', 'cc': ['https://diaspodon.fr/users/jaywink/followers', - 'https://dev.jasonrobinson.me/p/d4574854-a5d7-42be-bfac-f70c16fcaa97/'], + 'https://fosstodon.org/users/astdenis'], 'sensitive': False, 'atomUri': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237', 'inReplyToAtomUri': None, diff --git a/federation/tests/utils/test_activitypub.py b/federation/tests/utils/test_activitypub.py index a4c58ce..cbde46a 100644 --- a/federation/tests/utils/test_activitypub.py +++ b/federation/tests/utils/test_activitypub.py @@ -50,35 +50,39 @@ class TestRetrieveAndParseDocument: "https://example.com/foobar", extra_headers={'accept': 'application/activity+json'}, auth=auth, ) + @patch("federation.entities.activitypub.models.extract_receivers", return_value=[]) @patch("federation.utils.activitypub.fetch_document", autospec=True, return_value=( json.dumps(ACTIVITYPUB_FOLLOW), None, None), ) @patch.object(Follow, "post_receive") - def test_returns_entity_for_valid_document__follow(self, mock_post_receive, mock_fetch): + def test_returns_entity_for_valid_document__follow(self, mock_post_receive, mock_fetch, mock_recv): entity = retrieve_and_parse_document("https://example.com/foobar") assert isinstance(entity, Follow) + @patch("federation.entities.activitypub.models.extract_receivers", return_value=[]) @patch("federation.utils.activitypub.fetch_document", autospec=True, return_value=( json.dumps(ACTIVITYPUB_POST_OBJECT), None, None), ) - def test_returns_entity_for_valid_document__post__without_activity(self, mock_fetch): + def test_returns_entity_for_valid_document__post__without_activity(self, mock_fetch, mock_recv): entity = retrieve_and_parse_document("https://example.com/foobar") assert isinstance(entity, Note) + @patch("federation.entities.activitypub.models.extract_receivers", return_value=[]) @patch("federation.utils.activitypub.fetch_document", autospec=True, return_value=( json.dumps(ACTIVITYPUB_POST_OBJECT_IMAGES), None, None), ) - def test_returns_entity_for_valid_document__post__without_activity__with_images(self, mock_fetch): + def test_returns_entity_for_valid_document__post__without_activity__with_images(self, mock_fetch, mock_recv): entity = retrieve_and_parse_document("https://example.com/foobar") assert isinstance(entity, Note) assert len(entity._children) == 1 assert entity._children[0].url == "https://files.mastodon.social/media_attachments/files/017/792/237/original" \ "/foobar.jpg" + @patch("federation.entities.activitypub.models.extract_receivers", return_value=[]) @patch("federation.utils.activitypub.fetch_document", autospec=True, return_value=( json.dumps(ACTIVITYPUB_POST), None, None), ) - def test_returns_entity_for_valid_document__post__wrapped_in_activity(self, mock_fetch): + def test_returns_entity_for_valid_document__post__wrapped_in_activity(self, mock_fetch, mock_recv): entity = retrieve_and_parse_document("https://example.com/foobar") assert isinstance(entity, Note)