Fix tests and other issues tests revealed

jsonld-outbound
Alain St-Denis 2022-09-08 11:40:37 +00:00
rodzic ce00a6045d
commit 63b591db46
7 zmienionych plików z 216 dodań i 220 usunięć

Wyświetl plik

@ -43,6 +43,7 @@ def get_outbound_entity(entity: BaseEntity, private_key):
elif entity.entity_type == 'Share':
outbound = models.Announce.from_base(entity)
outbound.activity = models.Undo
outbound._required.remove('id')
elif entity.entity_type == 'Profile':
outbound = models.Delete.from_base(entity)
elif cls == Share:

Wyświetl plik

@ -5,6 +5,7 @@ import logging
from typing import List, Callable, Dict, Union, Optional
import uuid
import bleach
from calamus import fields
from calamus.schema import JsonLDAnnotation, JsonLDSchema, JsonLDSchemaOpts
from calamus.utils import normalize_value
@ -15,8 +16,9 @@ from pyld import jsonld
import requests_cache as rc
from federation.entities.activitypub.constants import CONTEXT, NAMESPACE_PUBLIC
from federation.entities.mixins import BaseEntity
from federation.entities.mixins import BaseEntity, RawContentMixin
from federation.entities.utils import get_base_attributes
from federation.outbound import handle_send
from federation.types import UserType, ReceiverVariant
from federation.utils.activitypub import retrieve_and_parse_document, retrieve_and_parse_profile
from federation.utils.text import with_slash, validate_handle
@ -199,15 +201,20 @@ class MixedField(fields.Nested):
isinstance(value, list) and len(value) > 0 and isinstance(value[0], str)):
return self.iri._serialize(value, attr, obj, **kwargs)
else:
value = value[0] if isinstance(value, list) and len(value) > 0 else value
value = value[0] if isinstance(value, list) and len(value) == 1 else value
if isinstance(value, list) and len(value) == 0: value = None
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
if isinstance(value, list) and value[0] == {}: return {}
if isinstance(value, list):
if value[0] == {}: return {}
else:
value = [value]
ret = []
for item in value:
@ -258,13 +265,13 @@ class Object(BaseEntity, metaclass=JsonLDAnnotation):
also_known_as = IRI(as2.alsoKnownAs)
icon = MixedField(as2.icon, nested='ImageSchema')
image = MixedField(as2.image, nested='ImageSchema')
tag_list = MixedField(as2.tag, nested=['HashtagSchema','MentionSchema','PropertyValueSchema','EmojiSchema'])
tag_objects = MixedField(as2.tag, nested=['HashtagSchema','MentionSchema','PropertyValueSchema','EmojiSchema'], many=True)
attachment = fields.Nested(as2.attachment, nested=['ImageSchema', 'AudioSchema', 'DocumentSchema','PropertyValueSchema','IdentityProofSchema'], many=True)
content_map = LanguageMap(as2.content) # language maps are not implemented in calamus
context = IRI(as2.context)
guid = fields.String(diaspora.guid)
name = fields.String(as2.name)
generator = MixedField(as2.generator, nested='ServiceSchema')
generator = MixedField(as2.generator, nested=['ApplicationSchema','ServiceSchema'])
created_at = fields.DateTime(as2.published, add_value_types=True)
replies = MixedField(as2.replies, nested=['CollectionSchema','OrderedCollectionSchema'])
signature = MixedField(sec.signature, nested = 'SignatureSchema')
@ -273,7 +280,6 @@ class Object(BaseEntity, metaclass=JsonLDAnnotation):
to = IRI(as2.to)
cc = IRI(as2.cc)
media_type = fields.String(as2.mediaType)
sensitive = fields.Boolean(as2.sensitive)
source = CompactedDict(as2.source)
# The following properties are defined by some platforms, but are not implemented yet
@ -287,6 +293,7 @@ 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
@ -379,7 +386,7 @@ class Object(BaseEntity, metaclass=JsonLDAnnotation):
@post_dump
def sanitize(self, data, **kwargs):
return {k: v for k,v in data.items() if v}
return {k: v for k,v in data.items() if v or isinstance(v, bool)}
class Home(metaclass=JsonLDAnnotation):
country_name = fields.String(fields.IRIReference("http://www.w3.org/2006/vcard/ns#","country-name"))
@ -392,8 +399,11 @@ class Home(metaclass=JsonLDAnnotation):
class NormalizedList(fields.List):
def _deserialize(self,value, attr, data, **kwargs):
print('List', attr, value)
value = normalize_value(value)
return super()._deserialize(value,attr,data,**kwargs)
ret = super()._deserialize(value,attr,data,**kwargs)
print('List after', ret)
return ret
class Collection(Object):
@ -435,7 +445,7 @@ class OrderedCollectionPage(OrderedCollection, CollectionPage):
# AP defines [Ii]mage and [Aa]udio objects/properties, but only a Video object
# seen with Peertube payloads only so far
class Document(Object):
inline = fields.Boolean(pyfed.inlineImage)
inline = fields.Boolean(pyfed.inlineImage, default=False)
height = Integer(as2.height, flavor=xsd.nonNegativeInteger, add_value_types=True)
width = Integer(as2.width, flavor=xsd.nonNegativeInteger, add_value_types=True)
blurhash = fields.String(toot.blurhash)
@ -490,9 +500,18 @@ class Link(metaclass=JsonLDAnnotation):
# Not implemented yet
#preview : variable type?
def __init__(self, *args, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
class Meta:
rdf_type = as2.Link
@post_dump
def noid(self, data, **kwargs):
if data['@id'].startswith('_:'): data.pop('@id')
return data
@post_load
def make_instance(self, data, **kwargs):
data.pop('@id', None)
@ -536,9 +555,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, 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']})
username = fields.String(as2.preferredUsername)
endpoints = CompactedDict(as2.endpoints)
shared_inbox = IRI(as2.sharedInbox) # misskey adds this
@ -578,6 +597,10 @@ class Person(Object, base.Profile):
super().__init__(*args, **kwargs)
self._allowed_children += (PropertyValue, IdentityProof)
def to_as2(self):
self.id = self.id.rstrip('/') # TODO: sort out the trailing / business
return super().to_as2()
@property
def inboxes(self):
if self._inboxes: return self._inboxes
@ -611,7 +634,8 @@ class Person(Object, base.Profile):
@public_key.setter
def public_key(self, value):
self._public_key = value
self.public_key_dict = {'id': self.id+'#main-key', 'owner': self.id, 'publicKeyPem': value}
id_ = self.id.rstrip('/')
self.public_key_dict = {'id': id_+'#main-key', 'owner': id_, 'publicKeyPem': value}
@property
def image_urls(self):
@ -667,28 +691,38 @@ class Service(Person):
rdf_type = as2.Service
class Application(Person):
class Meta:
rdf_type = as2.Application
# The to_base method is used to handle cases where an AP object type matches multiple
# classes depending on the existence/value of specific propertie(s) or
# when the same class is used both as an object or an activity or
# when a property can't be directly deserialized from the payload.
# calamus Nested field can't handle using the same model
# or the same type in multiple schemas
class Note(Object):
class Note(Object, RawContentMixin):
id = fields.Id()
actor_id = IRI(as2.attributedTo)
target_id = IRI(as2.inReplyTo)
conversation = fields.RawJsonLD(ostatus.conversation)
in_reply_to_atom_uri = IRI(ostatus.inReplyToAtomUri)
sensitive = fields.Boolean(as2.sensitive, default=False)
summary = fields.String(as2.summary)
url = IRI(as2.url)
_raw_content = None
__children = []
def __init__(self, *args, **kwargs):
self.tag_objects = [] # mutable objects...
super().__init__(*args, **kwargs)
self.extract_mentions()
self._allowed_children += (base.Image, base.Audio, base.Video)
def to_as2(self):
self.sensitive = 'nsfw' in self.tags
if self.activity_id:
activity = Create
if hasattr(self, 'times'):
@ -727,6 +761,9 @@ class Note(Object):
) for image in self.embedded_images
]
self.extract_mentions()
self.content_map = {'orig': self.rendered_content}
self.add_object_mentions()
self.add_object_tags()
def post_receive(self) -> None:
"""
@ -752,11 +789,10 @@ class Note(Object):
skip_tags=["code", "pre"],
)
def add_object_tags(self) -> List[Dict]:
def add_object_tags(self) -> None:
"""
Populate tags to the object.tag list.
"""
tags = []
try:
config = get_configuration()
except ImportError:
@ -767,14 +803,24 @@ class Note(Object):
else:
tags_path = None
for tag in self.tags:
_tag = {
'type': 'Hashtag',
'name': f'#{tag}',
}
_tag = Hashtag(name=f'#{tag}')
if tags_path:
_tag["href"] = tags_path.replace(":tag:", tag)
tags.append(_tag)
return tags
_tag.href = tags_path.replace(":tag:", tag)
self.tag_objects.append(_tag)
def add_object_mentions(self) -> None:
"""
Populate mentions to the object.tag list.
"""
if len(self._mentions):
mentions = list(self._mentions)
mentions.sort()
for mention in mentions:
if mention.startswith("http"):
self.tag_objects.append(Mention(href=mention, name=mention))
elif validate_handle(mention):
# Look up via WebFinger
self.tag_objects.append(Mention(href=mention, name=mention)) # TODO need to implement fetch via webfinger for AP handles first
def extract_mentions(self):
"""
@ -782,10 +828,9 @@ class Note(Object):
"""
super().extract_mentions()
if getattr(self, 'tag_list', None):
from federation.entities.activitypub.models import Mention # Circulars
tag_list = self.tag_list if isinstance(self.tag_list, list) else [self.tag_list]
for tag in tag_list:
if getattr(self, 'tag_objects', None):
tag_objects = self.tag_objects if isinstance(self.tag_objects, list) else [self.tag_objects]
for tag in tag_objects:
if isinstance(tag, Mention):
self._mentions.add(tag.href)
@ -817,7 +862,6 @@ class Note(Object):
self._raw_content = value
if self._media_type == 'text/markdown':
self.source = {'content': value, 'mediaType': self._media_type}
self.content_map = {'orig': self.rendered_content}
@property
def _children(self):
@ -1011,20 +1055,22 @@ class Follow(Activity, base.Follow):
class Meta:
rdf_type = as2.Follow
exclude = ('created_at',)
class Announce(Activity, base.Share):
id = fields.Id(default="unused") # needed for validation with undo
id = fields.Id()
target_id = IRI(as2.object)
def to_as2(self):
if isinstance(self.activity, type):
self.activity = self.activity(
activity_id = f"{self.actor_id}#share-{uuid.uuid4()}",
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.target_id
object_ = self
)
self.id = f"{self.target_id}"
return super().to_as2()
@ -1051,7 +1097,7 @@ class Tombstone(Object, base.Retraction):
def to_as2(self):
if not isinstance(self.activity, type): return None
self.activity = self.activity(
activity_id = f"{self.actor_id}#delete-{uuid.uuid4()}",
activity_id = self.activity_id,
actor_id = self.actor_id,
created_at = self.created_at,
object_ = self,
@ -1096,6 +1142,7 @@ class Accept(Create, base.Accept):
class Meta:
rdf_type = as2.Accept
exclude = ('created_at',)
class Delete(Create, base.Retraction):
@ -1117,6 +1164,7 @@ class Update(Create):
class Undo(Create):
class Meta:
rdf_type = as2.Undo
exclude = ('created_at',)
class View(Create):
@ -1241,7 +1289,7 @@ def model_to_objects(payload):
try:
entity = model.schema().load(payload)
except (KeyError, jsonld.JsonLdError, exceptions.ValidationError) as exc : # Just give up for now. This must be made robust
logger.error(f"Error parsing jsonld payload ({exc})")
logger.error(f"Error parsing jsonld payload ({exc})")
return None
if isinstance(getattr(entity, 'object_', None), Object):

Wyświetl plik

@ -2,6 +2,7 @@ import copy
import importlib
import json
import logging
from pprint import pprint
import traceback
from typing import List, Dict, Union
@ -357,8 +358,11 @@ def handle_send(
# Do actual sending
for payload in payloads:
for url in payload["urls"]:
print(url, payload["payload"])
continue
#try:
# pprint(json.loads(payload["payload"]))
#except:
# pass
#continue
try:
# TODO send_document and fetch_document need to handle rate limits
send_document(

Wyświetl plik

@ -4,9 +4,8 @@ from pprint import pprint
# noinspection PyPackageRequirements
from Crypto.PublicKey.RSA import RsaKey
from federation.entities.activitypub.constants import (
CONTEXTS_DEFAULT, CONTEXT_MANUALLY_APPROVES_FOLLOWERS, CONTEXT_LD_SIGNATURES, CONTEXT_DIASPORA)
from federation.entities.activitypub.entities import ActivitypubAccept
from federation.entities.activitypub.constants import CONTEXT
from federation.entities.activitypub.models import Accept
from federation.tests.fixtures.keys import PUBKEY
from federation.types import UserType
@ -15,12 +14,11 @@ class TestEntitiesConvertToAS2:
def test_accept_to_as2(self, activitypubaccept):
result = activitypubaccept.to_as2()
assert result == {
"@context": CONTEXTS_DEFAULT,
"@context": CONTEXT,
"id": "https://localhost/accept",
"type": "Accept",
"actor": "https://localhost/profile",
"object": {
"@context": CONTEXTS_DEFAULT,
"id": "https://localhost/follow",
"type": "Follow",
"actor": "https://localhost/profile",
@ -28,10 +26,10 @@ class TestEntitiesConvertToAS2:
},
}
def test_accounce_to_as2(self, activitypubannounce):
def test_announce_to_as2(self, activitypubannounce):
result = activitypubannounce.to_as2()
assert result == {
"@context": CONTEXTS_DEFAULT,
"@context": CONTEXT,
"id": "http://127.0.0.1:8000/post/123456/#create",
"type": "Announce",
"actor": "http://127.0.0.1:8000/profile/123456/",
@ -40,15 +38,10 @@ class TestEntitiesConvertToAS2:
}
def test_comment_to_as2(self, activitypubcomment):
activitypubcomment.pre_send()
result = activitypubcomment.to_as2()
assert result == {
'@context': [
'https://www.w3.org/ns/activitystreams',
{"pyfed": "https://docs.jasonrobinson.me/ns/python-federation#"},
{'Hashtag': 'as:Hashtag'},
'https://w3id.org/security/v1',
{'sensitive': 'as:sensitive'},
],
'@context': CONTEXT,
'type': 'Create',
'id': 'http://127.0.0.1:8000/post/123456/#create',
'actor': 'http://127.0.0.1:8000/profile/123456/',
@ -60,9 +53,6 @@ class TestEntitiesConvertToAS2:
'published': '2019-04-27T00:00:00',
'inReplyTo': 'http://127.0.0.1:8000/post/012345/',
'sensitive': False,
'summary': None,
'tag': [],
'url': '',
'source': {
'content': 'raw_content',
'mediaType': 'text/markdown',
@ -73,15 +63,10 @@ class TestEntitiesConvertToAS2:
def test_comment_to_as2__url_in_raw_content(self, activitypubcomment):
activitypubcomment.raw_content = 'raw_content http://example.com'
activitypubcomment.pre_send()
result = activitypubcomment.to_as2()
assert result == {
'@context': [
'https://www.w3.org/ns/activitystreams',
{"pyfed": "https://docs.jasonrobinson.me/ns/python-federation#"},
{'Hashtag': 'as:Hashtag'},
'https://w3id.org/security/v1',
{'sensitive': 'as:sensitive'},
],
'@context': CONTEXT,
'type': 'Create',
'id': 'http://127.0.0.1:8000/post/123456/#create',
'actor': 'http://127.0.0.1:8000/profile/123456/',
@ -94,9 +79,6 @@ class TestEntitiesConvertToAS2:
'published': '2019-04-27T00:00:00',
'inReplyTo': 'http://127.0.0.1:8000/post/012345/',
'sensitive': False,
'summary': None,
'tag': [],
'url': '',
'source': {
'content': 'raw_content http://example.com',
'mediaType': 'text/markdown',
@ -108,7 +90,7 @@ class TestEntitiesConvertToAS2:
def test_follow_to_as2(self, activitypubfollow):
result = activitypubfollow.to_as2()
assert result == {
"@context": CONTEXTS_DEFAULT,
"@context": CONTEXT,
"id": "https://localhost/follow",
"type": "Follow",
"actor": "https://localhost/profile",
@ -119,7 +101,7 @@ class TestEntitiesConvertToAS2:
result = activitypubundofollow.to_as2()
result["object"]["id"] = "https://localhost/follow" # Real object will have a random UUID postfix here
assert result == {
"@context": CONTEXTS_DEFAULT,
"@context": CONTEXT,
"id": "https://localhost/undo",
"type": "Undo",
"actor": "https://localhost/profile",
@ -132,15 +114,10 @@ class TestEntitiesConvertToAS2:
}
def test_post_to_as2(self, activitypubpost):
activitypubpost.pre_send()
result = activitypubpost.to_as2()
assert result == {
'@context': [
'https://www.w3.org/ns/activitystreams',
{"pyfed": "https://docs.jasonrobinson.me/ns/python-federation#"},
{'Hashtag': 'as:Hashtag'},
'https://w3id.org/security/v1',
{'sensitive': 'as:sensitive'},
],
'@context': CONTEXT,
'type': 'Create',
'id': 'http://127.0.0.1:8000/post/123456/#create',
'actor': 'http://127.0.0.1:8000/profile/123456/',
@ -150,11 +127,7 @@ class TestEntitiesConvertToAS2:
'attributedTo': 'http://127.0.0.1:8000/profile/123456/',
'content': '<h1>raw_content</h1>',
'published': '2019-04-27T00:00:00',
'inReplyTo': None,
'sensitive': False,
'summary': None,
'tag': [],
'url': '',
'source': {
'content': '# raw_content',
'mediaType': 'text/markdown',
@ -167,13 +140,7 @@ class TestEntitiesConvertToAS2:
activitypubpost_mentions.pre_send()
result = activitypubpost_mentions.to_as2()
assert result == {
'@context': [
'https://www.w3.org/ns/activitystreams',
{"pyfed": "https://docs.jasonrobinson.me/ns/python-federation#"},
{'Hashtag': 'as:Hashtag'},
'https://w3id.org/security/v1',
{'sensitive': 'as:sensitive'},
],
'@context': CONTEXT,
'type': 'Create',
'id': 'http://127.0.0.1:8000/post/123456/#create',
'actor': 'http://127.0.0.1:8000/profile/123456/',
@ -185,9 +152,7 @@ class TestEntitiesConvertToAS2:
'href="http://localhost.local/someone" rel="nofollow" target="_blank">'
'<span>Bob Bobértson</span></a></p>',
'published': '2019-04-27T00:00:00',
'inReplyTo': None,
'sensitive': False,
'summary': None,
'tag': [
{
"type": "Mention",
@ -210,7 +175,6 @@ class TestEntitiesConvertToAS2:
"name": "someone@localhost.local",
},
],
'url': '',
'source': {
'content': '# raw_content\n\n@{someone@localhost.local} @{http://localhost.local/someone}',
'mediaType': 'text/markdown',
@ -220,15 +184,10 @@ class TestEntitiesConvertToAS2:
}
def test_post_to_as2__with_tags(self, activitypubpost_tags):
activitypubpost_tags.pre_send()
result = activitypubpost_tags.to_as2()
assert result == {
'@context': [
'https://www.w3.org/ns/activitystreams',
{"pyfed": "https://docs.jasonrobinson.me/ns/python-federation#"},
{'Hashtag': 'as:Hashtag'},
'https://w3id.org/security/v1',
{'sensitive': 'as:sensitive'},
],
'@context': CONTEXT,
'type': 'Create',
'id': 'http://127.0.0.1:8000/post/123456/#create',
'actor': 'http://127.0.0.1:8000/profile/123456/',
@ -246,9 +205,7 @@ class TestEntitiesConvertToAS2:
'noreferrer nofollow" '
'target="_blank">#<span>barfoo</span></a></p>',
'published': '2019-04-27T00:00:00',
'inReplyTo': None,
'sensitive': False,
'summary': None,
'tag': [
{
"type": "Hashtag",
@ -261,7 +218,6 @@ class TestEntitiesConvertToAS2:
"name": "#foobar",
},
],
'url': '',
'source': {
'content': '# raw_content\n#foobar\n#barfoo',
'mediaType': 'text/markdown',
@ -271,15 +227,10 @@ class TestEntitiesConvertToAS2:
}
def test_post_to_as2__with_images(self, activitypubpost_images):
activitypubpost_images.pre_send()
result = activitypubpost_images.to_as2()
assert result == {
'@context': [
'https://www.w3.org/ns/activitystreams',
{"pyfed": "https://docs.jasonrobinson.me/ns/python-federation#"},
{'Hashtag': 'as:Hashtag'},
'https://w3id.org/security/v1',
{'sensitive': 'as:sensitive'},
],
'@context': CONTEXT,
'type': 'Create',
'id': 'http://127.0.0.1:8000/post/123456/#create',
'actor': 'http://127.0.0.1:8000/profile/123456/',
@ -289,16 +240,11 @@ class TestEntitiesConvertToAS2:
'attributedTo': 'http://127.0.0.1:8000/profile/123456/',
'content': '<p>raw_content</p>',
'published': '2019-04-27T00:00:00',
'inReplyTo': None,
'sensitive': False,
'summary': None,
'tag': [],
'url': '',
'attachment': [
{
'type': 'Image',
'mediaType': 'image/jpeg',
'name': '',
'url': 'foobar',
'pyfed:inlineImage': False,
},
@ -319,16 +265,10 @@ class TestEntitiesConvertToAS2:
}
def test_post_to_as2__with_diaspora_guid(self, activitypubpost_diaspora_guid):
activitypubpost_diaspora_guid.pre_send()
result = activitypubpost_diaspora_guid.to_as2()
assert result == {
'@context': [
'https://www.w3.org/ns/activitystreams',
{"pyfed": "https://docs.jasonrobinson.me/ns/python-federation#"},
{'Hashtag': 'as:Hashtag'},
'https://w3id.org/security/v1',
{'sensitive': 'as:sensitive'},
{'diaspora': 'https://diasporafoundation.org/ns/'},
],
'@context': CONTEXT,
'type': 'Create',
'id': 'http://127.0.0.1:8000/post/123456/#create',
'actor': 'http://127.0.0.1:8000/profile/123456/',
@ -339,11 +279,7 @@ class TestEntitiesConvertToAS2:
'attributedTo': 'http://127.0.0.1:8000/profile/123456/',
'content': '<p>raw_content</p>',
'published': '2019-04-27T00:00:00',
'inReplyTo': None,
'sensitive': False,
'summary': None,
'tag': [],
'url': '',
'source': {
'content': 'raw_content',
'mediaType': 'text/markdown',
@ -353,14 +289,10 @@ class TestEntitiesConvertToAS2:
}
# noinspection PyUnusedLocal
@patch("federation.entities.base.fetch_content_type", return_value="image/jpeg")
def test_profile_to_as2(self, mock_fetch, activitypubprofile):
def test_profile_to_as2(self, activitypubprofile):
result = activitypubprofile.to_as2()
assert result == {
"@context": CONTEXTS_DEFAULT + [
CONTEXT_LD_SIGNATURES,
CONTEXT_MANUALLY_APPROVES_FOLLOWERS,
],
"@context": CONTEXT,
"endpoints": {
"sharedInbox": "https://example.com/public",
},
@ -376,6 +308,7 @@ class TestEntitiesConvertToAS2:
"owner": "https://example.com/bob",
"publicKeyPem": PUBKEY,
},
'published': '2022-09-06T00:00:00',
"type": "Person",
"url": "https://example.com/bob-bobertson",
"summary": "foobar",
@ -383,21 +316,15 @@ class TestEntitiesConvertToAS2:
"type": "Image",
"url": "urllarge",
"mediaType": "image/jpeg",
"name": "",
"pyfed:inlineImage": False,
}
}
# noinspection PyUnusedLocal
@patch("federation.entities.base.fetch_content_type", return_value="image/jpeg")
def test_profile_to_as2__with_diaspora_guid(self, mock_fetch, activitypubprofile_diaspora_guid):
def test_profile_to_as2__with_diaspora_guid(self, activitypubprofile_diaspora_guid):
result = activitypubprofile_diaspora_guid.to_as2()
assert result == {
"@context": CONTEXTS_DEFAULT + [
CONTEXT_LD_SIGNATURES,
CONTEXT_MANUALLY_APPROVES_FOLLOWERS,
CONTEXT_DIASPORA,
],
"@context": CONTEXT,
"endpoints": {
"sharedInbox": "https://example.com/public",
},
@ -415,6 +342,7 @@ class TestEntitiesConvertToAS2:
"owner": "https://example.com/bob",
"publicKeyPem": PUBKEY,
},
'published': '2022-09-06T00:00:00',
"type": "Person",
"url": "https://example.com/bob-bobertson",
"summary": "foobar",
@ -422,7 +350,6 @@ class TestEntitiesConvertToAS2:
"type": "Image",
"url": "urllarge",
"mediaType": "image/jpeg",
"name": "",
"pyfed:inlineImage": False,
}
}
@ -430,10 +357,7 @@ class TestEntitiesConvertToAS2:
def test_retraction_to_as2(self, activitypubretraction):
result = activitypubretraction.to_as2()
assert result == {
'@context': [
'https://www.w3.org/ns/activitystreams',
{"pyfed": "https://docs.jasonrobinson.me/ns/python-federation#"},
],
'@context': CONTEXT,
'type': 'Delete',
'id': 'http://127.0.0.1:8000/post/123456/#delete',
'actor': 'http://127.0.0.1:8000/profile/123456/',
@ -447,10 +371,7 @@ class TestEntitiesConvertToAS2:
def test_retraction_to_as2__announce(self, activitypubretraction_announce):
result = activitypubretraction_announce.to_as2()
assert result == {
'@context': [
'https://www.w3.org/ns/activitystreams',
{"pyfed": "https://docs.jasonrobinson.me/ns/python-federation#"},
],
'@context': CONTEXT,
'type': 'Undo',
'id': 'http://127.0.0.1:8000/post/123456/#delete',
'actor': 'http://127.0.0.1:8000/profile/123456/',
@ -463,15 +384,15 @@ class TestEntitiesConvertToAS2:
class TestEntitiesPostReceive:
@patch("federation.utils.activitypub.retrieve_and_parse_profile", autospec=True)
@patch("federation.entities.activitypub.entities.handle_send", autospec=True)
@patch("federation.entities.activitypub.models.retrieve_and_parse_profile", autospec=True)
@patch("federation.entities.activitypub.models.handle_send", autospec=True)
def test_follow_post_receive__sends_correct_accept_back(
self, mock_send, mock_retrieve, activitypubfollow, profile
):
mock_retrieve.return_value = profile
activitypubfollow.post_receive()
args, kwargs = mock_send.call_args_list[0]
assert isinstance(args[0], ActivitypubAccept)
assert isinstance(args[0], Accept)
assert args[0].activity_id.startswith("https://example.com/profile#accept-")
assert args[0].actor_id == "https://example.com/profile"
assert args[0].target_id == "https://localhost/follow"
@ -485,13 +406,13 @@ class TestEntitiesPostReceive:
"public": False,
}]
@patch("federation.entities.activitypub.entities.bleach.linkify", autospec=True)
@patch("federation.entities.activitypub.models.bleach.linkify", autospec=True)
def test_post_post_receive__linkifies_if_not_markdown(self, mock_linkify, activitypubpost):
activitypubpost._media_type = 'text/html'
activitypubpost.post_receive()
mock_linkify.assert_called_once()
@patch("federation.entities.activitypub.entities.bleach.linkify", autospec=True)
@patch("federation.entities.activitypub.models.bleach.linkify", autospec=True)
def test_post_post_receive__skips_linkify_if_markdown(self, mock_linkify, activitypubpost):
activitypubpost.post_receive()
mock_linkify.assert_not_called()

Wyświetl plik

@ -3,9 +3,10 @@ from unittest.mock import patch, Mock
import pytest
from federation.entities.activitypub.entities import (
ActivitypubFollow, ActivitypubAccept, ActivitypubProfile, ActivitypubPost, ActivitypubComment,
ActivitypubRetraction, ActivitypubShare)
#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
from federation.tests.fixtures.payloads import (
@ -17,7 +18,7 @@ from federation.types import UserType, ReceiverVariant
class TestActivitypubEntityMappersReceive:
@patch.object(ActivitypubFollow, "post_receive", autospec=True)
@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
@ -26,7 +27,7 @@ class TestActivitypubEntityMappersReceive:
entities = message_to_objects(ACTIVITYPUB_SHARE, "https://mastodon.social/users/jaywink")
assert len(entities) == 1
entity = entities[0]
assert isinstance(entity, ActivitypubShare)
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"
@ -38,7 +39,7 @@ class TestActivitypubEntityMappersReceive:
entities = message_to_objects(ACTIVITYPUB_FOLLOW, "https://example.com/actor")
assert len(entities) == 1
entity = entities[0]
assert isinstance(entity, ActivitypubFollow)
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
@ -47,7 +48,7 @@ class TestActivitypubEntityMappersReceive:
entities = message_to_objects(ACTIVITYPUB_UNDO_FOLLOW, "https://example.com/actor")
assert len(entities) == 1
entity = entities[0]
assert isinstance(entity, ActivitypubFollow)
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
@ -65,7 +66,7 @@ class TestActivitypubEntityMappersReceive:
entities = message_to_objects(ACTIVITYPUB_POST, "https://diaspodon.fr/users/jaywink")
assert len(entities) == 1
post = entities[0]
assert isinstance(post, ActivitypubPost)
assert isinstance(post, models.Note)
assert isinstance(post, Post)
assert post.raw_content == '<p><span class="h-card"><a class="u-url mention" ' \
'href="https://dev.jasonrobinson.me/u/jaywink/">' \
@ -82,7 +83,7 @@ class TestActivitypubEntityMappersReceive:
entities = message_to_objects(ACTIVITYPUB_POST_WITH_TAGS, "https://diaspodon.fr/users/jaywink")
assert len(entities) == 1
post = entities[0]
assert isinstance(post, ActivitypubPost)
assert isinstance(post, models.Note)
assert isinstance(post, Post)
assert post.raw_content == '<p>boom #test</p>'
@ -90,7 +91,7 @@ class TestActivitypubEntityMappersReceive:
entities = message_to_objects(ACTIVITYPUB_POST_WITH_MENTIONS, "https://mastodon.social/users/jaywink")
assert len(entities) == 1
post = entities[0]
assert isinstance(post, ActivitypubPost)
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/"
@ -99,7 +100,7 @@ class TestActivitypubEntityMappersReceive:
entities = message_to_objects(ACTIVITYPUB_POST_WITH_SOURCE_BBCODE, "https://diaspodon.fr/users/jaywink")
assert len(entities) == 1
post = entities[0]
assert isinstance(post, ActivitypubPost)
assert isinstance(post, models.Note)
assert isinstance(post, Post)
assert post.rendered_content == '<p><span class="h-card"><a class="u-url mention" href="https://dev.jasonrobinson.me/u/jaywink/">' \
'@<span>jaywink</span></a></span> boom</p>'
@ -111,7 +112,7 @@ class TestActivitypubEntityMappersReceive:
entities = message_to_objects(ACTIVITYPUB_POST_WITH_SOURCE_MARKDOWN, "https://diaspodon.fr/users/jaywink")
assert len(entities) == 1
post = entities[0]
assert isinstance(post, ActivitypubPost)
assert isinstance(post, models.Note)
assert isinstance(post, Post)
assert post.rendered_content == '<p><span class="h-card"><a href="https://dev.jasonrobinson.me/u/jaywink/" ' \
'class="u-url mention">@<span>jaywink</span></a></span> boom</p>'
@ -126,7 +127,7 @@ class TestActivitypubEntityMappersReceive:
entities = message_to_objects(ACTIVITYPUB_POST_IMAGES, "https://mastodon.social/users/jaywink")
assert len(entities) == 1
post = entities[0]
assert isinstance(post, ActivitypubPost)
assert isinstance(post, models.Note)
# TODO: test video and audio attachment
assert len(post._children) == 2
photo = post._children[0]
@ -144,7 +145,7 @@ class TestActivitypubEntityMappersReceive:
entities = message_to_objects(ACTIVITYPUB_COMMENT, "https://diaspodon.fr/users/jaywink")
assert len(entities) == 1
comment = entities[0]
assert isinstance(comment, ActivitypubComment)
assert isinstance(comment, models.Note)
assert isinstance(comment, Comment)
assert comment.raw_content == '<p><span class="h-card"><a class="u-url mention" ' \
'href="https://dev.jasonrobinson.me/u/jaywink/">' \
@ -238,7 +239,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, ActivitypubRetraction)
assert isinstance(entity, models.Delete)
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"
@ -247,7 +248,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, ActivitypubRetraction)
assert isinstance(entity, models.Announce)
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"
@ -296,30 +297,30 @@ class TestActivitypubEntityMappersReceive:
class TestGetOutboundEntity:
def test_already_fine_entities_are_returned_as_is(self, private_key):
entity = ActivitypubAccept()
entity = models.Accept()
entity.validate = Mock()
assert get_outbound_entity(entity, private_key) == entity
entity = ActivitypubFollow()
entity = models.Follow()
entity.validate = Mock()
assert get_outbound_entity(entity, private_key) == entity
entity = ActivitypubProfile()
entity = models.Person()
entity.validate = Mock()
assert get_outbound_entity(entity, private_key) == entity
@patch.object(ActivitypubAccept, "validate", new=Mock())
@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), ActivitypubAccept)
assert isinstance(get_outbound_entity(entity, private_key), models.Accept)
@patch.object(ActivitypubFollow, "validate", new=Mock())
@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), ActivitypubFollow)
assert isinstance(get_outbound_entity(entity, private_key), models.Follow)
@patch.object(ActivitypubProfile, "validate", new=Mock())
@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), ActivitypubProfile)
assert isinstance(get_outbound_entity(entity, private_key), models.Person)
def test_entity_is_validated__fail(self, private_key):
entity = Share(

Wyświetl plik

@ -1,11 +1,12 @@
import pytest
# noinspection PyPackageRequirements
from freezegun import freeze_time
from unittest.mock import patch
from federation.entities.activitypub.entities import (
ActivitypubPost, ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubComment,
ActivitypubRetraction, ActivitypubShare, ActivitypubImage)
from federation.entities.base import Profile, Post
from federation.entities.activitypub.mappers import get_outbound_entity
import federation.entities.activitypub.models as models
from federation.entities.activitypub.models import make_content_class
from federation.entities.base import Profile, Post, Comment, Retraction
from federation.entities.diaspora.entities import (
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction,
DiasporaContact, DiasporaReshare,
@ -18,8 +19,8 @@ from federation.tests.fixtures.payloads import DIASPORA_PUBLIC_PAYLOAD
@pytest.fixture
def activitypubannounce():
with freeze_time("2019-08-05"):
return ActivitypubShare(
activity_id="http://127.0.0.1:8000/post/123456/#create",
return models.Announce(
id="http://127.0.0.1:8000/post/123456/#create",
actor_id="http://127.0.0.1:8000/profile/123456/",
target_id="http://127.0.0.1:8000/post/012345/",
)
@ -28,7 +29,7 @@ def activitypubannounce():
@pytest.fixture
def activitypubcomment():
with freeze_time("2019-04-27"):
return ActivitypubComment(
obj = make_content_class(Comment)(
raw_content="raw_content",
public=True,
provider_display_name="Socialhome",
@ -37,11 +38,13 @@ def activitypubcomment():
actor_id=f"http://127.0.0.1:8000/profile/123456/",
target_id="http://127.0.0.1:8000/post/012345/",
)
obj.times={'edited':False, 'created':obj.created_at}
return obj
@pytest.fixture
def activitypubfollow():
return ActivitypubFollow(
return models.Follow(
activity_id="https://localhost/follow",
actor_id="https://localhost/profile",
target_id="https://example.com/profile",
@ -50,18 +53,18 @@ def activitypubfollow():
@pytest.fixture
def activitypubaccept(activitypubfollow):
return ActivitypubAccept(
return models.Accept(
activity_id="https://localhost/accept",
actor_id="https://localhost/profile",
target_id="https://example.com/follow/1234",
object=activitypubfollow.to_as2(),
object_=activitypubfollow,
)
@pytest.fixture
def activitypubpost():
with freeze_time("2019-04-27"):
return ActivitypubPost(
obj = make_content_class(Post)(
raw_content="# raw_content",
public=True,
provider_display_name="Socialhome",
@ -70,12 +73,14 @@ def activitypubpost():
actor_id=f"http://127.0.0.1:8000/profile/123456/",
_media_type="text/markdown",
)
obj.times={'edited':False, 'created':obj.created_at}
return obj
@pytest.fixture
def activitypubpost_diaspora_guid():
with freeze_time("2019-04-27"):
return ActivitypubPost(
obj = make_content_class(Post)(
raw_content="raw_content",
public=True,
provider_display_name="Socialhome",
@ -84,12 +89,14 @@ def activitypubpost_diaspora_guid():
actor_id=f"http://127.0.0.1:8000/profile/123456/",
guid="totallyrandomguid",
)
obj.times={'edited':False, 'created':obj.created_at}
return obj
@pytest.fixture
def activitypubpost_images():
with freeze_time("2019-04-27"):
return ActivitypubPost(
obj = make_content_class(Post)(
raw_content="raw_content",
public=True,
provider_display_name="Socialhome",
@ -97,16 +104,18 @@ def activitypubpost_images():
activity_id=f"http://127.0.0.1:8000/post/123456/#create",
actor_id=f"http://127.0.0.1:8000/profile/123456/",
_children=[
ActivitypubImage(url="foobar", media_type="image/jpeg"),
ActivitypubImage(url="barfoo", name="spam and eggs", media_type="image/jpeg"),
models.Image(url="foobar", media_type="image/jpeg"),
models.Image(url="barfoo", name="spam and eggs", media_type="image/jpeg"),
],
)
obj.times={'edited':False, 'created':obj.created_at}
return obj
@pytest.fixture
def activitypubpost_mentions():
with freeze_time("2019-04-27"):
return ActivitypubPost(
obj = make_content_class(Post)(
raw_content="""# raw_content\n\n@{someone@localhost.local} @{http://localhost.local/someone}""",
public=True,
provider_display_name="Socialhome",
@ -119,12 +128,14 @@ def activitypubpost_mentions():
"http://localhost.local/someone",
}
)
obj.times={'edited':False, 'created':obj.created_at}
return obj
@pytest.fixture
def activitypubpost_tags():
with freeze_time("2019-04-27"):
return ActivitypubPost(
obj = make_content_class(Post)(
raw_content="# raw_content\n#foobar\n#barfoo",
public=True,
provider_display_name="Socialhome",
@ -132,12 +143,14 @@ def activitypubpost_tags():
activity_id=f"http://127.0.0.1:8000/post/123456/#create",
actor_id=f"http://127.0.0.1:8000/profile/123456/",
)
obj.times={'edited':False, 'created':obj.created_at}
return obj
@pytest.fixture
def activitypubpost_embedded_images():
with freeze_time("2019-04-27"):
return ActivitypubPost(
obj = make_content_class(Post)(
raw_content="""
#Cycling #lauttasaari #sea #sun
@ -158,60 +171,68 @@ https://jasonrobinson.me/media/uploads/2019/07/16/daa24d89-cedf-4fc7-bad8-74a902
activity_id=f"http://127.0.0.1:8000/post/123456/#create",
actor_id=f"https://jasonrobinson.me/u/jaywink/",
)
obj.times={'edited':False, 'created':obj.created_at}
return obj
@pytest.fixture
def activitypubprofile():
return ActivitypubProfile(
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={
"private": "https://example.com/bob/private",
"public": "https://example.com/public",
}, public_key=PUBKEY, url="https://example.com/bob-bobertson"
)
@patch.object(models.base.Image, 'get_media_type', return_value="image/jpeg")
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,
tag_list=["socialfederation", "federation"], image_urls={
"large": "urllarge", "medium": "urlmedium", "small": "urlsmall"
}, inboxes={
"private": "https://example.com/bob/private",
"public": "https://example.com/public",
}, public_key=PUBKEY, url="https://example.com/bob-bobertson"
)
@pytest.fixture
def activitypubprofile_diaspora_guid():
return ActivitypubProfile(
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={
"private": "https://example.com/bob/private",
"public": "https://example.com/public",
}, public_key=PUBKEY, url="https://example.com/bob-bobertson",
guid="totallyrandomguid", handle="bob@example.com",
)
@patch.object(models.base.Image, 'get_media_type', return_value="image/jpeg")
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,
tag_list=["socialfederation", "federation"], image_urls={
"large": "urllarge", "medium": "urlmedium", "small": "urlsmall"
}, inboxes={
"private": "https://example.com/bob/private",
"public": "https://example.com/public",
}, public_key=PUBKEY, url="https://example.com/bob-bobertson",
guid="totallyrandomguid", handle="bob@example.com",
)
@pytest.fixture
def activitypubretraction():
with freeze_time("2019-04-27"):
return ActivitypubRetraction(
obj = Retraction(
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="Post",
)
return get_outbound_entity(obj, None)
@pytest.fixture
def activitypubretraction_announce():
with freeze_time("2019-04-27"):
return ActivitypubRetraction(
obj = Retraction(
target_id="http://127.0.0.1:8000/post/123456/activity",
activity_id="http://127.0.0.1:8000/post/123456/#delete",
actor_id="http://127.0.0.1:8000/profile/123456/",
entity_type="Share",
)
return get_outbound_entity(obj, None)
@pytest.fixture
def activitypubundofollow():
return ActivitypubFollow(
return models.Follow(
activity_id="https://localhost/undo",
actor_id="https://localhost/profile",
target_id="https://example.com/profile",

Wyświetl plik

@ -3,7 +3,7 @@ from unittest.mock import patch, Mock
import pytest
from federation.entities.activitypub.entities import ActivitypubFollow, ActivitypubPost
from federation.entities.activitypub.models import Follow, Note
from federation.tests.fixtures.payloads import (
ACTIVITYPUB_FOLLOW, ACTIVITYPUB_POST, ACTIVITYPUB_POST_OBJECT, ACTIVITYPUB_POST_OBJECT_IMAGES)
from federation.utils.activitypub import (
@ -53,24 +53,24 @@ class TestRetrieveAndParseDocument:
@patch("federation.utils.activitypub.fetch_document", autospec=True, return_value=(
json.dumps(ACTIVITYPUB_FOLLOW), None, None),
)
@patch.object(ActivitypubFollow, "post_receive")
@patch.object(Follow, "post_receive")
def test_returns_entity_for_valid_document__follow(self, mock_post_receive, mock_fetch):
entity = retrieve_and_parse_document("https://example.com/foobar")
assert isinstance(entity, ActivitypubFollow)
assert isinstance(entity, Follow)
@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):
entity = retrieve_and_parse_document("https://example.com/foobar")
assert isinstance(entity, ActivitypubPost)
assert isinstance(entity, Note)
@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):
entity = retrieve_and_parse_document("https://example.com/foobar")
assert isinstance(entity, ActivitypubPost)
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"
@ -80,7 +80,7 @@ class TestRetrieveAndParseDocument:
)
def test_returns_entity_for_valid_document__post__wrapped_in_activity(self, mock_fetch):
entity = retrieve_and_parse_document("https://example.com/foobar")
assert isinstance(entity, ActivitypubPost)
assert isinstance(entity, Note)
@patch("federation.utils.activitypub.fetch_document", autospec=True, return_value=('{"foo": "bar"}', None, None))
def test_returns_none_for_invalid_document(self, mock_fetch):